Symfony: [Config] ClassExistenceResource::isFresh() throws fatal error on php 7.2.20/7.3.7 if a parent class is missing

Created on 5 Jul 2019  ·  82Comments  ·  Source: symfony/symfony

Symfony version(s) affected: 3.4.29, 4.2.10, 4.3.2, 4.4-dev, 5.0-dev

Description
After upgrading to php 7.2.20 or 7.3.7, AutowirePass starts to throw ReflectionExceptions on cache warmup on installations with DoctrineBundle but without Twig and the Form and Validator components. This has been reported to the DoctrineBundle repository, but to me it rather looks like a DI issue.

How to reproduce

Bug DependencyInjection Needs Review

Most helpful comment

I think we should ask @nikic to consider reverting the patch on PHP. There are many situations like this one in Symfony apps that used to work seamlessly, but won't now.

That depends on what you're asking for here. I can revert this change from 7.2 and 7.3. Enforcing this is not critical for those branches and if it causes breakage in Symfony I have no problem with reverting it.

For PHP 7.4 though this change is going to stay in some form. I can invest some effort to avoid throwing a fatal error, but if the request here is to restore the old behavior where you say Foo implements Bar and you can end up with a class Foo that does not actually implement Bar by throwing an exception during autoloading, then no, I'm not going to restore that behavior.

All 82 comments

@teohhanhui reported that the unit tests of API platform 2.4 are failing because of a similar problem. This is what I get when I run them locally with php 7.3.7:

PHP Fatal error:  During class fetch: Uncaught ReflectionException: Class Psr\SimpleCache\CacheInterface not found in /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Psr16Cache.php:27
Stack trace:
#0 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php(160): require('/Volumes/Projec...')
#1 [internal function]: Symfony\Component\Debug\DebugClassLoader->loadClass('Symfony\\Compone...')
#2 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php(21): spl_autoload_call('Symfony\\Compone...')
#3 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php(160): require('/Volumes/Projec...')
#4 [internal function]: Symfony\Component\Debug\DebugClassLoader->loadClass('Symfony\\Compone...')
#5 [internal function]: spl_autoload_call('Symfony\\Compone...')
#6 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php(78): class_exists('Symfony\\Compone...')
#7 /Volumes/Project in /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Psr16Cache.php on line 27
PHP Stack trace:
PHP   1. {main}() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/phpunit:0
PHP   2. PHPUnit\TextUI\Command::main() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/phpunit:61
PHP   3. PHPUnit\TextUI\Command->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/TextUI/Command.php:162
PHP   4. PHPUnit\TextUI\TestRunner->doRun() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/TextUI/Command.php:206
PHP   5. PHPUnit\Framework\TestSuite->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:652
PHP   6. PHPUnit\Framework\TestSuite->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestSuite.php:746
PHP   7. PHPUnit\Framework\DataProviderTestSuite->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestSuite.php:746
PHP   8. ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter\BooleanFilterTest->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestSuite.php:746
PHP   9. PHPUnit\Framework\TestResult->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestCase.php:796
PHP  10. ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter\BooleanFilterTest->runBare() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestResult.php:693
PHP  11. ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter\BooleanFilterTest->setUp() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestCase.php:838
PHP  12. Symfony\Bundle\FrameworkBundle\Test\KernelTestCase::bootKernel() /Volumes/Projects/OpenSource/api-platform-core/src/Test/DoctrineOrmFilterTestCase.php:58
PHP  13. AppKernel->boot() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/framework-bundle/Test/KernelTestCase.php:69
PHP  14. AppKernel->initializeContainer() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/http-kernel/Kernel.php:133
PHP  15. AppKernel->dumpContainer() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/http-kernel/Kernel.php:571
PHP  16. Symfony\Component\DependencyInjection\Dumper\PhpDumper->dump() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/http-kernel/Kernel.php:740
PHP  17. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServices() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:220
PHP  18. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addService() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:860
PHP  19. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addInlineService() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:725
PHP  20. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServiceInstance() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:831
PHP  21. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addNewInstance() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:529
PHP  22. Symfony\Component\DependencyInjection\Dumper\PhpDumper->dumpValue() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:913
PHP  23. Symfony\Component\DependencyInjection\Definition->getErrors() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:1581
PHP  24. Symfony\Component\DependencyInjection\Compiler\AutowirePass->Symfony\Component\DependencyInjection\Compiler\{closure:/Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:384-386}() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Definition.php:908
PHP  25. Symfony\Component\DependencyInjection\Compiler\AutowirePass->createTypeNotFoundMessage() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:385
PHP  26. Symfony\Component\DependencyInjection\Compiler\AutowirePass->createTypeAlternatives() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:404
PHP  27. Symfony\Component\DependencyInjection\Compiler\AutowirePass->populateAvailableTypes() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:430
PHP  28. Symfony\Component\DependencyInjection\Compiler\AutowirePass->populateAvailableType() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:322
PHP  29. Symfony\Component\DependencyInjection\ContainerBuilder->getReflectionClass() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:336
PHP  30. Symfony\Component\Config\Resource\ClassExistenceResource->isFresh() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/ContainerBuilder.php:353
PHP  31. class_exists() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php:78
PHP  32. spl_autoload_call() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php:78
PHP  33. Symfony\Component\Debug\DebugClassLoader->loadClass() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php:78
PHP  34. require() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php:160
PHP  35. spl_autoload_call() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php:21
PHP  36. Symfony\Component\Debug\DebugClassLoader->loadClass() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php:21
PHP  37. require() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php:160

Fatal error: During class fetch: Uncaught ReflectionException: Class Psr\SimpleCache\CacheInterface not found in /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Psr16Cache.php:27
Stack trace:
#0 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php(160): require('/Volumes/Projec...')
#1 [internal function]: Symfony\Component\Debug\DebugClassLoader->loadClass('Symfony\\Compone...')
#2 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php(21): spl_autoload_call('Symfony\\Compone...')
#3 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php(160): require('/Volumes/Projec...')
#4 [internal function]: Symfony\Component\Debug\DebugClassLoader->loadClass('Symfony\\Compone...')
#5 [internal function]: spl_autoload_call('Symfony\\Compone...')
#6 /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php(78): class_exists('Symfony\\Compone...')
#7 /Volumes/Project in /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Psr16Cache.php on line 27

Call Stack:
    0.0003     404976   1. {main}() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/phpunit:0
    0.0193    1609784   2. PHPUnit\TextUI\Command::main() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/phpunit:61
    0.0193    1609896   3. PHPUnit\TextUI\Command->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/TextUI/Command.php:162
    1.4202   36554680   4. PHPUnit\TextUI\TestRunner->doRun() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/TextUI/Command.php:206
    1.4628   37159168   5. PHPUnit\Framework\TestSuite->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:652
   11.2949   67571272   6. PHPUnit\Framework\TestSuite->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestSuite.php:746
   11.2967   67571800   7. PHPUnit\Framework\DataProviderTestSuite->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestSuite.php:746
   11.2970   67572328   8. ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter\BooleanFilterTest->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestSuite.php:746
   11.2970   67572328   9. PHPUnit\Framework\TestResult->run() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestCase.php:796
   11.2971   67572328  10. ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter\BooleanFilterTest->runBare() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestResult.php:693
   11.2972   67588920  11. ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Filter\BooleanFilterTest->setUp() /Volumes/Projects/OpenSource/api-platform-core/vendor/phpunit/phpunit/src/Framework/TestCase.php:838
   11.2972   67588920  12. Symfony\Bundle\FrameworkBundle\Test\KernelTestCase::bootKernel() /Volumes/Projects/OpenSource/api-platform-core/src/Test/DoctrineOrmFilterTestCase.php:58
   11.2973   67589320  13. AppKernel->boot() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/framework-bundle/Test/KernelTestCase.php:69
   11.3328   67822144  14. AppKernel->initializeContainer() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/http-kernel/Kernel.php:133
   13.4362   87715560  15. AppKernel->dumpContainer() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/http-kernel/Kernel.php:571
   13.4398   88185024  16. Symfony\Component\DependencyInjection\Dumper\PhpDumper->dump() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/http-kernel/Kernel.php:740
   13.8365   92596272  17. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServices() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:220
   13.8404   92758624  18. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addService() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:860
   13.8405   92760224  19. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addInlineService() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:725
   13.8405   92760688  20. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServiceInstance() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:831
   13.8406   92761104  21. Symfony\Component\DependencyInjection\Dumper\PhpDumper->addNewInstance() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:529
   13.8406   92761560  22. Symfony\Component\DependencyInjection\Dumper\PhpDumper->dumpValue() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:913
   13.8406   92761936  23. Symfony\Component\DependencyInjection\Definition->getErrors() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Dumper/PhpDumper.php:1581
   13.8406   92761936  24. Symfony\Component\DependencyInjection\Compiler\AutowirePass->Symfony\Component\DependencyInjection\Compiler\{closure:/Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:384-386}() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Definition.php:908
   13.8406   92761936  25. Symfony\Component\DependencyInjection\Compiler\AutowirePass->createTypeNotFoundMessage() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:385
   13.8406   92762048  26. Symfony\Component\DependencyInjection\Compiler\AutowirePass->createTypeAlternatives() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:404
   13.8406   92762048  27. Symfony\Component\DependencyInjection\Compiler\AutowirePass->populateAvailableTypes() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:430
   13.8852   93801152  28. Symfony\Component\DependencyInjection\Compiler\AutowirePass->populateAvailableType() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:322
   13.8852   93801152  29. Symfony\Component\DependencyInjection\ContainerBuilder->getReflectionClass() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/Compiler/AutowirePass.php:336
   13.8852   93801232  30. Symfony\Component\Config\Resource\ClassExistenceResource->isFresh() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/dependency-injection/ContainerBuilder.php:353
   13.8852   93801416  31. class_exists() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php:78
   13.8852   93801496  32. spl_autoload_call() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php:78
   13.8852   93801576  33. Symfony\Component\Debug\DebugClassLoader->loadClass() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/config/Resource/ClassExistenceResource.php:78
   13.8856   93804456  34. require('/Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php') /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php:160
   13.8858   93805248  35. spl_autoload_call() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php:21
   13.8858   93805312  36. Symfony\Component\Debug\DebugClassLoader->loadClass() /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Simple/Psr6Cache.php:21
   13.8866   93845512  37. require('/Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/cache/Psr16Cache.php') /Volumes/Projects/OpenSource/api-platform-core/vendor/symfony/debug/DebugClassLoader.php:160

I've managed to build a minimal reproducer.

https://github.com/derrabus/symfony-32395-reproducer

The problem seems to be ClassExistenceResource from the Config component. When calling isFresh(), a temporary class loader is registered:

https://github.com/symfony/symfony/blob/6811aaa8e087665769dd66c69fb3109d002196ef/src/Symfony/Component/Config/Resource/ClassExistenceResource.php#L71-L73

If a class cannot be loaded because its parent is not found, a ReflectionException is thrown:

https://github.com/symfony/symfony/blob/6811aaa8e087665769dd66c69fb3109d002196ef/src/Symfony/Component/Config/Resource/ClassExistenceResource.php#L121

Previously, this exception could be caught and handled. Since php 7.2.20/7.3.7 however, php seems to catch exceptions thrown during class loading and turns them into a fatal error that cannot be caught.

When a class implements an interface that can not be found, PHP 7.2.20/7.3.7 throws an exception as well now. See https://github.com/symfony/symfony/issues/32396

When a class implements an interface that can not be found, PHP 7.2.20/7.3.7 throws an exception as well now. See #32396

No, php itself has always triggered an uncatchable fatal error, see https://3v4l.org/jogPE

The trick in ClassExistenceResource was to register a temporary proxy class loader that throws an exception instead that could be caught and handled. This does not work anymore, see https://3v4l.org/sSSJQ

The new php bahavior seems fine to me. Can't we fix our code to not rely on such strange behavior?

The trick in ClassExistenceResource was to register a temporary proxy class loader that throws an exception instead that could be caught and handled.

Correct, and that worked fine in most situation.

The new php bahavior seems fine to me. Can't we fix our code to not rely on such strange behavior?

The error is now fatal, right? Why is it better, when the previous behavior allowed some resilience? About the strange code, sure, PHP is a runtime before a language. So: which code can we write to circumvent a fatal error?

I think we should ask @nikic to consider reverting the patch on PHP. There are many situations like this one in Symfony apps that used to work seamlessly, but won't now.

I hope I'm missing something and there's a workaround :)

The optional services might not rely on the container optimization removing unused services but those services should only be loaded then the dependency is present. But sadly many bundles would be affected.

I think we should ask @nikic to consider reverting the patch on PHP. There are many situations like this one in Symfony apps that used to work seamlessly, but won't now.

That depends on what you're asking for here. I can revert this change from 7.2 and 7.3. Enforcing this is not critical for those branches and if it causes breakage in Symfony I have no problem with reverting it.

For PHP 7.4 though this change is going to stay in some form. I can invest some effort to avoid throwing a fatal error, but if the request here is to restore the old behavior where you say Foo implements Bar and you can end up with a class Foo that does not actually implement Bar by throwing an exception during autoloading, then no, I'm not going to restore that behavior.

Reverted from 7.2 and 7.3 via https://github.com/php/php-src/commit/22ed362810c1b3a5ecb54ebd1d50d804c7fc3159, this change is only in 7.4 now.

I can invest some effort to avoid throwing a fatal error

Having thought about this for a bit, I think that covariance support in PHP 7.4 effectively makes this impossible. Due to cyclic dependencies, we sometimes have to link classes under the provision that other classes will either also successfully link or else be unusable. This means that we can't just unregister the class stub if a parent/interface is not found, because then it would be possible to register a class with different methods under the same name and thus violate variance assumptions. The only thing we could do is leave behind a dead class stub, such that the class can neither be used, nor a class using the same name declared, which seems like a pretty bad idea.

@nikic Can we find another way to make autoloading failures recoverable somehow? To stay in your example, what the ClassExistenceResource tries to do here is to find out if the class Foo is available. If the answer is no because the interface Bar is missing, that's fine. Nobody wants an incomplete class in that case.

@derrabus I don't think so. Let me try to give a specific example of the problem I have in mind, though it's somewhat convoluted:

// A.php
class A {
    public function foo($x): B {}
}
// B.php
class B extends A {
    public function foo($x): C {}
}
// C.php
class C extends B implements DoesNotExist {
}
// main.php
new C;

What happens now is the following:

  1. Autoload C. The class C is registered provisionally.
  2. Autoload the parent B. The class B is registered provisionally.
  3. Autoload the parent A. The class A is registered provisionally.
  4. The class A is verified (trivially) and registered fully.
  5. The class B is verified and registered fully (and may already be used). During the verification it makes use of the fact that C is a subclass of B, otherwise the return types would not be covariant.
  6. Autoload the interface DoesNotExist, which throws an exception.

After these steps have happened ... what can we do now? We can't fully register the class C, because the interface it implements does not exist. We can't remove the provisionally registered class C either, because then a new class C that is not a subclass of B could be registered, and thus violate the variance assumption we made above.

I don't really see a solution here that would allow us to gracefully recover while performing inheritance :(

I think the correct thing to do is to remove both B and C, as they're both invalid.

@teohhanhui The class B cannot be removed at the point where we realize that the interface does not exist, because the class could already be in use. While mixing declarations and code goes against modern coding guidelines, from the language perspective writing code like this is allowed:

// B.php
class B extends A {
    public function foo($x): C {}
}
$b = new B;

Of course, we can't drop the class B after someone has already created an object of it, or similar (a more "typical" use is via class_alias.)

Does it make sense then to only fail if class B is in use (provided there's a way to tell)?

Thank you for the detailed explanation, @nikic! Return type contravariance really complicates this. 😕

Hey @derrabus , glad that YOU are on it ;) !! We're deploying on Heroku and it's using PHP 7.2.20 / PHP 7.3.7. There's no simple option to change that. The "-latest" official docker hub release for PHP also stays at .19 / .6. Effect: we currently cannot deploy anything. Is there a chance that this is rolled back very soon (@nikic) or fixed by any change in Symfony? I'm a little shocked that this "breaking change" in PHP simply went upstream since Symfony+Doctrine is for sure not an unusual use / test case in the PHP world... 😱

@elmariachi111 I don't know how Heroku handles a situation like this, but there has to be a way to deploy to an older php release. Have you contacted their support yet?

If there really isn't another way, my suggestion would be:

  • Have a look at the error message you're getting. It should complain about a missing class or interface.
  • Find out, which package it belongs to (symfony/form, symfony/validator and psr/simple-cache are hot candidates).
  • Add that package to your dependencies.
  • If Flex makes any changes to your app because of the added packages, revert them.

That'll bloat your vendors folder, but you should be able to run your application again. Please remember to revert that change once php 7.2.21 and 7.3.8 are out.

yep, that worked -> adding symfony/form contained the FormTypeGuesserInterface that was supposedly missing but set as return type in Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser :D

Another solution would be fixing the doctrine bundle with registering the SymfonyBridge\Doctrine\Form\DoctrineOrmTypeGuesser only as a service if the FormTypeGuesserInterface exists.

@smoench that would be an option, yes. However, there are a few more such services that were ok previously because they would be removed from the container and were never used. If this can't be solved in the DI container, I'm open for such a change, but I'd rather not do that preemptively if it can be avoided.

Also, this issue affects other bundles as well, such as API platform.

I know many other bundles would be affected. Another option would be disable autowire on those affected services. But lets wait if PHP 7.4 could provide us something.

I am experiencing this same issue after updating to 7.2.20 this morning. FYI I do have the Twig, Form, and Validator dependencies that are mentioned in the OP. I am seeing the error thrown by a Test class in a custom vendor dependency that my team is responsible for--I am not sure why a dependency's test files are being parsed for caching.

Anyways, after reading the comments above, I am slightly confused. Do we think solving this requires making a change to Symfony or to PHP itself? If the later, are we really thinking the fix would only be deployed to 7.4.x and not 7.2.x and 7.3.x, or am I misundertsanding your most recent post @smoench? If I'm not, that would be highly unfortunate...

@arderyp The issue is already resolved in the upcoming PHP 7.2 and 7.3 releases. The remaining discussion is about what to do for PHP 7.4, where under my current understanding a change on the Symfony side will be necessary.

ahh, okay @nikic, thanks a lot for the clarification, and thanks everyone else for sorting this issue out.

Instead of using PHP RefelctionClass/class_exists which triggering autoload we might could adapt https://github.com/Roave/BetterReflection. It analyse code without triggering autoload.

You can reflect on classes that are not already loaded, without loading them
Ability to reflect on classes directly from a string of PHP code

@derrabus Any updates on this issue?

Instead of using PHP RefelctionClass/class_exists which triggering autoload we might could adapt https://github.com/Roave/BetterReflection. It analyse code without triggering autoload.

And it's even compatible with PHP's Reflection API: https://github.com/Roave/BetterReflection/blob/master/docs/compatibility.md

This bug takes 5 hours to get me here ;/

Just for information, I experience this bug because of symfony/security-bundle which enforce a few services definitions to be loaded for various templating engines. In the SecurityExtension class it calls $loader->load('templating_php.xml'); which leads the container to have services which references classes from the symfony/templating component (which is not installed).

@pounard Hmm... I'm guessing you're on Symfony 4.2. Looks like it's been fixed already: https://github.com/symfony/symfony/pull/32379

@teohhanhui it's not released yet

@teohhanhui yes I'm using 4.2 @Tobion thanks for the info.

This topic looks like collection of complaints, but I for one am glad this PHP fix helped us discover this. It uncovered wrongly configured DI, with missing exclude folders in our case, unnecessarily making DI crawl through them. Additionally, these classes shouldn't have even be available in production composer classmap, since they are working only when no --no-dev is used. So I applaud @nikic for keeping this in some form in PHP 7.4

@nikic regarding your example from above https://github.com/symfony/symfony/issues/32395#issuecomment-509593220

Would it be possible to mark a provisionally registered class as “in use” as soon as it is referenced somewhere?

This way we could determine in an exception case if it is OK to remove the provisionally registered class and gracefully recover or if we have to throw a fatal error.

In your example this would mean that once class B is verified it would mark class C as “in use”. When loading DoesNotExist throws the exception then, we would throw a fatal error because class C is “in use”.

This way we could gracefully recover for most cases while resulting in a fatal error for the impossible cases, which sounds like a perfect trade-off to me.

For reference, the core logic we need here is a way to make class checks resilient to missing parent classes. This means only these functions are concerned: get_class_methods, get_class_vars, get_parent_class, is_a, is_subclass_of, class_exists, class_implements, class_parents, trait_exists, defined, interface_exists, method_exists, property_exists, is_callable. We need to call them and have them return false in the case that currently crashes.

I don't see why, it's ok to make DI crash when trying to crawl through invalid classes

For anyone in the dark regarding how to fix this if you're _NOT_ running an automated, out-of-the-box pipeline, i.e. not on Heroku: https://github.com/docker-library/php/issues/864#issuecomment-511036456

If you're not on docker, kick your PHP version down one notch to 7.2.19 or 7.3.6.

Would it be possible to mark a provisionally registered class as “in use” as soon as it is referenced somewhere?

@nikic this proposal seems like a nice idea, WDYT? The current situation is pretty scary for 7.4: it's quite common to check for optional dependencies with a class_exists(), with resiliency relying on the possibility to recover from missing parents.

it's ok to make DI crash when trying to crawl through invalid classes

@ostrolucky this logic exists in Config and in Cache components. Fixing the symptoms in DI still leaves the root issue open, which is what we're discussing here.

I have the same issue but with FixturesBundle

PHP Fatal error: During class fetch: Uncaught ReflectionException: Class DoctrineBundle\FixturesBundle\Fixture not found

poc is here:
https://github.com/mmarton/symfony-php7.2.20

might be a php bug related to the fix:
https://bugs.php.net/bug.php?id=76980

I have this issue when upgrading to php 7.2.20

PHP Fatal error: During class fetch: Uncaught ReflectionException: Class SymfonyComponent\Templating\Helper\Helper not found in /var/www/html/project/vendor/symfony/security-bundle/Templating/Helper/LogoutUrlHelper.php:23

class LogoutUrlHelper extends Helper <<< class Helper does not exists

7.3.8RC1 was tagged today

And 7.2.21 RC1 as well. Yay. 🎉

I try to find a workaround with composer config platform but it dosn't work for "php" : "7.2.19" with 7.2.20 installed !

Hey @karousn , the problem in the PHP version, not in Composer dependencies. Composer config platform version only affects installed dependencies. Just upgrade to 7.2.21 or downgrade to 7.2.19 your PHP version and do nothing in your project

Hey @karousn , the problem in the PHP version, not in Composer dependencies. Composer config platform version only affects installed dependencies. Just upgrade to 7.2.21 or downgrade to 7.2.19 your PHP version and do nothing in your project

How to downgrade 7.2.20 to 7.2.19 ? i can't find any example :(
i'm not using docker

@arturslogins, you might have to uninstall and reinstall, which could be a pain if you have lots of complexity in dependencies and configurations. Hopefully 7.2.21/7.3.8 are tagged and published soon (I don't know how long it typically takes after RCs are tagged)

Hey @karousn , the problem in the PHP version, not in Composer dependencies. Composer config platform version only affects installed dependencies. Just upgrade to 7.2.21 or downgrade to 7.2.19 your PHP version and do nothing in your project

Thanks for explanation @bocharsky-bw , so after some search to downgrade the php version to 7.2.19 you have two choices :

  • Wait for oerdnj/deb.sury.org#1208 if you used as ppa under debian fork distribution
  • Do the Apt pinning by your self, if you realy understand what you do.

As example :
apt-cache policy php7.2 # to check if you have php7.2.19 in version table
sudo nano /etc/apt/preferences.d/php72 # create a file to add preferences

Put this in

Package: php7.2*
Pin: version 7.2.19-0ubuntu0.18.04.1
Pin-Priority: 1200

After that you need to update & upgrade
sudo apt update -y
sudo apt upgrade

it well be the same for 7.3 version.

Hey @karousn , the problem in the PHP version, not in Composer dependencies. Composer config platform version only affects installed dependencies. Just upgrade to 7.2.21 or downgrade to 7.2.19 your PHP version and do nothing in your project

Thanks for explanation @bocharsky-bw , so after some search to downgrade the php version to 7.2.19 you have two choices :

As example :
apt-cache policy php7.2 # to check if you have php7.2.19 in version table
sudo nano /etc/apt/preferences.d/php72 # create a file to add preferences

Put this in

Package: php7.2*
Pin: version 7.2.19-0ubuntu0.18.04.1
Pin-Priority: 1200

After that you need to update & upgrade
sudo apt update -y
sudo apt upgrade

it well be the same for 7.3 version.

Thanks for replay

@karousn Unfortunately debian repos of sury.org host only the latest version of PHP :(

@nikic wouldn't loading the class B also load the class C to perform variance checks, before you instantiate the B object ?

Hey @karousn , the problem in the PHP version, not in Composer dependencies. Composer config platform version only affects installed dependencies. Just upgrade to 7.2.21 or downgrade to 7.2.19 your PHP version and do nothing in your project

Thanks for explanation @bocharsky-bw , so after some search to downgrade the php version to 7.2.19 you have two choices :

* Wait for [oerdnj/deb.sury.org#1208](https://github.com/oerdnj/deb.sury.org/issues/1208) if you used as ppa under debian fork distribution

* Do the [Apt pinning](https://wiki.debian.org/AptPreferences) by your self, if you realy understand what you do.

As example :
apt-cache policy php7.2 # to check if you have php7.2.19 in version table
sudo nano /etc/apt/preferences.d/php72 # create a file to add preferences

Put this in

Package: php7.2*
Pin: version 7.2.19-0ubuntu0.18.04.1
Pin-Priority: 1200

After that you need to update & upgrade
sudo apt update -y
sudo apt upgrade

it well be the same for 7.3 version.

@karousn this only works if you haven't already updated to 7.2.20 though, right? In other words, this won't roll your PHP back, but prevent the 7.2.19 PHP from being upgraded to 7.2.20. Or is that wrong?

@arderyp : no it will also roll back php version if you installed 7.2.20 exactly when you execute sudo apt upgrade it will ask you for downgrade your php version to 7.2.19

@karousn, good to know, thanks for confirming!

For those who can't wait and are under debian 9 using sury.org depot. I manage to downgrade my php installation with the cached debs into /var/cache/apt/archive :

sudo dpkg -i /var/cache/apt/archives/php7.2-*_7.2.19*.deb

Then normally you have to sudo apt-mark hold php7.2-* to avoid upgrade but maybe we just have to wait few days to be able to upgrade to php 7.2.21.

This will works only if you need php-fpm and php-cli but if you are using apache then you have to also install the libapache2-mod-php7.2 deb and dependancies

I've ran a quick test with my reproducer on php 7.3.8RC1. Looks like this issue is gone. 🎉

(edit: 7.2.21RC1 is fine as well.)

All right, before this evolves into a StackOverflow thread on how to install specific php versions on various platforms:

  • If you encounter this problem, stay on php 7.2.19 or 7.3.6 untill 7.2.21 and 7.3.8 are released and packaged by your favorite package manager.
  • If you are brave enough, you can download the RC from here and compile it yourself. 😃
  • I you have already upgraded to 7.2.20/7.3.7 and cannot roll back or (or don't know how to do that), here's a workaround that allows you to use php 7.2.20/7.3.7 in the meantime.
  • This is still broken with php 7.4, but I am certain that we will find a solution before 7.4 gets a stable release.

For all people using homebrew (on MacOS), you can use this formula.
Copying it to your Formula folder will allow you to install [email protected] (instead of only the [email protected] option which installs 7.2.20).

Simply run this:

curl https://gist.githubusercontent.com/bossan/1cad9a8819808d1e684898af38541e52/raw/e05632cf98d9aff4f489422f4413d2a0678abfd0/[email protected] > /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/[email protected] && brew install [email protected]

api-platform users (im on mac using brew 7.3.7) - I had to add this to my composer.json file and all works after an update

"symfony/form": "4.2.*",

I still do not understand. What if we run symfony 4.1 and want to upgrade to php 7.3, installing form package only create more issues like upgrading symfony + 60+ packages which may take days.
Is there any soft salvation?


Error: During class fetch: Uncaught ReflectionException: Class Symfony\Component\Form\FormTypeGuesserInterface not found in /var/www/hint/vendor/symfony/doctrine-bridge/Form/DoctrineOrmTypeGuesser.php:25
Stack trace:
#0 /var/www/hint/vendor/symfony/debug/DebugClassLoader.php(145): require('/var/www/hint/v...')
#1 [internal function]: Symfony\Component\Debug\DebugClassLoader->loadClass('Symfony\\Bridge\\...')
#2 [internal function]: spl_autoload_call('Symfony\\Bridge\\...')
#3 /var/www/hint/vendor/symfony/config/Resource/ClassExistenceResource.php(76): class_exists('Symfony\\Bridge\\...')
#4 /var/www/hint/vendor/symfony/dependency-injection/ContainerBuilder.php(349): Symfony\Component\Config\Resource\ClassExistenceResource->isFresh(0)
#5 /var/www/hint/vendor/symfony/dependency-injection/Compiler/AutowirePass.php(312): Symfony\Component\DependencyInjection\ContainerBuilder->getReflectionClass('Symfony\\Bridge\\...', false)
#6 /var/www/hint/vendor/symfony/dependency-injection/Compiler/AutowirePass.php(298): Symfony\Com

The problem isn't in symfony, it's in the latest php releases.

If you encounter this problem, stay on php 7.2.19 or 7.3.6 untill 7.2.21 and 7.3.8 are released and packaged by your favorite package manager. https://github.com/symfony/symfony/issues/32395#issuecomment-512781215

@mmarton I saw php bug report, is there any solution except installing form component?

@BonBonSlick Just wait for PHP 7.2.21 or 7.3.8

@teohhanhui got to downgrade back to php7.2. < 7.20 :(

@BonBonSlick installing form component isn't even fix it every time. I have this issue with doctrine fixtures bundle.

You could try to separate service loading for any services that depend on any composer dev package, and only include that config if app_env=dev. And event this could fail if the form component is a dependency of a dependency...

@mmarton too tricky, just rolled back already

For Ubuntu and Debian users you can now update your php binaries, oerdnj fix it :
https://github.com/oerdnj/deb.sury.org/issues/1208#issuecomment-515014129

Filed a feature request in PHP:
https://bugs.php.net/bug.php?id=78351

Remove Entity from exclude list in service.yaml fix for me.

`e̶x̶c̶l̶u̶d̶e̶:̶ ̶'̶.̶.̶/̶s̶r̶c̶/̶{̶D̶e̶p̶e̶n̶d̶e̶n̶c̶y̶I̶n̶j̶e̶c̶t̶i̶o̶n̶,̶E̶n̶t̶i̶t̶y̶,̶M̶i̶g̶r̶a̶t̶i̶o̶n̶s̶,̶T̶e̶s̶t̶s̶,̶K̶e̶r̶n̶e̶l̶.̶p̶h̶p̶}̶'̶`

`exclude: '../src/{DependencyInjection,Migrations,Tests,Kernel.php}'`

https://github.com/php/php-src/releases
7.2.21 & 7.3.8 released

released two days ago, and yet still now showing on the official PHP downloads page: https://www.php.net/downloads.php

hopefully the various distros will distribute the updates soon.

released two days ago, and yet still now showing on the official PHP downloads page: https://www.php.net/downloads.php

hopefully the various distros will distribute the updates soon.

It's tagged but not released just yet. Releases tend to happen on Thursdays if I'm not mistaken, so likely tomorrow :)

PHP 7.2.21 doesn't include 78351 as a fixed issue at at https://www.php.net/ChangeLog-7.php#7.2.21, and https://bugs.php.net/bug.php?id=78351 is still listed as open - is there any way of verifying it's been fixed in there?
(Admittedly I could rebuild my system & test but it seems more efficient to understand if there's a canonical way of seeing if it's gone into PHP 7.2 master)

It contains the revert of the fix that caused the issue.
https://github.com/php/php-src/commit/22ed362810c1b3a5ecb54ebd1d50d804c7fc3159

Excellent, thanks @mmarton

looks like the Remi repo is distributing the new releases, just now waiting on Ondrej for the Debian/Ubuntu users.

@arderyp Ondřej provides patched packages since Jul 25 as per https://github.com/oerdnj/deb.sury.org/issues/1208#issuecomment-515014129

@discordier, right, patched, but not latest releases

Closing as this has been fixed in PHP.
For PHP 7.4 support, see #32995

Was this page helpful?
0 / 5 - 0 ratings