doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.93k stars 2.52k forks source link

`enable_lazy_ghost_objects: true` prevents detection of proxy creation errors at Symfony compile time #11402

Open chrif opened 7 months ago

chrif commented 7 months ago

enable_lazy_ghost_objects: false is deprecated, but when it is true, clearing the Symfony cache is not enough to get the Unable to create a proxy for a readonly class error. It is only when the proxy class is used that we get it.

So in dev we have auto_generate_proxy_classes: true and we didn't get an error when developing. I guess the problematic proxy was not used in the development use case.

In production we have auto_generate_proxy_classes: false as recommended. We also have a functional test to check that the prod Symfony cache can be cleared. It passed the test.

It's only when we released and used the proxy class that we got the Unable to create a proxy for a readonly class.

With enable_lazy_ghost_objects: false everything works fine, but we have a deprecation message.

stof commented 7 months ago

Can you share the stack trace of the exception you got to see when it was triggered in each case ?

chrif commented 7 months ago

Below you will find my initial reply, but I found the trigger in production while trying to get the full stack trace. It is the opcache preload.php file that triggers the error. If I comment opcache.preload=/app/config/preload.php in php.ini, I don't get the error, and if I keep it but set APP_DEBUG=1, I don't get the error either.

Versions

doctrine/doctrine-bundle              2.11.3
doctrine/orm                          2.19.0
symfony/framework-bundle              v6.4.4

Stack trace for the prod cache clear functional test when it fails with enable_lazy_ghost_objects: false

In InvalidArgumentException.php line 102:

  [Doctrine\Common\Proxy\Exception\InvalidArgumentException]                   
  Unable to create a proxy for a readonly class "App\Entity\MyEntity".              

Exception trace:
  at /app/vendor/doctrine/common/src/Proxy/Exception/InvalidArgumentException.php:102
 Doctrine\Common\Proxy\Exception\InvalidArgumentException::classMustNotBeReadOnly() at /app/vendor/doctrine/common/src/Proxy/ProxyGenerator.php:372
 Doctrine\Common\Proxy\ProxyGenerator->verifyClassCanBeProxied() at /app/vendor/doctrine/common/src/Proxy/ProxyGenerator.php:312
 Doctrine\Common\Proxy\ProxyGenerator->generateProxyClass() at /app/vendor/doctrine/common/src/Proxy/AbstractProxyFactory.php:146
 Doctrine\Common\Proxy\AbstractProxyFactory->generateProxyClasses() at /app/vendor/doctrine/orm/src/Proxy/ProxyFactory.php:228
 Doctrine\ORM\Proxy\ProxyFactory->generateProxyClasses() at /app/vendor/symfony/doctrine-bridge/CacheWarmer/ProxyCacheWarmer.php:60
 Symfony\Bridge\Doctrine\CacheWarmer\ProxyCacheWarmer->warmUp() at /app/vendor/symfony/http-kernel/CacheWarmer/CacheWarmerAggregate.php:108
 Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate->warmUp() at /app/vendor/symfony/http-kernel/Kernel.php:545
 Symfony\Component\HttpKernel\Kernel->initializeContainer() at /app/vendor/symfony/http-kernel/Kernel.php:763
 Symfony\Component\HttpKernel\Kernel->preBoot() at /app/vendor/symfony/http-kernel/Kernel.php:126
 Symfony\Component\HttpKernel\Kernel->boot() at /app/library/Common/Application/Kernel.php:34
 App\Kernel->boot() at /app/vendor/symfony/http-kernel/Kernel.php:144
 Symfony\Component\HttpKernel\Kernel->reboot() at /app/vendor/symfony/framework-bundle/Command/CacheClearCommand.php:229
 Symfony\Bundle\FrameworkBundle\Command\CacheClearCommand->warmup() at /app/vendor/symfony/framework-bundle/Command/CacheClearCommand.php:141
 Symfony\Bundle\FrameworkBundle\Command\CacheClearCommand->execute() at /app/vendor/symfony/console/Command/Command.php:326
 Symfony\Component\Console\Command\Command->run() at /app/vendor/symfony/console/Application.php:1096
 Symfony\Component\Console\Application->doRunCommand() at /app/vendor/symfony/framework-bundle/Console/Application.php:126
 Symfony\Bundle\FrameworkBundle\Console\Application->doRunCommand() at /app/vendor/symfony/console/Application.php:324
 Symfony\Component\Console\Application->doRun() at /app/vendor/symfony/framework-bundle/Console/Application.php:80
 Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /app/vendor/symfony/console/Application.php:175
 Symfony\Component\Console\Application->run() at /app/vendor/symfony/runtime/Runner/Symfony/ConsoleApplicationRunner.php:49
 Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner->run() at /app/vendor/autoload_runtime.php:39
 require_once() at /app/application/console:8

cache:clear [--no-warmup] [--no-optional-warmers] [--maintenance-override MAINTENANCE-OVERRIDE]

In production when it fails with enable_lazy_ghost_objects: true I don't have the full stack trace. If I use APP_DEBUG=true, I don't get the error. If it's not enough, I'll have to figure this out.

<b>Fatal error</b>:  Non-readonly class Proxies\__CG__\App\Entity\MyEntity cannot extend readonly class App\Entity\MyEntity in <b>/app/var/cache/production/doctrine/orm/Proxies/__CG__AppEntityMyEntity.php</b> on line <b>8</b><br />
stof commented 7 months ago

Indeed, the new implementation of ProxyFactory does not seem to validate the compatibility of classes at proxy generation time, but only when the proxy class is used.

/cc @nicolas-grekas

ostrolucky commented 7 months ago

I guess this is an issue in var-dumper? Shall we close here?

nicolas-grekas commented 7 months ago

I think this is for the ORM. VarExporter's lazy objects are compatible with readonly classes. But I'm not sure the ORM takes care of them properly (either failing early or generaton readonly proxies.)

derrabus commented 7 months ago

Can you create a reproducer for this issue?

chrif commented 7 months ago

Can you create a reproducer for this issue?

I'll make time next week for that.

beberlei commented 7 months ago

The error "the Unable to create a proxy for a readonly class" is only triggered when lazy ghost objects is false during ProxyFactory::generateProxyClasses. There is no other way this exception inside the ProxyGenerator can be triggered, because its from the old code.

https://github.com/doctrine/doctrine2/blob/9d1a4973ae2d8ddedd6b66e958e32ebb58cdf3d6/src/Proxy/ProxyFactory.php#L227-L229

beberlei commented 7 months ago

Some hints, A reproducer for the fatal error with lazy proxy should be a test with a readonly class, see @requires PHP 8.1 and other uses in the testsuite to restrict run for php with readonly classes. Call $this->_em->getReference() to create a proxy.