diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 8ffb10d4091f..e4f3a025d400 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -6,6 +6,12 @@ Cache * Added argument `$prefix` to `AdapterInterface::clear()` +Debug +----- + + * Deprecated `FlattenException`, use the `FlattenException` of the `ErrorRenderer` component + * Deprecated the whole component in favor of `ErrorHandler` component + DependencyInjection ------------------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 36ed24ff692a..e178f252cfce 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -51,6 +51,11 @@ Console $processHelper->run($output, Process::fromShellCommandline('ls -l')); ``` +Debug +----- + + * Removed the component + DependencyInjection ------------------- diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index bb1e8ab4c2f0..803f7114b812 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -18,7 +18,8 @@ use PHPUnit\Util\Blacklist; use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Bridge\PhpUnit\DnsMock; -use Symfony\Component\Debug\DebugClassLoader; +use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; +use Symfony\Component\ErrorHandler\DebugClassLoader; /** * PHP 5.3 compatible trait-like shared implementation. @@ -53,7 +54,7 @@ public function __construct(array $mockedNamespaces = array()) Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait'] = 2; } - $enableDebugClassLoader = class_exists('Symfony\Component\Debug\DebugClassLoader'); + $enableDebugClassLoader = class_exists(DebugClassLoader::class) || class_exists(LegacyDebugClassLoader::class); foreach ($mockedNamespaces as $type => $namespaces) { if (!\is_array($namespaces)) { @@ -74,7 +75,11 @@ public function __construct(array $mockedNamespaces = array()) } } if ($enableDebugClassLoader) { - DebugClassLoader::enable(); + if (class_exists(DebugClassLoader::class)) { + DebugClassLoader::enable(); + } else { + LegacyDebugClassLoader::enable(); + } } if (self::$globallyEnabled) { $this->state = -2; diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 1518be6503b5..6d06cb3fefc5 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -21,7 +21,7 @@ "php": ">=5.5.9" }, "suggest": { - "symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" }, "conflict": { "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 319b9be9dd36..0fdb7ecd44ab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -19,8 +19,8 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index f9a9d56b5652..b6ae6b7ecf22 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -29,10 +29,10 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass; use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; -use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\ErrorRenderer\DependencyInjection\ErrorRendererPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\Form\DependencyInjection\FormPass; diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index cb4bec153ce7..1cc09092b34d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -70,7 +70,6 @@ "symfony/asset": "<3.4", "symfony/browser-kit": "<4.3", "symfony/console": "<4.3", - "symfony/debug": "<4.4", "symfony/dotenv": "<4.2", "symfony/dom-crawler": "<4.3", "symfony/form": "<4.3", diff --git a/src/Symfony/Component/Debug/BufferingLogger.php b/src/Symfony/Component/Debug/BufferingLogger.php index e7db3a4ce4c6..6e308f224728 100644 --- a/src/Symfony/Component/Debug/BufferingLogger.php +++ b/src/Symfony/Component/Debug/BufferingLogger.php @@ -13,10 +13,14 @@ use Psr\Log\AbstractLogger; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', BufferingLogger::class, \Symfony\Component\ErrorHandler\BufferingLogger::class), E_USER_DEPRECATED); + /** * A buffering logger that stacks logs for later. * * @author Nicolas Grekas + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\BufferingLogger instead. */ class BufferingLogger extends AbstractLogger { diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index 437113854e66..b2e17f38211a 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * deprecated `FlattenException`, use the `FlattenException` of the `ErrorRenderer` component + * deprecated the whole component in favor of the `ErrorHandler` component 4.3.0 ----- diff --git a/src/Symfony/Component/Debug/Debug.php b/src/Symfony/Component/Debug/Debug.php index 5d2d55cf9f02..788ad7d9243f 100644 --- a/src/Symfony/Component/Debug/Debug.php +++ b/src/Symfony/Component/Debug/Debug.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', Debug::class, \Symfony\Component\ErrorHandler\Debug::class), E_USER_DEPRECATED); + /** * Registers all the debug tools. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Debug instead. */ class Debug { diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index ff9a8d72f903..84a94b691253 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', DebugClassLoader::class, \Symfony\Component\ErrorHandler\DebugClassLoader::class), E_USER_DEPRECATED); + /** * Autoloader checking if the class is really defined in the file found. * @@ -24,6 +26,8 @@ * @author Christophe Coevoet * @author Nicolas Grekas * @author Guilhem Niot + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\DebugClassLoader instead. */ class DebugClassLoader { diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index a99a000b07fa..0134a7142305 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -23,6 +23,8 @@ use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ErrorHandler::class, \Symfony\Component\ErrorHandler\ErrorHandler::class), E_USER_DEPRECATED); + /** * A generic ErrorHandler for the PHP engine. * @@ -47,6 +49,8 @@ * @author Grégoire Pineau * * @final since Symfony 4.3 + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ErrorHandler instead. */ class ErrorHandler { diff --git a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php index fa98c4975d02..21e2c0db53a9 100644 --- a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ClassNotFoundException::class, \Symfony\Component\ErrorHandler\Exception\ClassNotFoundException::class), E_USER_DEPRECATED); + /** * Class (or Trait or Interface) Not Found Exception. * * @author Konstanton Myakshin + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ClassNotFoundException instead. */ class ClassNotFoundException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 93880fbc323c..23c2ede7eb96 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorException::class, \Symfony\Component\ErrorHandler\Exception\FatalErrorException::class), E_USER_DEPRECATED); + /** * Fatal Error Exception. * * @author Konstanton Myakshin + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalErrorException instead. */ class FatalErrorException extends \ErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php index cdafb2a56842..d7d36ac17e31 100644 --- a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalThrowableError::class, \Symfony\Component\ErrorHandler\Exception\FatalThrowableError::class), E_USER_DEPRECATED); + /** * Fatal Throwable Error. * * @author Nicolas Grekas + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalThrowableError instead. */ class FatalThrowableError extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FlattenException.php b/src/Symfony/Component/Debug/Exception/FlattenException.php index eaed1f1fa6f9..f7bdcde9656c 100644 --- a/src/Symfony/Component/Debug/Exception/FlattenException.php +++ b/src/Symfony/Component/Debug/Exception/FlattenException.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "Symfony\Component\ErrorRenderer\Exception\FlattenException" instead.', FlattenException::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FlattenException::class, \Symfony\Component\ErrorRenderer\Exception\FlattenException::class), E_USER_DEPRECATED); /** * FlattenException wraps a PHP Error or Exception to be able to serialize it. diff --git a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php index fec197983645..5b02d52ad8bb 100644 --- a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php +++ b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', OutOfMemoryException::class, \Symfony\Component\ErrorHandler\Exception\OutOfMemoryException::class), E_USER_DEPRECATED); + /** * Out of memory exception. * * @author Nicolas Grekas + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException instead. */ class OutOfMemoryException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php index 236c56ed0e2e..7ed3d7eb5254 100644 --- a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php +++ b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', SilencedErrorContext::class, \Symfony\Component\ErrorHandler\Exception\SilencedErrorContext::class), E_USER_DEPRECATED); + /** * Data Object that represents a Silenced Error. * * @author Grégoire Pineau + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext instead. */ class SilencedErrorContext implements \JsonSerializable { diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php index d936c8759e36..fefd7d248eda 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionException::class, \Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException::class), E_USER_DEPRECATED); + /** * Undefined Function Exception. * * @author Konstanton Myakshin + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException instead. */ class UndefinedFunctionException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php index f627561fe16e..48559415868d 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug\Exception; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodException::class, \Symfony\Component\ErrorHandler\Exception\UndefinedMethodException::class), E_USER_DEPRECATED); + /** * Undefined Method Exception. * * @author Grégoire Pineau + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException instead. */ class UndefinedMethodException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index e36610c82e14..2be65ab6fe46 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -15,6 +15,8 @@ use Symfony\Component\Debug\Exception\OutOfMemoryException; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionHandler::class, \Symfony\Component\ErrorHandler\ExceptionHandler::class), E_USER_DEPRECATED); + /** * ExceptionHandler converts an exception to a Response object. * @@ -28,6 +30,8 @@ * @author Nicolas Grekas * * @final since Symfony 4.3 + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ExceptionHandler instead. */ class ExceptionHandler { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php index a0e2f770f001..8211d5d0e267 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -17,10 +17,14 @@ use Symfony\Component\Debug\Exception\ClassNotFoundException; use Symfony\Component\Debug\Exception\FatalErrorException; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ClassNotFoundFatalErrorHandler::class, \Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler::class), E_USER_DEPRECATED); + /** * ErrorHandler for classes that do not exist. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler instead. */ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php index 6b87eb30a126..a1c4a8ce547c 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php @@ -13,10 +13,14 @@ use Symfony\Component\Debug\Exception\FatalErrorException; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorHandlerInterface::class, \Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface::class), E_USER_DEPRECATED); + /** * Attempts to convert fatal errors to exceptions. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface instead. */ interface FatalErrorHandlerInterface { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php index 9eddeba5a64a..dc7cb85bf360 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -14,10 +14,14 @@ use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\UndefinedFunctionException; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionFatalErrorHandler::class, \Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler::class), E_USER_DEPRECATED); + /** * ErrorHandler for undefined functions. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler instead. */ class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php index 1318cb13baf8..5972327c498a 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php @@ -14,10 +14,14 @@ use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\UndefinedMethodException; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodFatalErrorHandler::class, \Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler::class), E_USER_DEPRECATED); + /** * ErrorHandler for undefined methods. * * @author Grégoire Pineau + * + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler instead. */ class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface { diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 6ee20ae8a7e4..6cca7a2e19a3 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -14,6 +14,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Debug\DebugClassLoader; +/** + * @group legacy + */ class DebugClassLoaderTest extends TestCase { /** diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index f758d21e0e98..c9c28097d60f 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -25,6 +25,8 @@ * * @author Robert Schönthal * @author Nicolas Grekas + * + * @group legacy */ class ErrorHandlerTest extends TestCase { diff --git a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php index 31f9a90bc4e4..0bfb680d9bba 100644 --- a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php @@ -19,6 +19,9 @@ require_once __DIR__.'/HeaderMock.php'; +/** + * @group legacy + */ class ExceptionHandlerTest extends TestCase { protected function setUp() diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php index 8e615ac640be..498fafd47cd0 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -17,6 +17,9 @@ use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +/** + * @group legacy + */ class ClassNotFoundFatalErrorHandlerTest extends TestCase { public static function setUpBeforeClass() @@ -71,6 +74,7 @@ public function provideClassNotFoundData() { $autoloader = new ComposerClassLoader(); $autoloader->add('Symfony\Component\Debug\Exception\\', realpath(__DIR__.'/../../Exception')); + $autoloader->add('Symfony_Component_Debug_Tests_Fixtures', realpath(__DIR__.'/../../Tests/Fixtures')); $debugClassLoader = new DebugClassLoader([$autoloader, 'loadClass']); @@ -101,6 +105,7 @@ public function provideClassNotFoundData() 'message' => 'Class \'UndefinedFunctionException\' not found', ], "Attempted to load class \"UndefinedFunctionException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", + [$debugClassLoader, 'loadClass'], ], [ [ @@ -110,6 +115,7 @@ public function provideClassNotFoundData() 'message' => 'Class \'PEARClass\' not found', ], "Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_Debug_Tests_Fixtures_PEARClass\"?", + [$debugClassLoader, 'loadClass'], ], [ [ @@ -119,6 +125,7 @@ public function provideClassNotFoundData() 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', ], "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", + [$debugClassLoader, 'loadClass'], ], [ [ diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php index de9994e447fe..b827c4c67aeb 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +/** + * @group legacy + */ class UndefinedFunctionFatalErrorHandlerTest extends TestCase { /** diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php index 268a841351ec..7ea1b1f95e78 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; +/** + * @group legacy + */ class UndefinedMethodFatalErrorHandlerTest extends TestCase { /** diff --git a/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt b/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt index 53839c4899b0..041affc14df6 100644 --- a/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt +++ b/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt @@ -23,5 +23,6 @@ class_exists(ExtendedFinalMethod::class); ?> --EXPECTF-- +%A The "Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod". The "Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod2()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod". diff --git a/src/Symfony/Component/Debug/Tests/phpt/decorate_exception_hander.phpt b/src/Symfony/Component/Debug/Tests/phpt/decorate_exception_hander.phpt index 9cd44388c316..25167a2c6aae 100644 --- a/src/Symfony/Component/Debug/Tests/phpt/decorate_exception_hander.phpt +++ b/src/Symfony/Component/Debug/Tests/phpt/decorate_exception_hander.phpt @@ -26,6 +26,7 @@ if (true) { ?> --EXPECTF-- +%A object(Symfony\Component\Debug\Exception\ClassNotFoundException)#%d (8) { ["message":protected]=> string(131) "Attempted to load class "missing" from namespace "Symfony\Component\Debug". diff --git a/src/Symfony/Component/Debug/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/Debug/Tests/phpt/fatal_with_nested_handlers.phpt index 1736a3b5f2a7..d0fa2411e6bd 100644 --- a/src/Symfony/Component/Debug/Tests/phpt/fatal_with_nested_handlers.phpt +++ b/src/Symfony/Component/Debug/Tests/phpt/fatal_with_nested_handlers.phpt @@ -35,6 +35,7 @@ array(1) { [0]=> string(37) "Error and exception handlers do match" } +%A object(Symfony\Component\Debug\Exception\FatalErrorException)#%d (%d) { ["message":protected]=> string(179) "Error: Class Symfony\Component\Debug\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" diff --git a/src/Symfony/Component/ErrorHandler/.gitignore b/src/Symfony/Component/ErrorHandler/.gitignore new file mode 100644 index 000000000000..c49a5d8df5c6 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/ErrorHandler/BufferingLogger.php b/src/Symfony/Component/ErrorHandler/BufferingLogger.php new file mode 100644 index 000000000000..fef10d16e5a1 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/BufferingLogger.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use Psr\Log\AbstractLogger; + +/** + * A buffering logger that stacks logs for later. + * + * @author Nicolas Grekas + */ +class BufferingLogger extends AbstractLogger +{ + private $logs = []; + + public function log($level, $message, array $context = []) + { + $this->logs[] = [$level, $message, $context]; + } + + public function cleanLogs() + { + $logs = $this->logs; + $this->logs = []; + + return $logs; + } +} diff --git a/src/Symfony/Component/ErrorHandler/CHANGELOG.md b/src/Symfony/Component/ErrorHandler/CHANGELOG.md new file mode 100644 index 000000000000..094072510d70 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +4.4.0 +----- + + * added the component diff --git a/src/Symfony/Component/ErrorHandler/Debug.php b/src/Symfony/Component/ErrorHandler/Debug.php new file mode 100644 index 000000000000..392abdb47528 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Debug.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +/** + * Registers all the debug tools. + * + * @author Fabien Potencier + */ +class Debug +{ + private static $enabled = false; + + /** + * Enables the debug tools. + * + * This method registers an error handler and an exception handler. + * + * @param int $errorReportingLevel The level of error reporting you want + * @param bool $displayErrors Whether to display errors (for development) or just log them (for production) + */ + public static function enable($errorReportingLevel = E_ALL, $displayErrors = true) + { + if (static::$enabled) { + return; + } + + static::$enabled = true; + + if (null !== $errorReportingLevel) { + error_reporting($errorReportingLevel); + } else { + error_reporting(E_ALL); + } + + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + ini_set('display_errors', 0); + ExceptionHandler::register(); + } elseif ($displayErrors && (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log'))) { + // CLI - display errors only if they're not already logged to STDERR + ini_set('display_errors', 1); + } + if ($displayErrors) { + ErrorHandler::register(new ErrorHandler(new BufferingLogger())); + } else { + ErrorHandler::register()->throwAt(0, true); + } + + DebugClassLoader::enable(); + } +} diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php new file mode 100644 index 000000000000..e1108a51bbc7 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -0,0 +1,525 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; + +/** + * Autoloader checking if the class is really defined in the file found. + * + * The ClassLoader will wrap all registered autoloaders + * and will throw an exception if a file is found but does + * not declare the class. + * + * @author Fabien Potencier + * @author Christophe Coevoet + * @author Nicolas Grekas + * @author Guilhem Niot + */ +class DebugClassLoader +{ + private $classLoader; + private $isFinder; + private $loaded = []; + private static $caseCheck; + private static $checkedClasses = []; + private static $final = []; + private static $finalMethods = []; + private static $deprecated = []; + private static $internal = []; + private static $internalMethods = []; + private static $annotatedParameters = []; + private static $darwinCache = ['/' => ['/', []]]; + private static $method = []; + + public function __construct(callable $classLoader) + { + $this->classLoader = $classLoader; + $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile'); + + if (!isset(self::$caseCheck)) { + $file = file_exists(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR); + $i = strrpos($file, \DIRECTORY_SEPARATOR); + $dir = substr($file, 0, 1 + $i); + $file = substr($file, 1 + $i); + $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file); + $test = realpath($dir.$test); + + if (false === $test || false === $i) { + // filesystem is case sensitive + self::$caseCheck = 0; + } elseif (substr($test, -\strlen($file)) === $file) { + // filesystem is case insensitive and realpath() normalizes the case of characters + self::$caseCheck = 1; + } elseif (false !== stripos(PHP_OS, 'darwin')) { + // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters + self::$caseCheck = 2; + } else { + // filesystem case checks failed, fallback to disabling them + self::$caseCheck = 0; + } + } + } + + /** + * Gets the wrapped class loader. + * + * @return callable The wrapped class loader + */ + public function getClassLoader() + { + return $this->classLoader; + } + + /** + * Wraps all autoloaders. + */ + public static function enable() + { + // Ensures we don't hit https://bugs.php.net/42098 + class_exists('Symfony\Component\ErrorHandler\ErrorHandler'); + class_exists('Psr\Log\LogLevel'); + + if (!\is_array($functions = spl_autoload_functions())) { + return; + } + + foreach ($functions as $function) { + spl_autoload_unregister($function); + } + + foreach ($functions as $function) { + if (!\is_array($function) || !$function[0] instanceof self) { + $function = [new static($function), 'loadClass']; + } + + spl_autoload_register($function); + } + } + + /** + * Disables the wrapping. + */ + public static function disable() + { + if (!\is_array($functions = spl_autoload_functions())) { + return; + } + + foreach ($functions as $function) { + spl_autoload_unregister($function); + } + + foreach ($functions as $function) { + if (\is_array($function) && $function[0] instanceof self) { + $function = $function[0]->getClassLoader(); + } + + spl_autoload_register($function); + } + } + + /** + * @return string|null + */ + public function findFile($class) + { + return $this->isFinder ? $this->classLoader[0]->findFile($class) ?: null : null; + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @throws \RuntimeException + */ + public function loadClass($class) + { + $e = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR); + + try { + if ($this->isFinder && !isset($this->loaded[$class])) { + $this->loaded[$class] = true; + if (!$file = $this->classLoader[0]->findFile($class) ?: false) { + // no-op + } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) { + include $file; + + return; + } elseif (false === include $file) { + return; + } + } else { + ($this->classLoader)($class); + $file = false; + } + } finally { + error_reporting($e); + } + + $this->checkClass($class, $file); + } + + private function checkClass($class, $file = null) + { + $exists = null === $file || class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); + + if (null !== $file && $class && '\\' === $class[0]) { + $class = substr($class, 1); + } + + if ($exists) { + if (isset(self::$checkedClasses[$class])) { + return; + } + self::$checkedClasses[$class] = true; + + $refl = new \ReflectionClass($class); + if (null === $file && $refl->isInternal()) { + return; + } + $name = $refl->getName(); + + if ($name !== $class && 0 === strcasecmp($name, $class)) { + throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); + } + + $deprecations = $this->checkAnnotations($refl, $name); + + foreach ($deprecations as $message) { + @trigger_error($message, E_USER_DEPRECATED); + } + } + + if (!$file) { + return; + } + + if (!$exists) { + if (false !== strpos($class, '/')) { + throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); + } + + throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); + } + + if (self::$caseCheck && $message = $this->checkCase($refl, $file, $class)) { + throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2])); + } + } + + public function checkAnnotations(\ReflectionClass $refl, $class) + { + $deprecations = []; + + // Don't trigger deprecations for classes in the same vendor + if (2 > $len = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) { + $len = 0; + $ns = ''; + } else { + $ns = str_replace('_', '\\', substr($class, 0, $len)); + } + + // Detect annotations on the class + if (false !== $doc = $refl->getDocComment()) { + foreach (['final', 'deprecated', 'internal'] as $annotation) { + if (false !== strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$|\r?\n)#s', $doc, $notice)) { + self::${$annotation}[$class] = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : ''; + } + } + + if ($refl->isInterface() && false !== strpos($doc, 'method') && preg_match_all('#\n \* @method\s+(static\s+)?+(?:[\w\|&\[\]\\\]+\s+)?(\w+(?:\s*\([^\)]*\))?)+(.+?([[:punct:]]\s*)?)?(?=\r?\n \*(?: @|/$|\r?\n))#', $doc, $notice, PREG_SET_ORDER)) { + foreach ($notice as $method) { + $static = '' !== $method[1]; + $name = $method[2]; + $description = $method[3] ?? null; + if (false === strpos($name, '(')) { + $name .= '()'; + } + if (null !== $description) { + $description = trim($description); + if (!isset($method[4])) { + $description .= '.'; + } + } + self::$method[$class][] = [$class, $name, $static, $description]; + } + } + } + + $parent = get_parent_class($class); + $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent); + if ($parent) { + $parentAndOwnInterfaces[$parent] = $parent; + + if (!isset(self::$checkedClasses[$parent])) { + $this->checkClass($parent); + } + + if (isset(self::$final[$parent])) { + $deprecations[] = sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $class); + } + } + + // Detect if the parent is annotated + foreach ($parentAndOwnInterfaces + class_uses($class, false) as $use) { + if (!isset(self::$checkedClasses[$use])) { + $this->checkClass($use); + } + if (isset(self::$deprecated[$use]) && strncmp($ns, str_replace('_', '\\', $use), $len) && !isset(self::$deprecated[$class])) { + $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait'); + $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); + + $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $class, $type, $verb, $use, self::$deprecated[$use]); + } + if (isset(self::$internal[$use]) && strncmp($ns, str_replace('_', '\\', $use), $len)) { + $deprecations[] = sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $class); + } + if (isset(self::$method[$use])) { + if ($refl->isAbstract()) { + if (isset(self::$method[$class])) { + self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]); + } else { + self::$method[$class] = self::$method[$use]; + } + } elseif (!$refl->isInterface()) { + $hasCall = $refl->hasMethod('__call'); + $hasStaticCall = $refl->hasMethod('__callStatic'); + foreach (self::$method[$use] as $method) { + list($interface, $name, $static, $description) = $method; + if ($static ? $hasStaticCall : $hasCall) { + continue; + } + $realName = substr($name, 0, strpos($name, '(')); + if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) { + $deprecations[] = sprintf('Class "%s" should implement method "%s::%s"%s', $class, ($static ? 'static ' : '').$interface, $name, null == $description ? '.' : ': '.$description); + } + } + } + } + } + + if (trait_exists($class)) { + return $deprecations; + } + + // Inherit @final, @internal and @param annotations for methods + self::$finalMethods[$class] = []; + self::$internalMethods[$class] = []; + self::$annotatedParameters[$class] = []; + foreach ($parentAndOwnInterfaces as $use) { + foreach (['finalMethods', 'internalMethods', 'annotatedParameters'] as $property) { + if (isset(self::${$property}[$use])) { + self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use]; + } + } + } + + foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) { + if ($method->class !== $class) { + continue; + } + + if ($parent && isset(self::$finalMethods[$parent][$method->name])) { + list($declaringClass, $message) = self::$finalMethods[$parent][$method->name]; + $deprecations[] = sprintf('The "%s::%s()" method is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $class); + } + + if (isset(self::$internalMethods[$class][$method->name])) { + list($declaringClass, $message) = self::$internalMethods[$class][$method->name]; + if (strncmp($ns, $declaringClass, $len)) { + $deprecations[] = sprintf('The "%s::%s()" method is considered internal%s. It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $class); + } + } + + // To read method annotations + $doc = $method->getDocComment(); + + if (isset(self::$annotatedParameters[$class][$method->name])) { + $definedParameters = []; + foreach ($method->getParameters() as $parameter) { + $definedParameters[$parameter->name] = true; + } + + foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) { + if (!isset($definedParameters[$parameterName]) && !($doc && preg_match("/\\n\\s+\\* @param +((?(?!callable *\().*?|callable *\(.*\).*?))(?<= )\\\${$parameterName}\\b/", $doc))) { + $deprecations[] = sprintf($deprecation, $class); + } + } + } + + if (!$doc) { + continue; + } + + $finalOrInternal = false; + + foreach (['final', 'internal'] as $annotation) { + if (false !== strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$|\r?\n)#s', $doc, $notice)) { + $message = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : ''; + self::${$annotation.'Methods'}[$class][$method->name] = [$class, $message]; + $finalOrInternal = true; + } + } + + if ($finalOrInternal || $method->isConstructor() || false === strpos($doc, '@param') || StatelessInvocation::class === $class) { + continue; + } + if (!preg_match_all('#\n\s+\* @param +((?(?!callable *\().*?|callable *\(.*\).*?))(?<= )\$([a-zA-Z0-9_\x7f-\xff]++)#', $doc, $matches, PREG_SET_ORDER)) { + continue; + } + if (!isset(self::$annotatedParameters[$class][$method->name])) { + $definedParameters = []; + foreach ($method->getParameters() as $parameter) { + $definedParameters[$parameter->name] = true; + } + } + foreach ($matches as list(, $parameterType, $parameterName)) { + if (!isset($definedParameters[$parameterName])) { + $parameterType = trim($parameterType); + self::$annotatedParameters[$class][$method->name][$parameterName] = sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its parent class "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType.' ' : '', $parameterName, $method->class); + } + } + } + + return $deprecations; + } + + public function checkCase(\ReflectionClass $refl, $file, $class) + { + $real = explode('\\', $class.strrchr($file, '.')); + $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file)); + + $i = \count($tail) - 1; + $j = \count($real) - 1; + + while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) { + --$i; + --$j; + } + + array_splice($tail, 0, $i + 1); + + if (!$tail) { + return; + } + + $tail = \DIRECTORY_SEPARATOR.implode(\DIRECTORY_SEPARATOR, $tail); + $tailLen = \strlen($tail); + $real = $refl->getFileName(); + + if (2 === self::$caseCheck) { + $real = $this->darwinRealpath($real); + } + + if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true) + && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false) + ) { + return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)]; + } + } + + /** + * `realpath` on MacOSX doesn't normalize the case of characters. + */ + private function darwinRealpath($real) + { + $i = 1 + strrpos($real, '/'); + $file = substr($real, $i); + $real = substr($real, 0, $i); + + if (isset(self::$darwinCache[$real])) { + $kDir = $real; + } else { + $kDir = strtolower($real); + + if (isset(self::$darwinCache[$kDir])) { + $real = self::$darwinCache[$kDir][0]; + } else { + $dir = getcwd(); + chdir($real); + $real = getcwd().'/'; + chdir($dir); + + $dir = $real; + $k = $kDir; + $i = \strlen($dir) - 1; + while (!isset(self::$darwinCache[$k])) { + self::$darwinCache[$k] = [$dir, []]; + self::$darwinCache[$dir] = &self::$darwinCache[$k]; + + while ('/' !== $dir[--$i]) { + } + $k = substr($k, 0, ++$i); + $dir = substr($dir, 0, $i--); + } + } + } + + $dirFiles = self::$darwinCache[$kDir][1]; + + if (!isset($dirFiles[$file]) && ') : eval()\'d code' === substr($file, -17)) { + // Get the file name from "file_name.php(123) : eval()'d code" + $file = substr($file, 0, strrpos($file, '(', -17)); + } + + if (isset($dirFiles[$file])) { + return $real .= $dirFiles[$file]; + } + + $kFile = strtolower($file); + + if (!isset($dirFiles[$kFile])) { + foreach (scandir($real, 2) as $f) { + if ('.' !== $f[0]) { + $dirFiles[$f] = $f; + if ($f === $file) { + $kFile = $k = $file; + } elseif ($f !== $k = strtolower($f)) { + $dirFiles[$k] = $f; + } + } + } + self::$darwinCache[$kDir][1] = $dirFiles; + } + + return $real .= $dirFiles[$kFile]; + } + + /** + * `class_implements` includes interfaces from the parents so we have to manually exclude them. + * + * @param string $class + * @param string|false $parent + * + * @return string[] + */ + private function getOwnInterfaces($class, $parent) + { + $ownInterfaces = class_implements($class, false); + + if ($parent) { + foreach (class_implements($parent, false) as $interface) { + unset($ownInterfaces[$interface]); + } + } + + foreach ($ownInterfaces as $interface) { + foreach (class_implements($interface) as $interface) { + unset($ownInterfaces[$interface]); + } + } + + return $ownInterfaces; + } +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php new file mode 100644 index 000000000000..2a2a8ca384ba --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -0,0 +1,715 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface; +use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler; +use Symfony\Component\ErrorRenderer\Exception\FlattenException; + +/** + * A generic ErrorHandler for the PHP engine. + * + * Provides five bit fields that control how errors are handled: + * - thrownErrors: errors thrown as \ErrorException + * - loggedErrors: logged errors, when not @-silenced + * - scopedErrors: errors thrown or logged with their local context + * - tracedErrors: errors logged with their stack trace + * - screamedErrors: never @-silenced errors + * + * Each error level can be logged by a dedicated PSR-3 logger object. + * Screaming only applies to logging. + * Throwing takes precedence over logging. + * Uncaught exceptions are logged as E_ERROR. + * E_DEPRECATED and E_USER_DEPRECATED levels never throw. + * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw. + * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so. + * As errors have a performance cost, repeated errors are all logged, so that the developer + * can see them and weight them as more important to fix than others of the same level. + * + * @author Nicolas Grekas + * @author Grégoire Pineau + * + * @final since Symfony 4.3 + */ +class ErrorHandler +{ + private $levels = [ + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User Deprecated', + E_NOTICE => 'Notice', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice', + E_WARNING => 'Warning', + E_USER_WARNING => 'User Warning', + E_COMPILE_WARNING => 'Compile Warning', + E_CORE_WARNING => 'Core Warning', + E_USER_ERROR => 'User Error', + E_RECOVERABLE_ERROR => 'Catchable Fatal Error', + E_COMPILE_ERROR => 'Compile Error', + E_PARSE => 'Parse Error', + E_ERROR => 'Error', + E_CORE_ERROR => 'Core Error', + ]; + + private $loggers = [ + E_DEPRECATED => [null, LogLevel::INFO], + E_USER_DEPRECATED => [null, LogLevel::INFO], + E_NOTICE => [null, LogLevel::WARNING], + E_USER_NOTICE => [null, LogLevel::WARNING], + E_STRICT => [null, LogLevel::WARNING], + E_WARNING => [null, LogLevel::WARNING], + E_USER_WARNING => [null, LogLevel::WARNING], + E_COMPILE_WARNING => [null, LogLevel::WARNING], + E_CORE_WARNING => [null, LogLevel::WARNING], + E_USER_ERROR => [null, LogLevel::CRITICAL], + E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], + E_COMPILE_ERROR => [null, LogLevel::CRITICAL], + E_PARSE => [null, LogLevel::CRITICAL], + E_ERROR => [null, LogLevel::CRITICAL], + E_CORE_ERROR => [null, LogLevel::CRITICAL], + ]; + + private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED + private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED + private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE + private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE + private $loggedErrors = 0; + private $traceReflector; + + private $isRecursive = 0; + private $isRoot = false; + private $exceptionHandler; + private $bootstrappingLogger; + + private static $reservedMemory; + private static $toStringException = null; + private static $silencedErrorCache = []; + private static $silencedErrorCount = 0; + private static $exitCode = 0; + + /** + * Registers the error handler. + * + * @param self|null $handler The handler to register + * @param bool $replace Whether to replace or not any existing handler + * + * @return self The registered error handler + */ + public static function register(self $handler = null, $replace = true) + { + if (null === self::$reservedMemory) { + self::$reservedMemory = str_repeat('x', 10240); + register_shutdown_function(__CLASS__.'::handleFatalError'); + } + + if ($handlerIsNew = null === $handler) { + $handler = new static(); + } + + if (null === $prev = set_error_handler([$handler, 'handleError'])) { + restore_error_handler(); + // Specifying the error types earlier would expose us to https://bugs.php.net/63206 + set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors); + $handler->isRoot = true; + } + + if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) { + $handler = $prev[0]; + $replace = false; + } + if (!$replace && $prev) { + restore_error_handler(); + $handlerIsRegistered = \is_array($prev) && $handler === $prev[0]; + } else { + $handlerIsRegistered = true; + } + if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) { + restore_exception_handler(); + if (!$handlerIsRegistered) { + $handler = $prev[0]; + } elseif ($handler !== $prev[0] && $replace) { + set_exception_handler([$handler, 'handleException']); + $p = $prev[0]->setExceptionHandler(null); + $handler->setExceptionHandler($p); + $prev[0]->setExceptionHandler($p); + } + } else { + $handler->setExceptionHandler($prev); + } + + $handler->throwAt(E_ALL & $handler->thrownErrors, true); + + return $handler; + } + + public function __construct(BufferingLogger $bootstrappingLogger = null) + { + if ($bootstrappingLogger) { + $this->bootstrappingLogger = $bootstrappingLogger; + $this->setDefaultLogger($bootstrappingLogger); + } + $this->traceReflector = new \ReflectionProperty('Exception', 'trace'); + $this->traceReflector->setAccessible(true); + } + + /** + * Sets a logger to non assigned errors levels. + * + * @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels + * @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants + * @param bool $replace Whether to replace or not any existing logger + */ + public function setDefaultLogger(LoggerInterface $logger, $levels = E_ALL, $replace = false) + { + $loggers = []; + + if (\is_array($levels)) { + foreach ($levels as $type => $logLevel) { + if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) { + $loggers[$type] = [$logger, $logLevel]; + } + } + } else { + if (null === $levels) { + $levels = E_ALL; + } + foreach ($this->loggers as $type => $log) { + if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) { + $log[0] = $logger; + $loggers[$type] = $log; + } + } + } + + $this->setLoggers($loggers); + } + + /** + * Sets a logger for each error level. + * + * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map + * + * @return array The previous map + * + * @throws \InvalidArgumentException + */ + public function setLoggers(array $loggers) + { + $prevLogged = $this->loggedErrors; + $prev = $this->loggers; + $flush = []; + + foreach ($loggers as $type => $log) { + if (!isset($prev[$type])) { + throw new \InvalidArgumentException('Unknown error type: '.$type); + } + if (!\is_array($log)) { + $log = [$log]; + } elseif (!\array_key_exists(0, $log)) { + throw new \InvalidArgumentException('No logger provided'); + } + if (null === $log[0]) { + $this->loggedErrors &= ~$type; + } elseif ($log[0] instanceof LoggerInterface) { + $this->loggedErrors |= $type; + } else { + throw new \InvalidArgumentException('Invalid logger provided'); + } + $this->loggers[$type] = $log + $prev[$type]; + + if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) { + $flush[$type] = $type; + } + } + $this->reRegister($prevLogged | $this->thrownErrors); + + if ($flush) { + foreach ($this->bootstrappingLogger->cleanLogs() as $log) { + $type = $log[2]['exception'] instanceof \ErrorException ? $log[2]['exception']->getSeverity() : E_ERROR; + if (!isset($flush[$type])) { + $this->bootstrappingLogger->log($log[0], $log[1], $log[2]); + } elseif ($this->loggers[$type][0]) { + $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]); + } + } + } + + return $prev; + } + + /** + * Sets a user exception handler. + * + * @param callable $handler A handler that will be called on Exception + * + * @return callable|null The previous exception handler + */ + public function setExceptionHandler(callable $handler = null) + { + $prev = $this->exceptionHandler; + $this->exceptionHandler = $handler; + + return $prev; + } + + /** + * Sets the PHP error levels that throw an exception when a PHP error occurs. + * + * @param int $levels A bit field of E_* constants for thrown errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function throwAt($levels, $replace = false) + { + $prev = $this->thrownErrors; + $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED; + if (!$replace) { + $this->thrownErrors |= $prev; + } + $this->reRegister($prev | $this->loggedErrors); + + return $prev; + } + + /** + * Sets the PHP error levels for which local variables are preserved. + * + * @param int $levels A bit field of E_* constants for scoped errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function scopeAt($levels, $replace = false) + { + $prev = $this->scopedErrors; + $this->scopedErrors = (int) $levels; + if (!$replace) { + $this->scopedErrors |= $prev; + } + + return $prev; + } + + /** + * Sets the PHP error levels for which the stack trace is preserved. + * + * @param int $levels A bit field of E_* constants for traced errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function traceAt($levels, $replace = false) + { + $prev = $this->tracedErrors; + $this->tracedErrors = (int) $levels; + if (!$replace) { + $this->tracedErrors |= $prev; + } + + return $prev; + } + + /** + * Sets the error levels where the @-operator is ignored. + * + * @param int $levels A bit field of E_* constants for screamed errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function screamAt($levels, $replace = false) + { + $prev = $this->screamedErrors; + $this->screamedErrors = (int) $levels; + if (!$replace) { + $this->screamedErrors |= $prev; + } + + return $prev; + } + + /** + * Re-registers as a PHP error handler if levels changed. + */ + private function reRegister($prev) + { + if ($prev !== $this->thrownErrors | $this->loggedErrors) { + $handler = set_error_handler('var_dump'); + $handler = \is_array($handler) ? $handler[0] : null; + restore_error_handler(); + if ($handler === $this) { + restore_error_handler(); + if ($this->isRoot) { + set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors); + } else { + set_error_handler([$this, 'handleError']); + } + } + } + } + + /** + * Handles errors by filtering then logging them according to the configured bit fields. + * + * @param int $type One of the E_* constants + * @param string $message + * @param string $file + * @param int $line + * + * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself + * + * @throws \ErrorException When $this->thrownErrors requests so + * + * @internal + */ + public function handleError($type, $message, $file, $line) + { + // @deprecated to be removed in Symfony 5.0 + if (\PHP_VERSION_ID >= 70300 && $message && '"' === $message[0] && 0 === strpos($message, '"continue') && preg_match('/^"continue(?: \d++)?" targeting switch is equivalent to "break(?: \d++)?"\. Did you mean to use "continue(?: \d++)?"\?$/', $message)) { + $type = E_DEPRECATED; + } + + // Level is the current error reporting level to manage silent error. + $level = error_reporting(); + $silenced = 0 === ($level & $type); + // Strong errors are not authorized to be silenced. + $level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; + $log = $this->loggedErrors & $type; + $throw = $this->thrownErrors & $type & $level; + $type &= $level | $this->screamedErrors; + + if (!$type || (!$log && !$throw)) { + return !$silenced && $type && $log; + } + $scope = $this->scopedErrors & $type; + + if (4 < $numArgs = \func_num_args()) { + $context = $scope ? (func_get_arg(4) ?: []) : []; + } else { + $context = []; + } + + if (isset($context['GLOBALS']) && $scope) { + $e = $context; // Whatever the signature of the method, + unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 + $context = $e; + } + + if (false !== strpos($message, "class@anonymous\0")) { + $logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage(); + } else { + $logMessage = $this->levels[$type].': '.$message; + } + + if (null !== self::$toStringException) { + $errorAsException = self::$toStringException; + self::$toStringException = null; + } elseif (!$throw && !($type & $level)) { + if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) { + $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5), $type, $file, $line, false) : []; + $errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace); + } elseif (isset(self::$silencedErrorCache[$id][$message])) { + $lightTrace = null; + $errorAsException = self::$silencedErrorCache[$id][$message]; + ++$errorAsException->count; + } else { + $lightTrace = []; + $errorAsException = null; + } + + if (100 < ++self::$silencedErrorCount) { + self::$silencedErrorCache = $lightTrace = []; + self::$silencedErrorCount = 1; + } + if ($errorAsException) { + self::$silencedErrorCache[$id][$message] = $errorAsException; + } + if (null === $lightTrace) { + return; + } + } else { + $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line); + + if ($throw || $this->tracedErrors & $type) { + $backtrace = $errorAsException->getTrace(); + $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw); + $this->traceReflector->setValue($errorAsException, $lightTrace); + } else { + $this->traceReflector->setValue($errorAsException, []); + $backtrace = []; + } + } + + if ($throw) { + if (E_USER_ERROR & $type) { + for ($i = 1; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function']) + && '__toString' === $backtrace[$i]['function'] + && '->' === $backtrace[$i]['type'] + && !isset($backtrace[$i - 1]['class']) + && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function']) + ) { + // Here, we know trigger_error() has been called from __toString(). + // PHP triggers a fatal error when throwing from __toString(). + // A small convention allows working around the limitation: + // given a caught $e exception in __toString(), quitting the method with + // `return trigger_error($e, E_USER_ERROR);` allows this error handler + // to make $e get through the __toString() barrier. + + foreach ($context as $e) { + if ($e instanceof \Throwable && $e->__toString() === $message) { + self::$toStringException = $e; + + return true; + } + } + + // Display the original error message instead of the default one. + $this->handleException($errorAsException); + + // Stop the process by giving back the error to the native handler. + return false; + } + } + } + + throw $errorAsException; + } + + if ($this->isRecursive) { + $log = 0; + } else { + if (!\defined('HHVM_VERSION')) { + $currentErrorHandler = set_error_handler('var_dump'); + restore_error_handler(); + } + + try { + $this->isRecursive = true; + $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG; + $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []); + } finally { + $this->isRecursive = false; + + if (!\defined('HHVM_VERSION')) { + set_error_handler($currentErrorHandler); + } + } + } + + return !$silenced && $type && $log; + } + + /** + * Handles an exception by logging then forwarding it to another handler. + * + * @param \Exception|\Throwable $exception An exception to handle + * @param array $error An array as returned by error_get_last() + * + * @internal + */ + public function handleException($exception, array $error = null) + { + if (null === $error) { + self::$exitCode = 255; + } + if (!$exception instanceof \Exception) { + $exception = new FatalThrowableError($exception); + } + $type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR; + $handlerException = null; + + if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) { + if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) { + $message = (new FlattenException())->setMessage($message)->getMessage(); + } + if ($exception instanceof FatalErrorException) { + if ($exception instanceof FatalThrowableError) { + $error = [ + 'type' => $type, + 'message' => $message, + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]; + } else { + $message = 'Fatal '.$message; + } + } elseif ($exception instanceof \ErrorException) { + $message = 'Uncaught '.$message; + } else { + $message = 'Uncaught Exception: '.$message; + } + } + if ($this->loggedErrors & $type) { + try { + $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]); + } catch (\Throwable $handlerException) { + } + } + if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { + foreach ($this->getFatalErrorHandlers() as $handler) { + if ($e = $handler->handleError($error, $exception)) { + $exception = $e; + break; + } + } + } + $exceptionHandler = $this->exceptionHandler; + $this->exceptionHandler = null; + try { + if (null !== $exceptionHandler) { + return $exceptionHandler($exception); + } + $handlerException = $handlerException ?: $exception; + } catch (\Throwable $handlerException) { + } + if ($exception === $handlerException) { + self::$reservedMemory = null; // Disable the fatal error handler + throw $exception; // Give back $exception to the native handler + } + $this->handleException($handlerException); + } + + /** + * Shutdown registered function for handling PHP fatal errors. + * + * @param array $error An array as returned by error_get_last() + * + * @internal + */ + public static function handleFatalError(array $error = null) + { + if (null === self::$reservedMemory) { + return; + } + + $handler = self::$reservedMemory = null; + $handlers = []; + $previousHandler = null; + $sameHandlerLimit = 10; + + while (!\is_array($handler) || !$handler[0] instanceof self) { + $handler = set_exception_handler('var_dump'); + restore_exception_handler(); + + if (!$handler) { + break; + } + restore_exception_handler(); + + if ($handler !== $previousHandler) { + array_unshift($handlers, $handler); + $previousHandler = $handler; + } elseif (0 === --$sameHandlerLimit) { + $handler = null; + break; + } + } + foreach ($handlers as $h) { + set_exception_handler($h); + } + if (!$handler) { + return; + } + if ($handler !== $h) { + $handler[0]->setExceptionHandler($h); + } + $handler = $handler[0]; + $handlers = []; + + if ($exit = null === $error) { + $error = error_get_last(); + } + + if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) { + // Let's not throw anymore but keep logging + $handler->throwAt(0, true); + $trace = isset($error['backtrace']) ? $error['backtrace'] : null; + + if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { + $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace); + } else { + $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); + } + } else { + $exception = null; + } + + try { + if (null !== $exception) { + self::$exitCode = 255; + $handler->handleException($exception, $error); + } + } catch (FatalErrorException $e) { + // Ignore this re-throw + } + + if ($exit && self::$exitCode) { + $exitCode = self::$exitCode; + register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); + } + } + + /** + * Gets the fatal error handlers. + * + * Override this method if you want to define more fatal error handlers. + * + * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface + */ + protected function getFatalErrorHandlers() + { + return [ + new UndefinedFunctionFatalErrorHandler(), + new UndefinedMethodFatalErrorHandler(), + new ClassNotFoundFatalErrorHandler(), + ]; + } + + /** + * Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader. + */ + private function cleanTrace($backtrace, $type, $file, $line, $throw) + { + $lightTrace = $backtrace; + + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { + $lightTrace = \array_slice($lightTrace, 1 + $i); + break; + } + } + if (class_exists(DebugClassLoader::class, false)) { + for ($i = \count($lightTrace) - 2; 0 < $i; --$i) { + if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) { + array_splice($lightTrace, --$i, 2); + } + } + } + if (!($throw || $this->scopedErrors & $type)) { + for ($i = 0; isset($lightTrace[$i]); ++$i) { + unset($lightTrace[$i]['args'], $lightTrace[$i]['object']); + } + } + + return $lightTrace; + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php b/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php new file mode 100644 index 000000000000..b0638826d641 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Class (or Trait or Interface) Not Found Exception. + * + * @author Konstanton Myakshin + */ +class ClassNotFoundException extends FatalErrorException +{ + public function __construct(string $message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + null, + true, + null, + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php b/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php new file mode 100644 index 000000000000..4269356d5724 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Fatal Error Exception. + * + * @author Konstanton Myakshin + */ +class FatalErrorException extends \ErrorException +{ + public function __construct(string $message, int $code, int $severity, string $filename, int $lineno, int $traceOffset = null, bool $traceArgs = true, array $trace = null, \Throwable $previous = null) + { + parent::__construct($message, $code, $severity, $filename, $lineno, $previous); + + if (null !== $trace) { + if (!$traceArgs) { + foreach ($trace as &$frame) { + unset($frame['args'], $frame['this'], $frame); + } + } + + $this->setTrace($trace); + } elseif (null !== $traceOffset) { + if (\function_exists('xdebug_get_function_stack')) { + $trace = xdebug_get_function_stack(); + if (0 < $traceOffset) { + array_splice($trace, -$traceOffset); + } + + foreach ($trace as &$frame) { + if (!isset($frame['type'])) { + // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 + if (isset($frame['class'])) { + $frame['type'] = '::'; + } + } elseif ('dynamic' === $frame['type']) { + $frame['type'] = '->'; + } elseif ('static' === $frame['type']) { + $frame['type'] = '::'; + } + + // XDebug also has a different name for the parameters array + if (!$traceArgs) { + unset($frame['params'], $frame['args']); + } elseif (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + unset($frame['params']); + } + } + + unset($frame); + $trace = array_reverse($trace); + } else { + $trace = []; + } + + $this->setTrace($trace); + } + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php b/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php new file mode 100644 index 000000000000..a690c835975f --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Fatal Throwable Error. + * + * @author Nicolas Grekas + */ +class FatalThrowableError extends FatalErrorException +{ + private $originalClassName; + + public function __construct(\Throwable $e) + { + $this->originalClassName = \get_class($e); + + if ($e instanceof \ParseError) { + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $severity = E_RECOVERABLE_ERROR; + } else { + $severity = E_ERROR; + } + + \ErrorException::__construct( + $e->getMessage(), + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine(), + $e->getPrevious() + ); + + $this->setTrace($e->getTrace()); + } + + public function getOriginalClassName(): string + { + return $this->originalClassName; + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php b/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php new file mode 100644 index 000000000000..18c367596f63 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Out of memory exception. + * + * @author Nicolas Grekas + */ +class OutOfMemoryException extends FatalErrorException +{ +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php b/src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php new file mode 100644 index 000000000000..2c4ae69db419 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Data Object that represents a Silenced Error. + * + * @author Grégoire Pineau + */ +class SilencedErrorContext implements \JsonSerializable +{ + public $count = 1; + + private $severity; + private $file; + private $line; + private $trace; + + public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1) + { + $this->severity = $severity; + $this->file = $file; + $this->line = $line; + $this->trace = $trace; + $this->count = $count; + } + + public function getSeverity() + { + return $this->severity; + } + + public function getFile() + { + return $this->file; + } + + public function getLine() + { + return $this->line; + } + + public function getTrace() + { + return $this->trace; + } + + public function JsonSerialize() + { + return [ + 'severity' => $this->severity, + 'file' => $this->file, + 'line' => $this->line, + 'trace' => $this->trace, + 'count' => $this->count, + ]; + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php b/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php new file mode 100644 index 000000000000..bb2f46564d4d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Undefined Function Exception. + * + * @author Konstanton Myakshin + */ +class UndefinedFunctionException extends FatalErrorException +{ + public function __construct(string $message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + null, + true, + null, + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php b/src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php new file mode 100644 index 000000000000..12efdc716c5d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Undefined Method Exception. + * + * @author Grégoire Pineau + */ +class UndefinedMethodException extends FatalErrorException +{ + public function __construct(string $message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + null, + true, + null, + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); + } +} diff --git a/src/Symfony/Component/ErrorHandler/ExceptionHandler.php b/src/Symfony/Component/ErrorHandler/ExceptionHandler.php new file mode 100644 index 000000000000..577234a8d9b5 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ExceptionHandler.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; + +/** + * ExceptionHandler converts an exception to a Response object. + * + * It is mostly useful in debug mode to replace the default PHP/XDebug + * output with something prettier and more useful. + * + * As this class is mainly used during Kernel boot, where nothing is yet + * available, the Response content is always HTML. + * + * @author Fabien Potencier + * @author Nicolas Grekas + * + * @final since Symfony 4.3 + */ +class ExceptionHandler +{ + private $debug; + private $charset; + private $handler; + private $caughtBuffer; + private $caughtLength; + private $fileLinkFormat; + private $htmlErrorRenderer; + + public function __construct(bool $debug = true, string $charset = null, $fileLinkFormat = null) + { + $this->debug = $debug; + $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; + $this->fileLinkFormat = $fileLinkFormat; + } + + /** + * Registers the exception handler. + * + * @param bool $debug Enable/disable debug mode, where the stack trace is displayed + * @param string|null $charset The charset used by exception messages + * @param string|null $fileLinkFormat The IDE link template + * + * @return static + */ + public static function register($debug = true, $charset = null, $fileLinkFormat = null) + { + $handler = new static($debug, $charset, $fileLinkFormat); + + $prev = set_exception_handler([$handler, 'handle']); + if (\is_array($prev) && $prev[0] instanceof ErrorHandler) { + restore_exception_handler(); + $prev[0]->setExceptionHandler([$handler, 'handle']); + } + + return $handler; + } + + /** + * Sets a user exception handler. + * + * @param callable $handler An handler that will be called on Exception + * + * @return callable|null The previous exception handler if any + */ + public function setHandler(callable $handler = null) + { + $old = $this->handler; + $this->handler = $handler; + + return $old; + } + + /** + * Sets the format for links to source files. + * + * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files + * + * @return string The previous file link format + */ + public function setFileLinkFormat($fileLinkFormat) + { + $old = $this->fileLinkFormat; + $this->fileLinkFormat = $fileLinkFormat; + + return $old; + } + + /** + * Sends a response for the given Exception. + * + * To be as fail-safe as possible, the exception is first handled + * by our simple exception handler, then by the user exception handler. + * The latter takes precedence and any output from the former is cancelled, + * if and only if nothing bad happens in this handling path. + */ + public function handle(\Exception $exception) + { + if (null === $this->handler || $exception instanceof OutOfMemoryException) { + $this->sendPhpResponse($exception); + + return; + } + + $caughtLength = $this->caughtLength = 0; + + ob_start(function ($buffer) { + $this->caughtBuffer = $buffer; + + return ''; + }); + + $this->sendPhpResponse($exception); + while (null === $this->caughtBuffer && ob_end_flush()) { + // Empty loop, everything is in the condition + } + if (isset($this->caughtBuffer[0])) { + ob_start(function ($buffer) { + if ($this->caughtLength) { + // use substr_replace() instead of substr() for mbstring overloading resistance + $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); + if (isset($cleanBuffer[0])) { + $buffer = $cleanBuffer; + } + } + + return $buffer; + }); + + echo $this->caughtBuffer; + $caughtLength = ob_get_length(); + } + $this->caughtBuffer = null; + + try { + ($this->handler)($exception); + $this->caughtLength = $caughtLength; + } catch (\Exception $e) { + if (!$caughtLength) { + // All handlers failed. Let PHP handle that now. + throw $exception; + } + } + } + + /** + * Sends the error associated with the given Exception as a plain PHP response. + * + * This method uses plain PHP functions like header() and echo to output + * the response. + * + * @param \Throwable|FlattenException $exception A \Throwable or FlattenException instance + */ + public function sendPhpResponse($exception) + { + if ($exception instanceof \Throwable) { + $exception = FlattenException::createFromThrowable($exception); + } + + if (!headers_sent()) { + header(sprintf('HTTP/1.0 %s', $exception->getStatusCode())); + foreach ($exception->getHeaders() as $name => $value) { + header($name.': '.$value, false); + } + header('Content-Type: text/html; charset='.$this->charset); + } + + if (null === $this->htmlErrorRenderer) { + $this->htmlErrorRenderer = new HtmlErrorRenderer($this->debug, $this->charset, $this->fileLinkFormat); + } + + echo $this->htmlErrorRenderer->render($exception); + } +} diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php new file mode 100644 index 000000000000..b59b0d451737 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; +use Symfony\Component\ErrorHandler\DebugClassLoader; +use Symfony\Component\ErrorHandler\Exception\ClassNotFoundException; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; + +/** + * ErrorHandler for classes that do not exist. + * + * @author Fabien Potencier + */ +class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = \strlen($error['message']); + $notFoundSuffix = '\' not found'; + $notFoundSuffixLen = \strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + foreach (['class', 'interface', 'trait'] as $typeName) { + $prefix = ucfirst($typeName).' \''; + $prefixLen = \strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + continue; + } + + $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { + $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); + $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); + $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); + $tail = ' for another namespace?'; + } else { + $className = $fullyQualifiedClassName; + $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); + $tail = '?'; + } + + if ($candidates = $this->getClassCandidates($className)) { + $tail = array_pop($candidates).'"?'; + if ($candidates) { + $tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; + } else { + $tail = ' for "'.$tail; + } + } + $message .= "\nDid you forget a \"use\" statement".$tail; + + return new ClassNotFoundException($message, $exception); + } + } + + /** + * Tries to guess the full namespace for a given class name. + * + * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer + * autoloader (that should cover all common cases). + * + * @param string $class A class name (without its namespace) + * + * @return array An array of possible fully qualified class names + */ + private function getClassCandidates(string $class): array + { + if (!\is_array($functions = spl_autoload_functions())) { + return []; + } + + // find Symfony and Composer autoloaders + $classes = []; + + foreach ($functions as $function) { + if (!\is_array($function)) { + continue; + } + // get class loaders wrapped by DebugClassLoader + if ($function[0] instanceof DebugClassLoader) { + $function = $function[0]->getClassLoader(); + + if (!\is_array($function)) { + continue; + } + } + + if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { + foreach ($function[0]->getPrefixes() as $prefix => $paths) { + foreach ($paths as $path) { + $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); + } + } + } + if ($function[0] instanceof ComposerClassLoader) { + foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { + foreach ($paths as $path) { + $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); + } + } + } + } + + return array_unique($classes); + } + + private function findClassInPath(string $path, string $class, string $prefix): array + { + if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.\dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { + return []; + } + + $classes = []; + $filename = $class.'.php'; + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { + $classes[] = $class; + } + } + + return $classes; + } + + private function convertFileToClass(string $path, string $file, string $prefix): ?string + { + $candidates = [ + // namespaced class + $namespacedClass = str_replace([$path.\DIRECTORY_SEPARATOR, '.php', '/'], ['', '', '\\'], $file), + // namespaced class (with target dir) + $prefix.$namespacedClass, + // namespaced class (with target dir and separator) + $prefix.'\\'.$namespacedClass, + // PEAR class + str_replace('\\', '_', $namespacedClass), + // PEAR class (with target dir) + str_replace('\\', '_', $prefix.$namespacedClass), + // PEAR class (with target dir and separator) + str_replace('\\', '_', $prefix.'\\'.$namespacedClass), + ]; + + if ($prefix) { + $candidates = array_filter($candidates, function ($candidate) use ($prefix) { return 0 === strpos($candidate, $prefix); }); + } + + // We cannot use the autoloader here as most of them use require; but if the class + // is not found, the new autoloader call will require the file again leading to a + // "cannot redeclare class" error. + foreach ($candidates as $candidate) { + if ($this->classExists($candidate)) { + return $candidate; + } + } + + try { + require_once $file; + } catch (\Throwable $e) { + return null; + } + + foreach ($candidates as $candidate) { + if ($this->classExists($candidate)) { + return $candidate; + } + } + + return null; + } + + private function classExists(string $class): bool + { + return class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); + } +} diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php new file mode 100644 index 000000000000..afa8b7d2367d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; + +/** + * Attempts to convert fatal errors to exceptions. + * + * @author Fabien Potencier + */ +interface FatalErrorHandlerInterface +{ + /** + * Attempts to convert an error into an exception. + * + * @param array $error An array as returned by error_get_last() + * @param FatalErrorException $exception A FatalErrorException instance + * + * @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise + */ + public function handleError(array $error, FatalErrorException $exception); +} diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php new file mode 100644 index 000000000000..9e3affb14dba --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException; + +/** + * ErrorHandler for undefined functions. + * + * @author Fabien Potencier + */ +class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = \strlen($error['message']); + $notFoundSuffix = '()'; + $notFoundSuffixLen = \strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + $prefix = 'Call to undefined function '; + $prefixLen = \strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + return; + } + + $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { + $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); + $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); + $message = sprintf('Attempted to call function "%s" from namespace "%s".', $functionName, $namespacePrefix); + } else { + $functionName = $fullyQualifiedFunctionName; + $message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName); + } + + $candidates = []; + foreach (get_defined_functions() as $type => $definedFunctionNames) { + foreach ($definedFunctionNames as $definedFunctionName) { + if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { + $definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); + } else { + $definedFunctionNameBasename = $definedFunctionName; + } + + if ($definedFunctionNameBasename === $functionName) { + $candidates[] = '\\'.$definedFunctionName; + } + } + } + + if ($candidates) { + sort($candidates); + $last = array_pop($candidates).'"?'; + if ($candidates) { + $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; + } else { + $candidates = '"'.$last; + } + $message .= "\nDid you mean to call ".$candidates; + } + + return new UndefinedFunctionException($message, $exception); + } +} diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php new file mode 100644 index 000000000000..49de27446945 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException; + +/** + * ErrorHandler for undefined methods. + * + * @author Grégoire Pineau + */ +class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches); + if (!$matches) { + return; + } + + $className = $matches[1]; + $methodName = $matches[2]; + + $message = sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); + + if (!class_exists($className) || null === $methods = get_class_methods($className)) { + // failed to get the class or its methods on which an unknown method was called (for example on an anonymous class) + return new UndefinedMethodException($message, $exception); + } + + $candidates = []; + foreach ($methods as $definedMethodName) { + $lev = levenshtein($methodName, $definedMethodName); + if ($lev <= \strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) { + $candidates[] = $definedMethodName; + } + } + + if ($candidates) { + sort($candidates); + $last = array_pop($candidates).'"?'; + if ($candidates) { + $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; + } else { + $candidates = '"'.$last; + } + + $message .= "\nDid you mean to call ".$candidates; + } + + return new UndefinedMethodException($message, $exception); + } +} diff --git a/src/Symfony/Component/ErrorHandler/LICENSE b/src/Symfony/Component/ErrorHandler/LICENSE new file mode 100644 index 000000000000..1a1869751d25 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/ErrorHandler/README.md b/src/Symfony/Component/ErrorHandler/README.md new file mode 100644 index 000000000000..17e1cfd751d0 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/README.md @@ -0,0 +1,12 @@ +ErrorHandler Component +====================== + +The ErrorHandler component provides tools to manage errors and ease debugging PHP code. + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php new file mode 100644 index 000000000000..2699daec18a2 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php @@ -0,0 +1,448 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\DebugClassLoader; + +class DebugClassLoaderTest extends TestCase +{ + /** + * @var int Error reporting level before running tests + */ + private $errorReporting; + + private $loader; + + protected function setUp() + { + $this->errorReporting = error_reporting(E_ALL); + $this->loader = new ClassLoader(); + spl_autoload_register([$this->loader, 'loadClass'], true, true); + DebugClassLoader::enable(); + } + + protected function tearDown() + { + DebugClassLoader::disable(); + spl_autoload_unregister([$this->loader, 'loadClass']); + error_reporting($this->errorReporting); + } + + public function testIdempotence() + { + DebugClassLoader::enable(); + + $functions = spl_autoload_functions(); + foreach ($functions as $function) { + if (\is_array($function) && $function[0] instanceof DebugClassLoader) { + $reflClass = new \ReflectionClass($function[0]); + $reflProp = $reflClass->getProperty('classLoader'); + $reflProp->setAccessible(true); + + $this->assertNotInstanceOf('Symfony\Component\ErrorHandler\DebugClassLoader', $reflProp->getValue($function[0])); + + return; + } + } + + $this->fail('DebugClassLoader did not register'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage boo + */ + public function testThrowingClass() + { + try { + class_exists(__NAMESPACE__.'\Fixtures\Throwing'); + $this->fail('Exception expected'); + } catch (\Exception $e) { + $this->assertSame('boo', $e->getMessage()); + } + + // the second call also should throw + class_exists(__NAMESPACE__.'\Fixtures\Throwing'); + } + + /** + * @expectedException \RuntimeException + */ + public function testNameCaseMismatch() + { + class_exists(__NAMESPACE__.'\TestingCaseMismatch', true); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Case mismatch between class and real file names + */ + public function testFileCaseMismatch() + { + if (!file_exists(__DIR__.'/Fixtures/CaseMismatch.php')) { + $this->markTestSkipped('Can only be run on case insensitive filesystems'); + } + + class_exists(__NAMESPACE__.'\Fixtures\CaseMismatch', true); + } + + /** + * @expectedException \RuntimeException + */ + public function testPsr4CaseMismatch() + { + class_exists(__NAMESPACE__.'\Fixtures\Psr4CaseMismatch', true); + } + + public function testNotPsr0() + { + $this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\NotPSR0', true)); + } + + public function testNotPsr0Bis() + { + $this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\NotPSR0bis', true)); + } + + public function testClassAlias() + { + $this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\ClassAlias', true)); + } + + /** + * @dataProvider provideDeprecatedSuper + */ + public function testDeprecatedSuper($class, $super, $type) + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_DEPRECATED); + + class_exists('Test\\'.__NAMESPACE__.'\\'.$class, true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $xError = [ + 'type' => E_USER_DEPRECATED, + 'message' => 'The "Test\Symfony\Component\ErrorHandler\Tests\\'.$class.'" class '.$type.' "Symfony\Component\ErrorHandler\Tests\Fixtures\\'.$super.'" that is deprecated but this is a test deprecation notice.', + ]; + + $this->assertSame($xError, $lastError); + } + + public function provideDeprecatedSuper() + { + return [ + ['DeprecatedInterfaceClass', 'DeprecatedInterface', 'implements'], + ['DeprecatedParentClass', 'DeprecatedClass', 'extends'], + ]; + } + + public function testInterfaceExtendsDeprecatedInterface() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists('Test\\'.__NAMESPACE__.'\\NonDeprecatedInterfaceClass', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $xError = [ + 'type' => E_USER_NOTICE, + 'message' => '', + ]; + + $this->assertSame($xError, $lastError); + } + + public function testDeprecatedSuperInSameNamespace() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists('Symfony\Bridge\ErrorHandler\Tests\Fixtures\ExtendsDeprecatedParent', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $xError = [ + 'type' => E_USER_NOTICE, + 'message' => '', + ]; + + $this->assertSame($xError, $lastError); + } + + public function testExtendedFinalClass() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + require __DIR__.'/Fixtures/FinalClasses.php'; + + $i = 1; + while (class_exists($finalClass = __NAMESPACE__.'\\Fixtures\\FinalClass'.$i++, false)) { + spl_autoload_call($finalClass); + class_exists('Test\\'.__NAMESPACE__.'\\Extends'.substr($finalClass, strrpos($finalClass, '\\') + 1), true); + } + + error_reporting($e); + restore_error_handler(); + + $this->assertSame([ + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass1" class is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass1".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass2" class is considered final. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass2".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass3" class is considered final comment with @@@ and ***. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass3".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass4" class is considered final. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass4".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass5" class is considered final multiline comment. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass5".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass6" class is considered final. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass6".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass7" class is considered final another multiline comment... It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass7".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalClass8" class is considered final. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsFinalClass8".', + ], $deprecations); + } + + public function testExtendedFinalMethod() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists(__NAMESPACE__.'\\Fixtures\\ExtendedFinalMethod', true); + + error_reporting($e); + restore_error_handler(); + + $xError = [ + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalMethod::finalMethod()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\ErrorHandler\Tests\Fixtures\ExtendedFinalMethod".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalMethod::finalMethod2()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\ErrorHandler\Tests\Fixtures\ExtendedFinalMethod".', + ]; + + $this->assertSame($xError, $deprecations); + } + + public function testExtendedDeprecatedMethodDoesntTriggerAnyNotice() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsAnnotatedClass', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $this->assertSame(['type' => E_USER_NOTICE, 'message' => ''], $lastError); + } + + public function testInternalsUse() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsInternals', true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame($deprecations, [ + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\InternalInterface" interface is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsInternalsParent".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\InternalClass" class is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsInternalsParent".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\InternalTrait" trait is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsInternals".', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\InternalClass::internalMethod()" method is considered internal. It may change without further notice. You should not extend it from "Test\Symfony\Component\ErrorHandler\Tests\ExtendsInternals".', + ]); + } + + public function testExtendedMethodDefinesNewParameters() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists(__NAMESPACE__.'\\Fixtures\SubClassWithAnnotatedParameters', true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame([ + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::quzMethod()" method will require a new "Quz $quz" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\ClassWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::whereAmI()" method will require a new "bool $matrix" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::iAmHere()" method will require a new "$noType" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::iAmHere()" method will require a new "callable(\Throwable|null $reason, mixed $value) $callback" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::iAmHere()" method will require a new "string $param" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::iAmHere()" method will require a new "callable ($a, $b) $anotherOne" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::iAmHere()" method will require a new "Type$WithDollarIsStillAType $ccc" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.', + 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\SubClassWithAnnotatedParameters::isSymfony()" method will require a new "true $yes" argument in the next major version of its parent class "Symfony\Component\ErrorHandler\Tests\Fixtures\ClassWithAnnotatedParameters", not defining it is deprecated.', + ], $deprecations); + } + + public function testUseTraitWithInternalMethod() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists('Test\\'.__NAMESPACE__.'\\UseTraitWithInternalMethod', true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame([], $deprecations); + } + + public function testVirtualUse() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsVirtual', true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame([ + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::sameLineInterfaceMethodNoBraces()".', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::newLineInterfaceMethod()": Some description!', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::newLineInterfaceMethodNoBraces()": Description.', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::invalidInterfaceMethod()".', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::invalidInterfaceMethodNoBraces()".', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::complexInterfaceMethod($arg, ...$args)".', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::complexInterfaceMethodTyped($arg, int ...$args)": Description ...', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::staticMethodNoBraces()".', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::staticMethodTyped(int $arg)": Description.', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualInterface::staticMethodTypedNoBraces()".', + 'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtual" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualSubInterface::subInterfaceMethod()".', + ], $deprecations); + } + + public function testVirtualUseWithMagicCall() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsVirtualMagicCall', true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame([], $deprecations); + } + + public function testEvaluatedCode() + { + $this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\DefinitionInEvaluatedCode', true)); + } +} + +class ClassLoader +{ + public function loadClass($class) + { + } + + public function getClassMap() + { + return [__NAMESPACE__.'\Fixtures\NotPSR0bis' => __DIR__.'/Fixtures/notPsr0Bis.php']; + } + + public function findFile($class) + { + $fixtureDir = __DIR__.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR; + + if (__NAMESPACE__.'\TestingUnsilencing' === $class) { + eval('-- parse error --'); + } elseif (__NAMESPACE__.'\TestingStacking' === $class) { + eval('namespace '.__NAMESPACE__.'; class TestingStacking { function foo() {} }'); + } elseif (__NAMESPACE__.'\TestingCaseMismatch' === $class) { + eval('namespace '.__NAMESPACE__.'; class TestingCaseMisMatch {}'); + } elseif (__NAMESPACE__.'\Fixtures\Psr4CaseMismatch' === $class) { + return $fixtureDir.'psr4'.\DIRECTORY_SEPARATOR.'Psr4CaseMismatch.php'; + } elseif (__NAMESPACE__.'\Fixtures\NotPSR0' === $class) { + return $fixtureDir.'reallyNotPsr0.php'; + } elseif (__NAMESPACE__.'\Fixtures\NotPSR0bis' === $class) { + return $fixtureDir.'notPsr0Bis.php'; + } elseif ('Symfony\Bridge\Debug\Tests\Fixtures\ExtendsDeprecatedParent' === $class) { + eval('namespace Symfony\Bridge\Debug\Tests\Fixtures; class ExtendsDeprecatedParent extends \\'.__NAMESPACE__.'\Fixtures\DeprecatedClass {}'); + } elseif ('Test\\'.__NAMESPACE__.'\DeprecatedParentClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class DeprecatedParentClass extends \\'.__NAMESPACE__.'\Fixtures\DeprecatedClass {}'); + } elseif ('Test\\'.__NAMESPACE__.'\DeprecatedInterfaceClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class DeprecatedInterfaceClass implements \\'.__NAMESPACE__.'\Fixtures\DeprecatedInterface {}'); + } elseif ('Test\\'.__NAMESPACE__.'\NonDeprecatedInterfaceClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class NonDeprecatedInterfaceClass implements \\'.__NAMESPACE__.'\Fixtures\NonDeprecatedInterface {}'); + } elseif ('Test\\'.__NAMESPACE__.'\Float' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class Float {}'); + } elseif (0 === strpos($class, 'Test\\'.__NAMESPACE__.'\ExtendsFinalClass')) { + $classShortName = substr($class, strrpos($class, '\\') + 1); + eval('namespace Test\\'.__NAMESPACE__.'; class '.$classShortName.' extends \\'.__NAMESPACE__.'\Fixtures\\'.substr($classShortName, 7).' {}'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsAnnotatedClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsAnnotatedClass extends \\'.__NAMESPACE__.'\Fixtures\AnnotatedClass { + public function deprecatedMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternals' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternals extends ExtendsInternalsParent { + use \\'.__NAMESPACE__.'\Fixtures\InternalTrait; + + public function internalMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternalsParent' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternalsParent extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface { }'); + } elseif ('Test\\'.__NAMESPACE__.'\UseTraitWithInternalMethod' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class UseTraitWithInternalMethod { use \\'.__NAMESPACE__.'\Fixtures\TraitWithInternalMethod; }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtual' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtual extends ExtendsVirtualParent implements \\'.__NAMESPACE__.'\Fixtures\VirtualSubInterface { + public function ownClassMethod() { } + public function classMethod() { } + public function sameLineInterfaceMethodNoBraces() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualParent' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualParent extends ExtendsVirtualAbstract { + public function ownParentMethod() { } + public function traitMethod() { } + public function sameLineInterfaceMethod() { } + public function staticMethodNoBraces() { } // should be static + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualAbstract' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; abstract class ExtendsVirtualAbstract extends ExtendsVirtualAbstractBase { + public static function staticMethod() { } + public function ownAbstractMethod() { } + public function interfaceMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualAbstractBase' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; abstract class ExtendsVirtualAbstractBase extends \\'.__NAMESPACE__.'\Fixtures\VirtualClass implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface { + public function ownAbstractBaseMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualMagicCall' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualMagicCall extends \\'.__NAMESPACE__.'\Fixtures\VirtualClassMagicCall implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface { + }'); + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php new file mode 100644 index 000000000000..af667e49ec99 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -0,0 +1,563 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LogLevel; +use Psr\Log\NullLogger; +use Symfony\Component\ErrorHandler\BufferingLogger; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne; +use Symfony\Component\ErrorHandler\Tests\Fixtures\LoggerThatSetAnErrorHandler; + +/** + * ErrorHandlerTest. + * + * @author Robert Schönthal + * @author Nicolas Grekas + */ +class ErrorHandlerTest extends TestCase +{ + public function testRegister() + { + $handler = ErrorHandler::register(); + + try { + $this->assertInstanceOf('Symfony\Component\ErrorHandler\ErrorHandler', $handler); + $this->assertSame($handler, ErrorHandler::register()); + + $newHandler = new ErrorHandler(); + + $this->assertSame($handler, ErrorHandler::register($newHandler, false)); + $h = set_error_handler('var_dump'); + restore_error_handler(); + $this->assertSame([$handler, 'handleError'], $h); + + try { + $this->assertSame($newHandler, ErrorHandler::register($newHandler, true)); + $h = set_error_handler('var_dump'); + restore_error_handler(); + $this->assertSame([$newHandler, 'handleError'], $h); + } catch (\Exception $e) { + } + + restore_error_handler(); + restore_exception_handler(); + + if (isset($e)) { + throw $e; + } + } catch (\Exception $e) { + } + + restore_error_handler(); + restore_exception_handler(); + + if (isset($e)) { + throw $e; + } + } + + public function testErrorGetLast() + { + $handler = ErrorHandler::register(); + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $handler->setDefaultLogger($logger); + $handler->screamAt(E_ALL); + + try { + @trigger_error('Hello', E_USER_WARNING); + $expected = [ + 'type' => E_USER_WARNING, + 'message' => 'Hello', + 'file' => __FILE__, + 'line' => __LINE__ - 5, + ]; + $this->assertSame($expected, error_get_last()); + } catch (\Exception $e) { + restore_error_handler(); + restore_exception_handler(); + + throw $e; + } + } + + public function testNotice() + { + ErrorHandler::register(); + + try { + self::triggerNotice($this); + $this->fail('ErrorException expected'); + } catch (\ErrorException $exception) { + // if an exception is thrown, the test passed + $this->assertEquals(E_NOTICE, $exception->getSeverity()); + $this->assertEquals(__FILE__, $exception->getFile()); + $this->assertRegExp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage()); + + $trace = $exception->getTrace(); + + $this->assertEquals(__FILE__, $trace[0]['file']); + $this->assertEquals(__CLASS__, $trace[0]['class']); + $this->assertEquals('triggerNotice', $trace[0]['function']); + $this->assertEquals('::', $trace[0]['type']); + + $this->assertEquals(__FILE__, $trace[0]['file']); + $this->assertEquals(__CLASS__, $trace[1]['class']); + $this->assertEquals(__FUNCTION__, $trace[1]['function']); + $this->assertEquals('->', $trace[1]['type']); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + // dummy function to test trace in error handler. + private static function triggerNotice($that) + { + $that->assertSame('', $foo.$foo.$bar); + } + + public function testConstruct() + { + try { + $handler = ErrorHandler::register(); + $handler->throwAt(3, true); + $this->assertEquals(3 | E_RECOVERABLE_ERROR | E_USER_ERROR, $handler->throwAt(0)); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function testDefaultLogger() + { + try { + $handler = ErrorHandler::register(); + + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + + $handler->setDefaultLogger($logger, E_NOTICE); + $handler->setDefaultLogger($logger, [E_USER_NOTICE => LogLevel::CRITICAL]); + + $loggers = [ + E_DEPRECATED => [null, LogLevel::INFO], + E_USER_DEPRECATED => [null, LogLevel::INFO], + E_NOTICE => [$logger, LogLevel::WARNING], + E_USER_NOTICE => [$logger, LogLevel::CRITICAL], + E_STRICT => [null, LogLevel::WARNING], + E_WARNING => [null, LogLevel::WARNING], + E_USER_WARNING => [null, LogLevel::WARNING], + E_COMPILE_WARNING => [null, LogLevel::WARNING], + E_CORE_WARNING => [null, LogLevel::WARNING], + E_USER_ERROR => [null, LogLevel::CRITICAL], + E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], + E_COMPILE_ERROR => [null, LogLevel::CRITICAL], + E_PARSE => [null, LogLevel::CRITICAL], + E_ERROR => [null, LogLevel::CRITICAL], + E_CORE_ERROR => [null, LogLevel::CRITICAL], + ]; + $this->assertSame($loggers, $handler->setLoggers([])); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function testHandleError() + { + try { + $handler = ErrorHandler::register(); + $handler->throwAt(0, true); + $this->assertFalse($handler->handleError(0, 'foo', 'foo.php', 12, [])); + + restore_error_handler(); + restore_exception_handler(); + + $handler = ErrorHandler::register(); + $handler->throwAt(3, true); + $this->assertFalse($handler->handleError(4, 'foo', 'foo.php', 12, [])); + + restore_error_handler(); + restore_exception_handler(); + + $handler = ErrorHandler::register(); + $handler->throwAt(3, true); + try { + $handler->handleError(4, 'foo', 'foo.php', 12, []); + } catch (\ErrorException $e) { + $this->assertSame('Parse Error: foo', $e->getMessage()); + $this->assertSame(4, $e->getSeverity()); + $this->assertSame('foo.php', $e->getFile()); + $this->assertSame(12, $e->getLine()); + } + + restore_error_handler(); + restore_exception_handler(); + + $handler = ErrorHandler::register(); + $handler->throwAt(E_USER_DEPRECATED, true); + $this->assertFalse($handler->handleError(E_USER_DEPRECATED, 'foo', 'foo.php', 12, [])); + + restore_error_handler(); + restore_exception_handler(); + + $handler = ErrorHandler::register(); + $handler->throwAt(E_DEPRECATED, true); + $this->assertFalse($handler->handleError(E_DEPRECATED, 'foo', 'foo.php', 12, [])); + + restore_error_handler(); + restore_exception_handler(); + + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + + $warnArgCheck = function ($logLevel, $message, $context) { + $this->assertEquals('info', $logLevel); + $this->assertEquals('User Deprecated: foo', $message); + $this->assertArrayHasKey('exception', $context); + $exception = $context['exception']; + $this->assertInstanceOf(\ErrorException::class, $exception); + $this->assertSame('User Deprecated: foo', $exception->getMessage()); + $this->assertSame(E_USER_DEPRECATED, $exception->getSeverity()); + }; + + $logger + ->expects($this->once()) + ->method('log') + ->willReturnCallback($warnArgCheck) + ; + + $handler = ErrorHandler::register(); + $handler->setDefaultLogger($logger, E_USER_DEPRECATED); + $this->assertTrue($handler->handleError(E_USER_DEPRECATED, 'foo', 'foo.php', 12, [])); + + restore_error_handler(); + restore_exception_handler(); + + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + + $line = null; + $logArgCheck = function ($level, $message, $context) use (&$line) { + $this->assertEquals('Notice: Undefined variable: undefVar', $message); + $this->assertArrayHasKey('exception', $context); + $exception = $context['exception']; + $this->assertInstanceOf(SilencedErrorContext::class, $exception); + $this->assertSame(E_NOTICE, $exception->getSeverity()); + $this->assertSame(__FILE__, $exception->getFile()); + $this->assertSame($line, $exception->getLine()); + $this->assertNotEmpty($exception->getTrace()); + $this->assertSame(1, $exception->count); + }; + + $logger + ->expects($this->once()) + ->method('log') + ->willReturnCallback($logArgCheck) + ; + + $handler = ErrorHandler::register(); + $handler->setDefaultLogger($logger, E_NOTICE); + $handler->screamAt(E_NOTICE); + unset($undefVar); + $line = __LINE__ + 1; + @$undefVar++; + + restore_error_handler(); + restore_exception_handler(); + } catch (\Exception $e) { + restore_error_handler(); + restore_exception_handler(); + + throw $e; + } + } + + public function testHandleUserError() + { + try { + $handler = ErrorHandler::register(); + $handler->throwAt(0, true); + + $e = null; + $x = new \Exception('Foo'); + + try { + $f = new Fixtures\ToStringThrower($x); + $f .= ''; // Trigger $f->__toString() + } catch (\Exception $e) { + } + + $this->assertSame($x, $e); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function testHandleDeprecation() + { + $logArgCheck = function ($level, $message, $context) { + $this->assertEquals(LogLevel::INFO, $level); + $this->assertArrayHasKey('exception', $context); + $exception = $context['exception']; + $this->assertInstanceOf(\ErrorException::class, $exception); + $this->assertSame('User Deprecated: Foo deprecation', $exception->getMessage()); + }; + + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $logger + ->expects($this->once()) + ->method('log') + ->willReturnCallback($logArgCheck) + ; + + $handler = new ErrorHandler(); + $handler->setDefaultLogger($logger); + @$handler->handleError(E_USER_DEPRECATED, 'Foo deprecation', __FILE__, __LINE__, []); + + restore_error_handler(); + } + + public function testHandleException() + { + try { + $handler = ErrorHandler::register(); + + $exception = new \Exception('foo'); + + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + + $logArgCheck = function ($level, $message, $context) { + $this->assertSame('Uncaught Exception: foo', $message); + $this->assertArrayHasKey('exception', $context); + $this->assertInstanceOf(\Exception::class, $context['exception']); + }; + + $logger + ->expects($this->exactly(2)) + ->method('log') + ->willReturnCallback($logArgCheck) + ; + + $handler->setDefaultLogger($logger, E_ERROR); + + try { + $handler->handleException($exception); + $this->fail('Exception expected'); + } catch (\Exception $e) { + $this->assertSame($exception, $e); + } + + $handler->setExceptionHandler(function ($e) use ($exception) { + $this->assertSame($exception, $e); + }); + + $handler->handleException($exception); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function testBootstrappingLogger() + { + $bootLogger = new BufferingLogger(); + $handler = new ErrorHandler($bootLogger); + + $loggers = [ + E_DEPRECATED => [$bootLogger, LogLevel::INFO], + E_USER_DEPRECATED => [$bootLogger, LogLevel::INFO], + E_NOTICE => [$bootLogger, LogLevel::WARNING], + E_USER_NOTICE => [$bootLogger, LogLevel::WARNING], + E_STRICT => [$bootLogger, LogLevel::WARNING], + E_WARNING => [$bootLogger, LogLevel::WARNING], + E_USER_WARNING => [$bootLogger, LogLevel::WARNING], + E_COMPILE_WARNING => [$bootLogger, LogLevel::WARNING], + E_CORE_WARNING => [$bootLogger, LogLevel::WARNING], + E_USER_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_RECOVERABLE_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_COMPILE_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_PARSE => [$bootLogger, LogLevel::CRITICAL], + E_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_CORE_ERROR => [$bootLogger, LogLevel::CRITICAL], + ]; + + $this->assertSame($loggers, $handler->setLoggers([])); + + $handler->handleError(E_DEPRECATED, 'Foo message', __FILE__, 123, []); + + $logs = $bootLogger->cleanLogs(); + + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertSame('info', $log[0]); + $this->assertSame('Deprecated: Foo message', $log[1]); + $this->assertArrayHasKey('exception', $log[2]); + $exception = $log[2]['exception']; + $this->assertInstanceOf(\ErrorException::class, $exception); + $this->assertSame('Deprecated: Foo message', $exception->getMessage()); + $this->assertSame(__FILE__, $exception->getFile()); + $this->assertSame(123, $exception->getLine()); + $this->assertSame(E_DEPRECATED, $exception->getSeverity()); + + $bootLogger->log(LogLevel::WARNING, 'Foo message', ['exception' => $exception]); + + $mockLogger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $mockLogger->expects($this->once()) + ->method('log') + ->with(LogLevel::WARNING, 'Foo message', ['exception' => $exception]); + + $handler->setLoggers([E_DEPRECATED => [$mockLogger, LogLevel::WARNING]]); + } + + public function testSettingLoggerWhenExceptionIsBuffered() + { + $bootLogger = new BufferingLogger(); + $handler = new ErrorHandler($bootLogger); + + $exception = new \Exception('Foo message'); + + $mockLogger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $mockLogger->expects($this->once()) + ->method('log') + ->with(LogLevel::CRITICAL, 'Uncaught Exception: Foo message', ['exception' => $exception]); + + $handler->setExceptionHandler(function () use ($handler, $mockLogger) { + $handler->setDefaultLogger($mockLogger); + }); + + $handler->handleException($exception); + } + + public function testHandleFatalError() + { + try { + $handler = ErrorHandler::register(); + + $error = [ + 'type' => E_PARSE, + 'message' => 'foo', + 'file' => 'bar', + 'line' => 123, + ]; + + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + + $logArgCheck = function ($level, $message, $context) { + $this->assertEquals('Fatal Parse Error: foo', $message); + $this->assertArrayHasKey('exception', $context); + $this->assertInstanceOf(\Exception::class, $context['exception']); + }; + + $logger + ->expects($this->once()) + ->method('log') + ->willReturnCallback($logArgCheck) + ; + + $handler->setDefaultLogger($logger, E_PARSE); + + $handler->handleFatalError($error); + + restore_error_handler(); + restore_exception_handler(); + } catch (\Exception $e) { + restore_error_handler(); + restore_exception_handler(); + + throw $e; + } + } + + public function testHandleErrorException() + { + $exception = new \Error("Class 'IReallyReallyDoNotExistAnywhereInTheRepositoryISwear' not found"); + + $handler = new ErrorHandler(); + $handler->setExceptionHandler(function () use (&$args) { + $args = \func_get_args(); + }); + + $handler->handleException($exception); + + $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $args[0]); + $this->assertStringStartsWith("Attempted to load class \"IReallyReallyDoNotExistAnywhereInTheRepositoryISwear\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage()); + } + + /** + * @expectedException \Exception + */ + public function testCustomExceptionHandler() + { + $handler = new ErrorHandler(); + $handler->setExceptionHandler(function ($e) use ($handler) { + $handler->handleException($e); + }); + + $handler->handleException(new \Exception()); + } + + /** + * @dataProvider errorHandlerWhenLoggingProvider + */ + public function testErrorHandlerWhenLogging($previousHandlerWasDefined, $loggerSetsAnotherHandler, $nextHandlerIsDefined) + { + try { + if ($previousHandlerWasDefined) { + set_error_handler('count'); + } + + $logger = $loggerSetsAnotherHandler ? new LoggerThatSetAnErrorHandler() : new NullLogger(); + + $handler = ErrorHandler::register(); + $handler->setDefaultLogger($logger); + + if ($nextHandlerIsDefined) { + $handler = ErrorHandlerThatUsesThePreviousOne::register(); + } + + @trigger_error('foo', E_USER_DEPRECATED); + @trigger_error('bar', E_USER_DEPRECATED); + + $this->assertSame([$handler, 'handleError'], set_error_handler('var_dump')); + + if ($logger instanceof LoggerThatSetAnErrorHandler) { + $this->assertCount(2, $logger->cleanLogs()); + } + + restore_error_handler(); + + if ($previousHandlerWasDefined) { + restore_error_handler(); + } + + if ($nextHandlerIsDefined) { + restore_error_handler(); + } + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function errorHandlerWhenLoggingProvider() + { + foreach ([false, true] as $previousHandlerWasDefined) { + foreach ([false, true] as $loggerSetsAnotherHandler) { + foreach ([false, true] as $nextHandlerIsDefined) { + yield [$previousHandlerWasDefined, $loggerSetsAnotherHandler, $nextHandlerIsDefined]; + } + } + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php new file mode 100644 index 000000000000..694177f91e43 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; +use Symfony\Component\ErrorHandler\ExceptionHandler; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +require_once __DIR__.'/HeaderMock.php'; + +class ExceptionHandlerTest extends TestCase +{ + protected function setUp() + { + testHeader(); + } + + protected function tearDown() + { + testHeader(); + } + + /** + * @group legacy + */ + public function testDebug() + { + $handler = new ExceptionHandler(false); + + ob_start(); + $handler->sendPhpResponse(new \RuntimeException('Foo')); + $response = ob_get_clean(); + + $this->assertContains('The server returned a "500 Internal Server Error".', $response); + $this->assertNotContains('
', $response); + + $handler = new ExceptionHandler(true); + + ob_start(); + $handler->sendPhpResponse(new \RuntimeException('Foo')); + $response = ob_get_clean(); + + $this->assertContains('

Foo

', $response); + $this->assertContains('
', $response); + + // taken from https://www.owasp.org/index.php/Cross-site_Scripting_(XSS) + $htmlWithXss = ' click me! '; + ob_start(); + $handler->sendPhpResponse(new \RuntimeException($htmlWithXss)); + $response = ob_get_clean(); + + $this->assertContains(sprintf('

%s

', htmlspecialchars($htmlWithXss, ENT_COMPAT | ENT_SUBSTITUTE, 'UTF-8')), $response); + } + + public function testStatusCode() + { + $handler = new ExceptionHandler(false, 'iso8859-1'); + + ob_start(); + $handler->sendPhpResponse(new NotFoundHttpException('Foo')); + $response = ob_get_clean(); + + $this->assertContains('The server returned a "404 Not Found".', $response); + + $expectedHeaders = [ + ['HTTP/1.0 404', true, null], + ['Content-Type: text/html; charset=iso8859-1', true, null], + ]; + + $this->assertSame($expectedHeaders, testHeader()); + } + + public function testHeaders() + { + $handler = new ExceptionHandler(false, 'iso8859-1'); + + ob_start(); + $handler->sendPhpResponse(new MethodNotAllowedHttpException(['POST'])); + $response = ob_get_clean(); + + $expectedHeaders = [ + ['HTTP/1.0 405', true, null], + ['Allow: POST', false, null], + ['Content-Type: text/html; charset=iso8859-1', true, null], + ]; + + $this->assertSame($expectedHeaders, testHeader()); + } + + public function testNestedExceptions() + { + $handler = new ExceptionHandler(true); + ob_start(); + $handler->sendPhpResponse(new \RuntimeException('Foo', 0, new \RuntimeException('Bar'))); + $response = ob_get_clean(); + + $this->assertStringMatchesFormat('%A

Foo

%A

Bar

%A', $response); + } + + public function testHandle() + { + $exception = new \Exception('foo'); + + $handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); + $handler + ->expects($this->exactly(2)) + ->method('sendPhpResponse'); + + $handler->handle($exception); + + $handler->setHandler(function ($e) use ($exception) { + $this->assertSame($exception, $e); + }); + + $handler->handle($exception); + } + + public function testHandleOutOfMemoryException() + { + $exception = new OutOfMemoryException('foo', 0, E_ERROR, __FILE__, __LINE__); + + $handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); + $handler + ->expects($this->once()) + ->method('sendPhpResponse'); + + $handler->setHandler(function ($e) { + $this->fail('OutOfMemoryException should bypass the handler'); + }); + + $handler->handle($exception); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php new file mode 100644 index 000000000000..6057c313f411 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler; + +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\DebugClassLoader; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler; + +class ClassNotFoundFatalErrorHandlerTest extends TestCase +{ + public static function setUpBeforeClass() + { + foreach (spl_autoload_functions() as $function) { + if (!\is_array($function)) { + continue; + } + + // get class loaders wrapped by DebugClassLoader + if ($function[0] instanceof DebugClassLoader) { + $function = $function[0]->getClassLoader(); + } + + if ($function[0] instanceof ComposerClassLoader) { + $function[0]->add('Symfony_Component_ErrorHandler_Tests_Fixtures', \dirname(\dirname(\dirname(\dirname(\dirname(__DIR__)))))); + break; + } + } + } + + /** + * @dataProvider provideClassNotFoundData + */ + public function testHandleClassNotFound($error, $translatedMessage, $autoloader = null) + { + if ($autoloader) { + // Unregister all autoloaders to ensure the custom provided + // autoloader is the only one to be used during the test run. + $autoloaders = spl_autoload_functions(); + array_map('spl_autoload_unregister', $autoloaders); + spl_autoload_register($autoloader); + } + + $handler = new ClassNotFoundFatalErrorHandler(); + + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + if ($autoloader) { + spl_autoload_unregister($autoloader); + array_map('spl_autoload_register', $autoloaders); + } + + $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $exception); + $this->assertSame($translatedMessage, $exception->getMessage()); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + public function provideClassNotFoundData() + { + $autoloader = new ComposerClassLoader(); + $autoloader->add('Symfony\Component\ErrorHandler\Exception\\', realpath(__DIR__.'/../../Exception')); + $autoloader->add('Symfony_Component_ErrorHandler_Tests_Fixtures', realpath(__DIR__.'/../../Tests/Fixtures')); + + $debugClassLoader = new DebugClassLoader([$autoloader, 'loadClass']); + + return [ + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'WhizBangFactory\' not found', + ], + "Attempted to load class \"WhizBangFactory\" from the global namespace.\nDid you forget a \"use\" statement?", + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\WhizBangFactory\' not found', + ], + "Attempted to load class \"WhizBangFactory\" from namespace \"Foo\\Bar\".\nDid you forget a \"use\" statement for another namespace?", + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'UndefinedFunctionException\' not found', + ], + "Attempted to load class \"UndefinedFunctionException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'PEARClass\' not found', + ], + "Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_ErrorHandler_Tests_Fixtures_PEARClass\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', + ], + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', + ], + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException\"?", + [$autoloader, 'loadClass'], + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', + ], + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', + ], + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\\Bar\".\nDid you forget a \"use\" statement for another namespace?", + function ($className) { /* do nothing here */ }, + ], + ]; + } + + public function testCannotRedeclareClass() + { + if (!file_exists(__DIR__.'/../FIXTURES2/REQUIREDTWICE.PHP')) { + $this->markTestSkipped('Can only be run on case insensitive filesystems'); + } + + require_once __DIR__.'/../FIXTURES2/REQUIREDTWICE.PHP'; + + $error = [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\RequiredTwice\' not found', + ]; + + $handler = new ClassNotFoundFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $exception); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php new file mode 100644 index 000000000000..c24109b1b352 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; + +class UndefinedFunctionFatalErrorHandlerTest extends TestCase +{ + /** + * @dataProvider provideUndefinedFunctionData + */ + public function testUndefinedFunction($error, $translatedMessage) + { + $handler = new UndefinedFunctionFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException', $exception); + // class names are case insensitive and PHP do not return the same + $this->assertSame(strtolower($translatedMessage), strtolower($exception->getMessage())); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + public function provideUndefinedFunctionData() + { + return [ + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function test_namespaced_function()', + ], + "Attempted to call function \"test_namespaced_function\" from the global namespace.\nDid you mean to call \"\\symfony\\component\\errorhandler\\tests\\fatalerrorhandler\\test_namespaced_function\"?", + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function Foo\\Bar\\Baz\\test_namespaced_function()', + ], + "Attempted to call function \"test_namespaced_function\" from namespace \"Foo\\Bar\\Baz\".\nDid you mean to call \"\\symfony\\component\\errorhandler\\tests\\fatalerrorhandler\\test_namespaced_function\"?", + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function foo()', + ], + 'Attempted to call function "foo" from the global namespace.', + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function Foo\\Bar\\Baz\\foo()', + ], + 'Attempted to call function "foo" from namespace "Foo\Bar\Baz".', + ], + ]; + } +} + +function test_namespaced_function() +{ +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php new file mode 100644 index 000000000000..b91792b44032 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler; + +class UndefinedMethodFatalErrorHandlerTest extends TestCase +{ + /** + * @dataProvider provideUndefinedMethodData + */ + public function testUndefinedMethod($error, $translatedMessage) + { + $handler = new UndefinedMethodFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\UndefinedMethodException', $exception); + $this->assertSame($translatedMessage, $exception->getMessage()); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + public function provideUndefinedMethodData() + { + return [ + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined method SplObjectStorage::what()', + ], + 'Attempted to call an undefined method named "what" of class "SplObjectStorage".', + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined method SplObjectStorage::walid()', + ], + "Attempted to call an undefined method named \"walid\" of class \"SplObjectStorage\".\nDid you mean to call \"valid\"?", + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined method SplObjectStorage::offsetFet()', + ], + "Attempted to call an undefined method named \"offsetFet\" of class \"SplObjectStorage\".\nDid you mean to call e.g. \"offsetGet\", \"offsetSet\" or \"offsetUnset\"?", + ], + [ + [ + 'type' => 1, + 'message' => 'Call to undefined method class@anonymous::test()', + 'file' => '/home/possum/work/symfony/test.php', + 'line' => 11, + ], + 'Attempted to call an undefined method named "test" of class "class@anonymous".', + ], + ]; + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/AnnotatedClass.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/AnnotatedClass.php new file mode 100644 index 000000000000..bbd19e15a6f4 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/AnnotatedClass.php @@ -0,0 +1,13 @@ +exception = $e; + } + + public function __toString() + { + try { + throw $this->exception; + } catch (\Exception $e) { + // Using user_error() here is on purpose so we do not forget + // that this alias also should work alongside with trigger_error(). + return trigger_error($e, E_USER_ERROR); + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/Fixtures/TraitWithAnnotatedParameters.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/TraitWithAnnotatedParameters.php new file mode 100644 index 000000000000..c9abd4096f3e --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/TraitWithAnnotatedParameters.php @@ -0,0 +1,13 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +function headers_sent() +{ + return false; +} + +function header($str, $replace = true, $status = null) +{ + Tests\testHeader($str, $replace, $status); +} + +namespace Symfony\Component\ErrorHandler\Tests; + +function testHeader() +{ + static $headers = []; + + if (!$h = \func_get_args()) { + $h = $headers; + $headers = []; + + return $h; + } + + $headers[] = \func_get_args(); +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/MockExceptionHandler.php b/src/Symfony/Component/ErrorHandler/Tests/MockExceptionHandler.php new file mode 100644 index 000000000000..700990de58ec --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/MockExceptionHandler.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests; + +use Symfony\Component\ErrorHandler\ExceptionHandler; + +class MockExceptionHandler extends ExceptionHandler +{ + public $e; + + public function handle(\Exception $e) + { + $this->e = $e; + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/debug_class_loader.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/debug_class_loader.phpt new file mode 100644 index 000000000000..865dbd7e03e1 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/debug_class_loader.phpt @@ -0,0 +1,27 @@ +--TEST-- +Test DebugClassLoader with previously loaded parents +--FILE-- + +--EXPECTF-- +The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalMethod::finalMethod()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\ErrorHandler\Tests\Fixtures\ExtendedFinalMethod". +The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalMethod::finalMethod2()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\ErrorHandler\Tests\Fixtures\ExtendedFinalMethod". diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_hander.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_hander.phpt new file mode 100644 index 000000000000..034d5a5292a4 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_hander.phpt @@ -0,0 +1,47 @@ +--TEST-- +Test catching fatal errors when handlers are nested +--INI-- +display_errors=0 +--FILE-- + +--EXPECTF-- +object(Symfony\Component\ErrorHandler\Exception\ClassNotFoundException)#%d (8) { + ["message":protected]=> + string(138) "Attempted to load class "missing" from namespace "Symfony\Component\ErrorHandler". +Did you forget a "use" statement for another namespace?" + ["string":"Exception":private]=> + string(0) "" + ["code":protected]=> + int(0) + ["file":protected]=> + string(%d) "%s" + ["line":protected]=> + int(%d) + ["trace":"Exception":private]=> + array(%d) {%A} + ["previous":"Exception":private]=> + NULL + ["severity":protected]=> + int(1) +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt new file mode 100644 index 000000000000..82a9006d840f --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt @@ -0,0 +1,35 @@ +--TEST-- +Test rethrowing in custom exception handler +--FILE-- +setDefaultLogger(new TestLogger()); +ini_set('display_errors', 1); + +throw new \Exception('foo'); +?> +--EXPECTF-- +Uncaught Exception: foo +123 +Fatal error: Uncaught %s:25 +Stack trace: +%a diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt new file mode 100644 index 000000000000..532fe922410c --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt @@ -0,0 +1,42 @@ +--TEST-- +Test catching fatal errors when handlers are nested +--FILE-- +setExceptionHandler('print_r'); + +if (true) { + class Broken implements \JsonSerializable + { + } +} + +?> +--EXPECTF-- +array(1) { + [0]=> + string(37) "Error and exception handlers do match" +} +object(Symfony\Component\ErrorHandler\Exception\FatalErrorException)#%d (%d) { + ["message":protected]=> + string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" +%a +} diff --git a/src/Symfony/Component/ErrorHandler/composer.json b/src/Symfony/Component/ErrorHandler/composer.json new file mode 100644 index 000000000000..3794930f35b9 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/composer.json @@ -0,0 +1,41 @@ +{ + "name": "symfony/error-handler", + "type": "library", + "description": "Symfony ErrorHandler Component", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.1.3", + "psr/log": "~1.0", + "symfony/error-renderer": "^4.4|^5.0" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\ErrorHandler\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/phpunit.xml.dist b/src/Symfony/Component/ErrorHandler/phpunit.xml.dist new file mode 100644 index 000000000000..6c42fd1815b2 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/phpunit.xml.dist @@ -0,0 +1,33 @@ + + + + + + + + + + ./Tests/ + + + ./Resources/ext/tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php index 29d1685583b3..8bbd6e695c8a 100644 --- a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php +++ b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php @@ -11,7 +11,7 @@ namespace Symfony\Component\ErrorRenderer\Exception; -use Symfony\Component\Debug\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; diff --git a/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php index 5994be23973e..0220d75b9e56 100644 --- a/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\ErrorRenderer\Tests\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/src/Symfony/Component/ErrorRenderer/composer.json b/src/Symfony/Component/ErrorRenderer/composer.json index bf46db539d56..4ce8a991f428 100644 --- a/src/Symfony/Component/ErrorRenderer/composer.json +++ b/src/Symfony/Component/ErrorRenderer/composer.json @@ -20,12 +20,10 @@ "psr/log": "~1.0" }, "require-dev": { - "symfony/debug": "^4.4", "symfony/dependency-injection": "^4.4", "symfony/http-kernel": "^4.4" }, "conflict": { - "symfony/debug": "<4.4", "symfony/http-kernel": "<4.4" }, "autoload": { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 405a951526bd..b71f33c5686e 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -11,7 +11,7 @@ namespace Symfony\Component\HttpKernel\DataCollector; -use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php index 2659d34de649..728ebe35ed56 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/AddAnnotatedClassesToCachePass.php @@ -12,9 +12,9 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; use Composer\Autoload\ClassLoader; -use Symfony\Component\Debug\DebugClassLoader; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\ErrorHandler\DebugClassLoader; use Symfony\Component\HttpKernel\Kernel; /** diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php index 9d45d8378650..4f7a54291e52 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -15,8 +15,8 @@ use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\ExceptionHandler; use Symfony\Component\ErrorRenderer\ErrorRenderer; use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException; diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index e6532861215a..4f33015653b6 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -16,7 +16,6 @@ use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\Loader\DelegatingLoader; use Symfony\Component\Config\Loader\LoaderResolver; -use Symfony\Component\Debug\DebugClassLoader; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,6 +28,7 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\ErrorHandler\DebugClassLoader; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php index 261f098a72c1..3010c5e02eb8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -106,7 +106,7 @@ public function testCollect($nb, $logs, $expectedLogs, $expectedDeprecationCount $logs = array_map(function ($v) { if (isset($v['context']['exception'])) { $e = &$v['context']['exception']; - $e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\Debug\Exception\SilencedErrorContext\0severity"]]; + $e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\ErrorHandler\Exception\SilencedErrorContext\0severity"]]; } return $v; diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php index 1e52bce7a359..746756cf01c6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php @@ -19,8 +19,8 @@ use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\ExceptionHandler; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\KernelEvent; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 52d026be20c3..fcea6c32c4c7 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.1.3", - "symfony/debug": "^4.4|^5.0", + "symfony/error-handler": "^4.4|^5.0", "symfony/error-renderer": "^4.4|^5.0", "symfony/event-dispatcher": "^4.3", "symfony/http-foundation": "^4.4|^5.0", diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index 02bcaec43012..7f2ee3ca5fe5 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -38,8 +38,7 @@ }, "conflict": { "symfony/event-dispatcher": "<4.3", - "symfony/http-kernel": "<4.4", - "symfony/debug": "<4.4" + "symfony/http-kernel": "<4.4" }, "suggest": { "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php index ec168c8da9c9..60cffcb6b73a 100644 --- a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php @@ -11,7 +11,7 @@ namespace Symfony\Component\VarDumper\Caster; -use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index ea2b45ffa1a0..ed16de76bfaf 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -85,7 +85,7 @@ abstract class AbstractCloner implements ClonerInterface 'Symfony\Component\VarDumper\Caster\TraceStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castTraceStub'], 'Symfony\Component\VarDumper\Caster\FrameStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFrameStub'], 'Symfony\Component\VarDumper\Cloner\AbstractCloner' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], - 'Symfony\Component\Debug\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'], + 'Symfony\Component\ErrorHandler\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'], 'ProxyManager\Proxy\ProxyInterface' => ['Symfony\Component\VarDumper\Caster\ProxyManagerCaster', 'castProxy'], 'PHPUnit_Framework_MockObject_MockObject' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy