From aaa0cdf523ae148d26786f69512ac70f68db8658 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Mon, 5 Aug 2019 16:25:47 +0200 Subject: [PATCH 1/7] [ErrorHandler] Rework fatal error handlers --- .../FrameworkBundle/Console/Application.php | 4 +- src/Symfony/Component/Console/Application.php | 4 +- .../Exception/ClassNotFoundException.php | 4 +- .../Debug/Exception/FatalErrorException.php | 4 +- .../Debug/Exception/FatalThrowableError.php | 4 +- .../Debug/Exception/OutOfMemoryException.php | 4 +- .../Exception/UndefinedFunctionException.php | 4 +- .../Exception/UndefinedMethodException.php | 4 +- .../UndefinedFunctionFatalErrorHandler.php | 4 +- .../UndefinedMethodFatalErrorHandler.php | 4 +- .../ErrorHandler/Error/ClassNotFoundError.php | 33 ++++ .../FatalError.php} | 43 +++-- .../OutOfMemoryError.php} | 9 +- .../Error/UndefinedFunctionError.php | 33 ++++ .../Error/UndefinedMethodError.php | 33 ++++ .../ClassNotFoundErrorEnhancer.php} | 28 +-- .../ErrorEnhancer/ErrorEnhancerInterface.php | 20 ++ .../UndefinedFunctionErrorEnhancer.php} | 27 +-- .../UndefinedMethodErrorEnhancer.php} | 23 ++- .../Component/ErrorHandler/ErrorHandler.php | 89 ++++----- .../Exception/ClassNotFoundException.php | 36 ---- ...lThrowableError.php => ErrorException.php} | 13 +- .../Exception/UndefinedFunctionException.php | 36 ---- .../Exception/UndefinedMethodException.php | 36 ---- .../FatalErrorHandlerInterface.php | 31 --- .../ClassNotFoundErrorEnhancerTest.php | 149 +++++++++++++++ .../UndefinedFunctionErrorEnhancerTest.php | 62 ++++++ .../UndefinedMethodErrorEnhancerTest.php | 57 ++++++ .../ErrorHandler/Tests/ErrorHandlerTest.php | 6 +- .../ClassNotFoundFatalErrorHandlerTest.php | 180 ------------------ ...UndefinedFunctionFatalErrorHandlerTest.php | 81 -------- .../UndefinedMethodFatalErrorHandlerTest.php | 76 -------- ...r.phpt => decorate_exception_handler.phpt} | 12 +- .../phpt/fatal_with_nested_handlers.phpt | 13 +- .../Exception/FlattenException.php | 4 +- .../Tests/Exception/FlattenExceptionTest.php | 4 +- .../EventListener/DebugHandlersListener.php | 6 +- 37 files changed, 547 insertions(+), 633 deletions(-) create mode 100644 src/Symfony/Component/ErrorHandler/Error/ClassNotFoundError.php rename src/Symfony/Component/ErrorHandler/{Exception/FatalErrorException.php => Error/FatalError.php} (68%) rename src/Symfony/Component/ErrorHandler/{Exception/OutOfMemoryException.php => Error/OutOfMemoryError.php} (56%) create mode 100644 src/Symfony/Component/ErrorHandler/Error/UndefinedFunctionError.php create mode 100644 src/Symfony/Component/ErrorHandler/Error/UndefinedMethodError.php rename src/Symfony/Component/ErrorHandler/{FatalErrorHandler/ClassNotFoundFatalErrorHandler.php => ErrorEnhancer/ClassNotFoundErrorEnhancer.php} (87%) create mode 100644 src/Symfony/Component/ErrorHandler/ErrorEnhancer/ErrorEnhancerInterface.php rename src/Symfony/Component/ErrorHandler/{FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php => ErrorEnhancer/UndefinedFunctionErrorEnhancer.php} (75%) rename src/Symfony/Component/ErrorHandler/{FatalErrorHandler/UndefinedMethodFatalErrorHandler.php => ErrorEnhancer/UndefinedMethodErrorEnhancer.php} (73%) delete mode 100644 src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php rename src/Symfony/Component/ErrorHandler/Exception/{FatalThrowableError.php => ErrorException.php} (77%) delete mode 100644 src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php delete mode 100644 src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php delete mode 100644 src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedMethodErrorEnhancerTest.php delete mode 100644 src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php delete mode 100644 src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php delete mode 100644 src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php rename src/Symfony/Component/ErrorHandler/Tests/phpt/{decorate_exception_hander.phpt => decorate_exception_handler.phpt} (77%) diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 08595ce917..1b5be3bcc1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -20,7 +20,7 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; @@ -208,7 +208,7 @@ class Application extends BaseApplication foreach ($this->registrationErrors as $error) { if (!$error instanceof \Exception) { - $error = new FatalThrowableError($error); + $error = new ErrorException($error); } $this->doRenderException($error, $output); diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index e66705ab37..995aceaf19 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -44,7 +44,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Debug\ErrorHandler as LegacyErrorHandler; use Symfony\Component\Debug\Exception\FatalThrowableError as LegacyFatalThrowableError; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; use Symfony\Contracts\Service\ResetInterface; @@ -129,7 +129,7 @@ class Application implements ResetInterface $renderException = function (\Throwable $e) use ($output) { if (!$e instanceof \Exception) { - $e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : (class_exists(LegacyFatalThrowableError::class) ? new LegacyFatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine())); + $e = class_exists(ErrorException::class) ? new ErrorException($e) : (class_exists(LegacyFatalThrowableError::class) ? new LegacyFatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine())); } if ($output instanceof ConsoleOutputInterface) { $this->renderException($e, $output->getErrorOutput()); diff --git a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php index 21e2c0db53..6c87f98c55 100644 --- a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -11,14 +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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ClassNotFoundException::class, \Symfony\Component\ErrorHandler\Error\ClassNotFoundError::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. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Error\ClassNotFoundError instead. */ class ClassNotFoundException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 23c2ede7eb..571f3975da 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -11,14 +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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorException::class, \Symfony\Component\ErrorHandler\Exception\ErrorException::class), E_USER_DEPRECATED); /** * Fatal Error Exception. * * @author Konstanton Myakshin * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalErrorException instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ErrorException instead. */ class FatalErrorException extends \ErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php index d7d36ac17e..53c410b014 100644 --- a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php @@ -11,14 +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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalThrowableError::class, \Symfony\Component\ErrorHandler\Exception\ErrorException::class), E_USER_DEPRECATED); /** * Fatal Throwable Error. * * @author Nicolas Grekas * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalThrowableError instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ErrorException instead. */ class FatalThrowableError extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php index 5b02d52ad8..a4a90ee997 100644 --- a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php +++ b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php @@ -11,14 +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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', OutOfMemoryException::class, \Symfony\Component\ErrorHandler\Error\OutOfMemoryError::class), E_USER_DEPRECATED); /** * Out of memory exception. * * @author Nicolas Grekas * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Error\OutOfMemoryError instead. */ class OutOfMemoryException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php index fefd7d248e..0a7037aee8 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -11,14 +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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionException::class, \Symfony\Component\ErrorHandler\Error\UndefinedFunctionError::class), E_USER_DEPRECATED); /** * Undefined Function Exception. * * @author Konstanton Myakshin * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Error\UndefinedFunctionError instead. */ class UndefinedFunctionException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php index 4855941586..4d31c56d37 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php @@ -11,14 +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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodException::class, \Symfony\Component\ErrorHandler\Error\UndefinedMethodError::class), E_USER_DEPRECATED); /** * Undefined Method Exception. * * @author Grégoire Pineau * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Error\UndefinedMethodError instead. */ class UndefinedMethodException extends FatalErrorException { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php index ce9f4c0bb9..67b7ddd161 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -14,14 +14,14 @@ namespace Symfony\Component\Debug\FatalErrorHandler; 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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionFatalErrorHandler::class, \Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer::class), E_USER_DEPRECATED); /** * ErrorHandler for undefined functions. * * @author Fabien Potencier * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer instead. */ class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php index 4fa3e46cd5..8ee359ab27 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php @@ -14,14 +14,14 @@ namespace Symfony\Component\Debug\FatalErrorHandler; 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); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodFatalErrorHandler::class, \Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer::class), E_USER_DEPRECATED); /** * ErrorHandler for undefined methods. * * @author Grégoire Pineau * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer instead. */ class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface { diff --git a/src/Symfony/Component/ErrorHandler/Error/ClassNotFoundError.php b/src/Symfony/Component/ErrorHandler/Error/ClassNotFoundError.php new file mode 100644 index 0000000000..443fba2c3b --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Error/ClassNotFoundError.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Error; + +class ClassNotFoundError extends \Error +{ + /** + * {@inheritdoc} + */ + public function __construct(string $message, \Throwable $previous) + { + parent::__construct($message, $previous->getCode(), $previous->getPrevious()); + + foreach ([ + 'file' => $previous->getFile(), + 'line' => $previous->getLine(), + 'trace' => $previous->getTrace(), + ] as $property => $value) { + $refl = new \ReflectionProperty(\Error::class, $property); + $refl->setAccessible(true); + $refl->setValue($this, $value); + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php b/src/Symfony/Component/ErrorHandler/Error/FatalError.php similarity index 68% rename from src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php rename to src/Symfony/Component/ErrorHandler/Error/FatalError.php index ab6268a5c0..68172d876c 100644 --- a/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php +++ b/src/Symfony/Component/ErrorHandler/Error/FatalError.php @@ -9,18 +9,22 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorHandler\Exception; +namespace Symfony\Component\ErrorHandler\Error; -/** - * Fatal Error Exception. - * - * @author Konstanton Myakshin - */ -class FatalErrorException extends \ErrorException +class FatalError extends \Error { - 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) + private $error; + + /** + * {@inheritdoc} + * + * @param array $error An array as returned by error_get_last() + */ + public function __construct(string $message, int $code, array $error, int $traceOffset = null, bool $traceArgs = true, array $trace = null) { - parent::__construct($message, $code, $severity, $filename, $lineno, $previous); + parent::__construct($message, $code); + + $this->error = $error; if (null !== $trace) { if (!$traceArgs) { @@ -28,8 +32,6 @@ class FatalErrorException extends \ErrorException unset($frame['args'], $frame['this'], $frame); } } - - $this->setTrace($trace); } elseif (null !== $traceOffset) { if (\function_exists('xdebug_get_function_stack')) { $trace = xdebug_get_function_stack(); @@ -63,15 +65,24 @@ class FatalErrorException extends \ErrorException } else { $trace = []; } + } - $this->setTrace($trace); + foreach ([ + 'file' => $error['file'], + 'line' => $error['line'], + 'trace' => $trace, + ] as $property => $value) { + $refl = new \ReflectionProperty(\Error::class, $property); + $refl->setAccessible(true); + $refl->setValue($this, $value); } } - protected function setTrace(array $trace): void + /** + * {@inheritdoc} + */ + public function getError(): array { - $traceReflector = new \ReflectionProperty('Exception', 'trace'); - $traceReflector->setAccessible(true); - $traceReflector->setValue($this, $trace); + return $this->error; } } diff --git a/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php b/src/Symfony/Component/ErrorHandler/Error/OutOfMemoryError.php similarity index 56% rename from src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php rename to src/Symfony/Component/ErrorHandler/Error/OutOfMemoryError.php index 18c367596f..d685c3d369 100644 --- a/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php +++ b/src/Symfony/Component/ErrorHandler/Error/OutOfMemoryError.php @@ -9,13 +9,8 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorHandler\Exception; +namespace Symfony\Component\ErrorHandler\Error; -/** - * Out of memory exception. - * - * @author Nicolas Grekas - */ -class OutOfMemoryException extends FatalErrorException +class OutOfMemoryError extends FatalError { } diff --git a/src/Symfony/Component/ErrorHandler/Error/UndefinedFunctionError.php b/src/Symfony/Component/ErrorHandler/Error/UndefinedFunctionError.php new file mode 100644 index 0000000000..b57dd1579d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Error/UndefinedFunctionError.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Error; + +class UndefinedFunctionError extends \Error +{ + /** + * {@inheritdoc} + */ + public function __construct(string $message, \Throwable $previous) + { + parent::__construct($message, $previous->getCode(), $previous->getPrevious()); + + foreach ([ + 'file' => $previous->getFile(), + 'line' => $previous->getLine(), + 'trace' => $previous->getTrace(), + ] as $property => $value) { + $refl = new \ReflectionProperty(\Error::class, $property); + $refl->setAccessible(true); + $refl->setValue($this, $value); + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/Error/UndefinedMethodError.php b/src/Symfony/Component/ErrorHandler/Error/UndefinedMethodError.php new file mode 100644 index 0000000000..adc8731f36 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Error/UndefinedMethodError.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Error; + +class UndefinedMethodError extends \Error +{ + /** + * {@inheritdoc} + */ + public function __construct(string $message, \Throwable $previous) + { + parent::__construct($message, $previous->getCode(), $previous->getPrevious()); + + foreach ([ + 'file' => $previous->getFile(), + 'line' => $previous->getLine(), + 'trace' => $previous->getTrace(), + ] as $property => $value) { + $refl = new \ReflectionProperty(\Error::class, $property); + $refl->setAccessible(true); + $refl->setValue($this, $value); + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/ClassNotFoundErrorEnhancer.php similarity index 87% rename from src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php rename to src/Symfony/Component/ErrorHandler/ErrorEnhancer/ClassNotFoundErrorEnhancer.php index ee9930c097..38111078bc 100644 --- a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/ClassNotFoundErrorEnhancer.php @@ -9,45 +9,45 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorHandler\FatalErrorHandler; +namespace Symfony\Component\ErrorHandler\ErrorEnhancer; 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; +use Symfony\Component\ErrorHandler\Error\ClassNotFoundError; +use Symfony\Component\ErrorHandler\Error\FatalError; /** - * ErrorHandler for classes that do not exist. - * * @author Fabien Potencier */ -class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface +class ClassNotFoundErrorEnhancer implements ErrorEnhancerInterface { /** * {@inheritdoc} */ - public function handleError(array $error, FatalErrorException $exception) + public function enhance(\Throwable $error): ?\Throwable { - $messageLen = \strlen($error['message']); + // Some specific versions of PHP produce a fatal error when extending a not found class. + $message = !$error instanceof FatalError ? $error->getMessage() : $error->getError()['message']; + $messageLen = \strlen($message); $notFoundSuffix = '\' not found'; $notFoundSuffixLen = \strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { return null; } - if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + if (0 !== substr_compare($message, $notFoundSuffix, -$notFoundSuffixLen)) { return null; } foreach (['class', 'interface', 'trait'] as $typeName) { $prefix = ucfirst($typeName).' \''; $prefixLen = \strlen($prefix); - if (0 !== strpos($error['message'], $prefix)) { + if (0 !== strpos($message, $prefix)) { continue; } - $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + $fullyQualifiedClassName = substr($message, $prefixLen, -$notFoundSuffixLen); if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); @@ -69,8 +69,10 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface } $message .= "\nDid you forget a \"use\" statement".$tail; - return new ClassNotFoundException($message, $exception); + return new ClassNotFoundError($message, $error); } + + return null; } /** @@ -81,7 +83,7 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface * * @param string $class A class name (without its namespace) * - * @return array An array of possible fully qualified class names + * Returns an array of possible fully qualified class names */ private function getClassCandidates(string $class): array { diff --git a/src/Symfony/Component/ErrorHandler/ErrorEnhancer/ErrorEnhancerInterface.php b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/ErrorEnhancerInterface.php new file mode 100644 index 0000000000..7c3f4ef940 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/ErrorEnhancerInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorEnhancer; + +interface ErrorEnhancerInterface +{ + /** + * Returns an \Throwable instance if the class is able to improve the error, null otherwise. + */ + public function enhance(\Throwable $error): ?\Throwable; +} diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php similarity index 75% rename from src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php rename to src/Symfony/Component/ErrorHandler/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php index b944b8e11c..f4c49c2856 100644 --- a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php @@ -9,41 +9,44 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorHandler\FatalErrorHandler; +namespace Symfony\Component\ErrorHandler\ErrorEnhancer; -use Symfony\Component\ErrorHandler\Exception\FatalErrorException; -use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException; +use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\ErrorHandler\Error\UndefinedFunctionError; /** - * ErrorHandler for undefined functions. - * * @author Fabien Potencier */ -class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface +class UndefinedFunctionErrorEnhancer implements ErrorEnhancerInterface { /** * {@inheritdoc} */ - public function handleError(array $error, FatalErrorException $exception) + public function enhance(\Throwable $error): ?\Throwable { - $messageLen = \strlen($error['message']); + if ($error instanceof FatalError) { + return null; + } + + $message = $error->getMessage(); + $messageLen = \strlen($message); $notFoundSuffix = '()'; $notFoundSuffixLen = \strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { return null; } - if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + if (0 !== substr_compare($message, $notFoundSuffix, -$notFoundSuffixLen)) { return null; } $prefix = 'Call to undefined function '; $prefixLen = \strlen($prefix); - if (0 !== strpos($error['message'], $prefix)) { + if (0 !== strpos($message, $prefix)) { return null; } - $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + $fullyQualifiedFunctionName = substr($message, $prefixLen, -$notFoundSuffixLen); if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); @@ -79,6 +82,6 @@ class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface $message .= "\nDid you mean to call ".$candidates; } - return new UndefinedFunctionException($message, $exception); + return new UndefinedFunctionError($message, $error); } } diff --git a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/UndefinedMethodErrorEnhancer.php similarity index 73% rename from src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php rename to src/Symfony/Component/ErrorHandler/ErrorEnhancer/UndefinedMethodErrorEnhancer.php index 90bb09837e..ad0e4b3eef 100644 --- a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorEnhancer/UndefinedMethodErrorEnhancer.php @@ -9,24 +9,27 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorHandler\FatalErrorHandler; +namespace Symfony\Component\ErrorHandler\ErrorEnhancer; -use Symfony\Component\ErrorHandler\Exception\FatalErrorException; -use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException; +use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\ErrorHandler\Error\UndefinedMethodError; /** - * ErrorHandler for undefined methods. - * * @author Grégoire Pineau */ -class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface +class UndefinedMethodErrorEnhancer implements ErrorEnhancerInterface { /** * {@inheritdoc} */ - public function handleError(array $error, FatalErrorException $exception) + public function enhance(\Throwable $error): ?\Throwable { - preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches); + if ($error instanceof FatalError) { + return null; + } + + $message = $error->getMessage(); + preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $message, $matches); if (!$matches) { return null; } @@ -38,7 +41,7 @@ class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface 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); + return new UndefinedMethodError($message, $error); } $candidates = []; @@ -61,6 +64,6 @@ class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface $message .= "\nDid you mean to call ".$candidates; } - return new UndefinedMethodException($message, $exception); + return new UndefinedMethodError($message, $error); } } diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index 37047911f7..5bb43df04d 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -13,15 +13,13 @@ namespace Symfony\Component\ErrorHandler; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; -use Symfony\Component\ErrorHandler\Exception\FatalErrorException; -use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; +use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\ErrorHandler\Error\OutOfMemoryError; +use Symfony\Component\ErrorHandler\ErrorEnhancer\ClassNotFoundErrorEnhancer; +use Symfony\Component\ErrorHandler\ErrorEnhancer\ErrorEnhancerInterface; +use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer; +use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer; 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\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; /** * A generic ErrorHandler for the PHP engine. @@ -538,64 +536,50 @@ class ErrorHandler /** * Handles an exception by logging then forwarding it to another handler. * - * @param array $error An array as returned by error_get_last() - * * @internal */ - public function handleException(\Throwable $exception, array $error = null) + public function handleException(\Throwable $exception) { - if (null === $error) { - self::$exitCode = 255; - } - - $type = ThrowableUtils::getSeverity($exception); $handlerException = null; - if (($this->loggedErrors & $type) || $exception instanceof \Error) { + if (!$exception instanceof FatalError) { + self::$exitCode = 255; + + $type = ThrowableUtils::getSeverity($exception); + } else { + $type = $exception->getError()['type']; + } + + if ($this->loggedErrors & $type) { if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) { $message = $this->parseAnonymousClass($message); } - if ($exception instanceof FatalErrorException) { + if ($exception instanceof FatalError) { $message = 'Fatal '.$message; + } elseif ($exception instanceof \Error) { + $message = 'Uncaught Error: '.$message; } elseif ($exception instanceof \ErrorException) { $message = 'Uncaught '.$message; - } elseif ($exception instanceof \Error) { - $error = [ - 'type' => $type, - 'message' => $message, - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - ]; - $message = 'Uncaught Error: '.$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) { } } - // temporary until fatal error handlers rework - $originalException = $exception; - if (!$exception instanceof \Exception) { - $exception = new FatalErrorException($exception->getMessage(), $exception->getCode(), $type, $exception->getFile(), $exception->getLine(), null, true, $exception->getTrace()); - } - - if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { - foreach ($this->getFatalErrorHandlers() as $handler) { - if ($e = $handler->handleError($error, $exception)) { - $convertedException = $e; + if (!$exception instanceof OutOfMemoryError) { + foreach ($this->getErrorEnhancers() as $errorEnhancer) { + if ($e = $errorEnhancer->enhance($exception)) { + $exception = $e; break; } } } - $exception = $convertedException ?? $originalException; $exceptionHandler = $this->exceptionHandler; if ((!\is_array($exceptionHandler) || !$exceptionHandler[0] instanceof self || 'sendPhpResponse' !== $exceptionHandler[1]) && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { $this->exceptionHandler = [$this, 'sendPhpResponse']; @@ -613,6 +597,7 @@ class ErrorHandler self::$reservedMemory = null; // Disable the fatal error handler throw $exception; // Give back $exception to the native handler } + $this->handleException($handlerException); } @@ -673,20 +658,20 @@ class ErrorHandler $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); + $fatalError = new OutOfMemoryError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, false, $trace); } else { - $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); + $fatalError = new FatalError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, true, $trace); } } else { - $exception = null; + $fatalError = null; } try { - if (null !== $exception) { + if (null !== $fatalError) { self::$exitCode = 255; - $handler->handleException($exception, $error); + $handler->handleException($fatalError); } - } catch (FatalErrorException $e) { + } catch (FatalError $e) { // Ignore this re-throw } @@ -730,18 +715,16 @@ class ErrorHandler } /** - * Gets the fatal error handlers. + * Override this method if you want to define more error enhancers. * - * Override this method if you want to define more fatal error handlers. - * - * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface + * @return ErrorEnhancerInterface[] */ - protected function getFatalErrorHandlers(): array + protected function getErrorEnhancers(): iterable { return [ - new UndefinedFunctionFatalErrorHandler(), - new UndefinedMethodFatalErrorHandler(), - new ClassNotFoundFatalErrorHandler(), + new UndefinedFunctionErrorEnhancer(), + new UndefinedMethodErrorEnhancer(), + new ClassNotFoundErrorEnhancer(), ]; } diff --git a/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php b/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php deleted file mode 100644 index b0638826d6..0000000000 --- a/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * 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/FatalThrowableError.php b/src/Symfony/Component/ErrorHandler/Exception/ErrorException.php similarity index 77% rename from src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php rename to src/Symfony/Component/ErrorHandler/Exception/ErrorException.php index 460d3e1ae8..759d3fdc47 100644 --- a/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/ErrorHandler/Exception/ErrorException.php @@ -13,12 +13,7 @@ namespace Symfony\Component\ErrorHandler\Exception; use Symfony\Component\ErrorHandler\ThrowableUtils; -/** - * Fatal Throwable Error. - * - * @author Nicolas Grekas - */ -class FatalThrowableError extends FatalErrorException +class ErrorException extends \ErrorException { private $originalClassName; @@ -26,7 +21,7 @@ class FatalThrowableError extends FatalErrorException { $this->originalClassName = \get_class($e); - \ErrorException::__construct( + parent::__construct( $e->getMessage(), $e->getCode(), ThrowableUtils::getSeverity($e), @@ -35,7 +30,9 @@ class FatalThrowableError extends FatalErrorException $e->getPrevious() ); - $this->setTrace($e->getTrace()); + $refl = new \ReflectionProperty(\Exception::class, 'trace'); + $refl->setAccessible(true); + $refl->setValue($this, $e->getTrace()); } public function getOriginalClassName(): string diff --git a/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php b/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php deleted file mode 100644 index bb2f46564d..0000000000 --- a/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * 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 deleted file mode 100644 index 12efdc716c..0000000000 --- a/src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * 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/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php deleted file mode 100644 index daf5dcd1fd..0000000000 --- a/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * 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() - * - * @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/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php new file mode 100644 index 0000000000..3098200b01 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorEnhancer; + +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\DebugClassLoader; +use Symfony\Component\ErrorHandler\Error\ClassNotFoundError; +use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\ErrorHandler\ErrorEnhancer\ClassNotFoundErrorEnhancer; + +class ClassNotFoundErrorEnhancerTest extends TestCase +{ + public static function setUpBeforeClass(): void + { + 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(__DIR__, 5)); + break; + } + } + } + + /** + * @dataProvider provideClassNotFoundData + */ + public function testEnhance(string $originalMessage, string $enhancedMessage, $autoloader = null) + { + try { + 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); + } + + $expectedLine = __LINE__ + 1; + $error = (new ClassNotFoundErrorEnhancer())->enhance(new \Error($originalMessage)); + } finally { + if ($autoloader) { + spl_autoload_unregister($autoloader); + array_map('spl_autoload_register', $autoloaders); + } + } + + $this->assertInstanceOf(ClassNotFoundError::class, $error); + $this->assertSame($enhancedMessage, $error->getMessage()); + $this->assertSame(realpath(__FILE__), $error->getFile()); + $this->assertSame($expectedLine, $error->getLine()); + } + + public function provideClassNotFoundData() + { + $autoloader = new ComposerClassLoader(); + $autoloader->add('Symfony\Component\ErrorHandler\Error\\', realpath(__DIR__.'/../../Error')); + $autoloader->add('Symfony_Component_ErrorHandler_Tests_Fixtures', realpath(__DIR__.'/../../Tests/Fixtures')); + + $debugClassLoader = new DebugClassLoader([$autoloader, 'loadClass']); + + return [ + [ + 'Class \'WhizBangFactory\' not found', + "Attempted to load class \"WhizBangFactory\" from the global namespace.\nDid you forget a \"use\" statement?", + ], + [ + 'Class \'Foo\\Bar\\WhizBangFactory\' not found', + "Attempted to load class \"WhizBangFactory\" from namespace \"Foo\\Bar\".\nDid you forget a \"use\" statement for another namespace?", + ], + [ + 'Class \'UndefinedFunctionError\' not found', + "Attempted to load class \"UndefinedFunctionError\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Error\UndefinedFunctionError\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + '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'], + ], + [ + 'Class \'Foo\\Bar\\UndefinedFunctionError\' not found', + "Attempted to load class \"UndefinedFunctionError\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Error\UndefinedFunctionError\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + 'Class \'Foo\\Bar\\UndefinedFunctionError\' not found', + "Attempted to load class \"UndefinedFunctionError\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Error\UndefinedFunctionError\"?", + [$autoloader, 'loadClass'], + ], + [ + 'Class \'Foo\\Bar\\UndefinedFunctionError\' not found', + "Attempted to load class \"UndefinedFunctionError\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Error\UndefinedFunctionError\"?", + [$debugClassLoader, 'loadClass'], + ], + [ + 'Class \'Foo\\Bar\\UndefinedFunctionError\' not found', + "Attempted to load class \"UndefinedFunctionError\" from namespace \"Foo\\Bar\".\nDid you forget a \"use\" statement for another namespace?", + function ($className) { /* do nothing here */ }, + ], + ]; + } + + public function testEnhanceWithFatalError() + { + $error = (new ClassNotFoundErrorEnhancer())->enhance(new FatalError('foo', 0, [ + 'type' => E_ERROR, + 'message' => "Class 'FooBarCcc' not found", + 'file' => $expectedFile = realpath(__FILE__), + 'line' => $expectedLine = __LINE__, + ])); + + $this->assertInstanceOf(ClassNotFoundError::class, $error); + $this->assertSame("Attempted to load class \"FooBarCcc\" from the global namespace.\nDid you forget a \"use\" statement?", $error->getMessage()); + $this->assertSame($expectedFile, $error->getFile()); + $this->assertSame($expectedLine, $error->getLine()); + } + + 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'; + + $enhancer = new ClassNotFoundErrorEnhancer(); + $error = $enhancer->enhance(new \Error("Class 'Foo\\Bar\\RequiredTwice' not found")); + + $this->assertInstanceOf(ClassNotFoundError::class, $error); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php new file mode 100644 index 0000000000..fe7d5371a1 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorEnhancer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Error\UndefinedFunctionError; +use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer; + +class UndefinedFunctionErrorEnhancerTest extends TestCase +{ + /** + * @dataProvider provideUndefinedFunctionData + */ + public function testEnhance(string $originalMessage, string $enhancedMessage) + { + $enhancer = new UndefinedFunctionErrorEnhancer(); + + $expectedLine = __LINE__ + 1; + $error = $enhancer->enhance(new \Error($originalMessage)); + + $this->assertInstanceOf(UndefinedFunctionError::class, $error); + // class names are case insensitive and PHP do not return the same + $this->assertSame(strtolower($enhancedMessage), strtolower($error->getMessage())); + $this->assertSame(realpath(__FILE__), $error->getFile()); + $this->assertSame($expectedLine, $error->getLine()); + } + + public function provideUndefinedFunctionData() + { + return [ + [ + '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\\errorenhancer\\test_namespaced_function\"?", + ], + [ + '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\\errorenhancer\\test_namespaced_function\"?", + ], + [ + 'Call to undefined function foo()', + 'Attempted to call function "foo" from the global namespace.', + ], + [ + '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/ErrorEnhancer/UndefinedMethodErrorEnhancerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedMethodErrorEnhancerTest.php new file mode 100644 index 0000000000..d6ac6c029c --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/UndefinedMethodErrorEnhancerTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorEnhancer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Error\UndefinedMethodError; +use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer; + +class UndefinedMethodErrorEnhancerTest extends TestCase +{ + /** + * @dataProvider provideUndefinedMethodData + */ + public function testEnhance(string $originalMessage, string $enhancedMessage) + { + $enhancer = new UndefinedMethodErrorEnhancer(); + + $expectedLine = __LINE__ + 1; + $error = $enhancer->enhance(new \Error($originalMessage)); + + $this->assertInstanceOf(UndefinedMethodError::class, $error); + $this->assertSame($enhancedMessage, $error->getMessage()); + $this->assertSame(realpath(__FILE__), $error->getFile()); + $this->assertSame($expectedLine, $error->getLine()); + } + + public function provideUndefinedMethodData() + { + return [ + [ + 'Call to undefined method SplObjectStorage::what()', + 'Attempted to call an undefined method named "what" of class "SplObjectStorage".', + ], + [ + 'Call to undefined method SplObjectStorage::walid()', + "Attempted to call an undefined method named \"walid\" of class \"SplObjectStorage\".\nDid you mean to call \"valid\"?", + ], + [ + '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\"?", + ], + [ + 'Call to undefined method class@anonymous::test()', + 'Attempted to call an undefined method named "test" of class "class@anonymous".', + ], + ]; + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index bb694cfc3b..1c170732e8 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; use Psr\Log\NullLogger; use Symfony\Component\ErrorHandler\BufferingLogger; +use Symfony\Component\ErrorHandler\Error\ClassNotFoundError; +use Symfony\Component\ErrorHandler\Error\FatalError; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\ErrorHandler\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne; @@ -518,7 +520,7 @@ class ErrorHandlerTest extends TestCase $logArgCheck = function ($level, $message, $context) { $this->assertEquals('Fatal Parse Error: foo', $message); $this->assertArrayHasKey('exception', $context); - $this->assertInstanceOf(\Exception::class, $context['exception']); + $this->assertInstanceOf(FatalError::class, $context['exception']); }; $logger @@ -552,7 +554,7 @@ class ErrorHandlerTest extends TestCase $handler->handleException($exception); - $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $args[0]); + $this->assertInstanceOf(ClassNotFoundError::class, $args[0]); $this->assertStringStartsWith("Attempted to load class \"IReallyReallyDoNotExistAnywhereInTheRepositoryISwear\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage()); } diff --git a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php deleted file mode 100644 index 0661d742c0..0000000000 --- a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ /dev/null @@ -1,180 +0,0 @@ - - * - * 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(): void - { - 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(__DIR__, 5)); - break; - } - } - } - - /** - * @dataProvider provideClassNotFoundData - */ - public function testHandleClassNotFound(array $error, string $translatedMessage, callable $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(): array - { - $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 deleted file mode 100644 index fa8dd42268..0000000000 --- a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * 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(array $error, string $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(): array - { - 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 deleted file mode 100644 index 17414e1eb9..0000000000 --- a/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * 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(array $error, string $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(): array - { - 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/phpt/decorate_exception_hander.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_handler.phpt similarity index 77% rename from src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_hander.phpt rename to src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_handler.phpt index 034d5a5292..5fb5981d2d 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_hander.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/decorate_exception_handler.phpt @@ -23,14 +23,12 @@ if (true) { { } } - -?> --EXPECTF-- -object(Symfony\Component\ErrorHandler\Exception\ClassNotFoundException)#%d (8) { +object(Symfony\Component\ErrorHandler\Error\ClassNotFoundError)#%d (7) { ["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":"Error":private]=> string(0) "" ["code":protected]=> int(0) @@ -38,10 +36,8 @@ Did you forget a "use" statement for another namespace?" string(%d) "%s" ["line":protected]=> int(%d) - ["trace":"Exception":private]=> + ["trace":"Error":private]=> array(%d) {%A} - ["previous":"Exception":private]=> + ["previous":"Error":private]=> NULL - ["severity":protected]=> - int(1) } 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 index 532fe92241..15933828bd 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt @@ -35,7 +35,18 @@ array(1) { [0]=> string(37) "Error and exception handlers do match" } -object(Symfony\Component\ErrorHandler\Exception\FatalErrorException)#%d (%d) { +object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { + ["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=> + array(4) { + ["type"]=> + int(1) + ["message"]=> + string(179) "Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(%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/ErrorRenderer/Exception/FlattenException.php b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php index 56a5221ed8..7c053a328d 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\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; @@ -64,7 +64,7 @@ class FlattenException $e->setStatusCode($statusCode); $e->setHeaders($headers); $e->setTraceFromThrowable($exception); - $e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception)); + $e->setClass($exception instanceof ErrorException ? $exception->getOriginalClassName() : \get_class($exception)); $e->setFile($exception->getFile()); $e->setLine($exception->getLine()); diff --git a/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php index 7651e57f11..dc2678be81 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\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -132,7 +132,7 @@ class FlattenExceptionTest extends TestCase public function testWrappedThrowable() { - $exception = new FatalThrowableError(new \DivisionByZeroError('Ouch', 42)); + $exception = new ErrorException(new \DivisionByZeroError('Ouch', 42)); $flattened = FlattenException::createFromThrowable($exception); $this->assertSame('Ouch', $flattened->getMessage(), 'The message is copied from the original error.'); diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php index 03885a46b8..edff2d45d0 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -16,7 +16,7 @@ use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; @@ -113,7 +113,7 @@ class DebugHandlersListener implements EventSubscriberInterface } if (!$e instanceof \Exception) { - $e = new FatalThrowableError($e); + $e = new ErrorException($e); } $hasRun = true; @@ -127,7 +127,7 @@ class DebugHandlersListener implements EventSubscriberInterface } $this->exceptionHandler = static function (\Throwable $e) use ($app, $output) { if (!$e instanceof \Exception) { - $e = new FatalThrowableError($e); + $e = new ErrorException($e); } $app->renderException($e, $output); From 015eb6625c47886eacb38b766feeb6e61572f0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ba=CC=81lint=20Szekeres?= Date: Tue, 8 Oct 2019 14:15:27 +0200 Subject: [PATCH 2/7] added image/svg MIME support --- src/Symfony/Component/Mime/MimeTypes.php | 3 ++- src/Symfony/Component/Mime/Tests/MimeTypesTest.php | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Mime/MimeTypes.php b/src/Symfony/Component/Mime/MimeTypes.php index 6b5ad49e18..268658d158 100644 --- a/src/Symfony/Component/Mime/MimeTypes.php +++ b/src/Symfony/Component/Mime/MimeTypes.php @@ -1251,6 +1251,7 @@ final class MimeTypes implements MimeTypesInterface 'image/psd' => ['psd'], 'image/rle' => ['rle'], 'image/sgi' => ['sgi'], + 'image/svg' => ['svg'], 'image/svg+xml' => ['svg', 'svgz'], 'image/svg+xml-compressed' => ['svgz'], 'image/tiff' => ['tiff', 'tif'], @@ -2808,7 +2809,7 @@ final class MimeTypes implements MimeTypesInterface 'sv4crc' => ['application/x-sv4crc'], 'svc' => ['application/vnd.dvb.service'], 'svd' => ['application/vnd.svd'], - 'svg' => ['image/svg+xml'], + 'svg' => ['image/svg+xml', 'image/svg'], 'svgz' => ['image/svg+xml', 'image/svg+xml-compressed'], 'svh' => ['text/x-svhdr'], 'swa' => ['application/x-director'], diff --git a/src/Symfony/Component/Mime/Tests/MimeTypesTest.php b/src/Symfony/Component/Mime/Tests/MimeTypesTest.php index c5ff262b80..a736dbebba 100644 --- a/src/Symfony/Component/Mime/Tests/MimeTypesTest.php +++ b/src/Symfony/Component/Mime/Tests/MimeTypesTest.php @@ -47,6 +47,8 @@ class MimeTypesTest extends AbstractMimeTypeGuesserTest $mt = new MimeTypes(); $this->assertSame(['mbox'], $mt->getExtensions('application/mbox')); $this->assertSame(['ai', 'eps', 'ps'], $mt->getExtensions('application/postscript')); + $this->assertContains('svg', $mt->getExtensions('image/svg+xml')); + $this->assertContains('svg', $mt->getExtensions('image/svg')); $this->assertSame([], $mt->getExtensions('application/whatever-symfony')); } @@ -56,6 +58,8 @@ class MimeTypesTest extends AbstractMimeTypeGuesserTest $this->assertSame(['application/mbox'], $mt->getMimeTypes('mbox')); $this->assertContains('application/postscript', $mt->getMimeTypes('ai')); $this->assertContains('application/postscript', $mt->getMimeTypes('ps')); + $this->assertContains('image/svg+xml', $mt->getMimeTypes('svg')); + $this->assertContains('image/svg', $mt->getMimeTypes('svg')); $this->assertSame([], $mt->getMimeTypes('symfony')); } } From aff4e56fc5458ee13a2baffc422597552e916c4a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 9 Oct 2019 18:19:03 +0200 Subject: [PATCH 3/7] [HttpClient] send `Accept: */*` by default, fix removing it when needed --- .../Component/HttpClient/CurlHttpClient.php | 4 +-- .../Component/HttpClient/HttpClientTrait.php | 2 +- .../HttpClient/Tests/HttpClientTestCase.php | 28 +++++++++++++++++++ .../HttpClient/Tests/HttpClientTraitTest.php | 2 +- .../HttpClient/Tests/MockHttpClientTest.php | 7 +++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 83413a23ed..471f694b4e 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -220,7 +220,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface // Prevent curl from sending its default Accept and Expect headers foreach (['accept', 'expect'] as $header) { - if (!isset($options['normalized_headers'][$header])) { + if (!isset($options['normalized_headers'][$header][0])) { $curlopts[CURLOPT_HTTPHEADER][] = $header.':'; } } @@ -413,7 +413,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface return 0 !== stripos($h, 'Host:'); }); - if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) { + if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) { $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) { return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); }); diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 3fd9814df6..ab5f3502f7 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -57,7 +57,7 @@ trait HttpClientTrait } if (!isset($options['normalized_headers']['accept'])) { - $options['normalized_headers']['accept'] = [$options['headers'][] = 'Accept: *']; + $options['normalized_headers']['accept'] = [$options['headers'][] = 'Accept: */*']; } if (isset($options['body'])) { diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index 3d5a70a0a9..bc836a7fad 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -19,4 +19,32 @@ abstract class HttpClientTestCase extends BaseHttpClientTestCase { $this->markTestSkipped('Implemented as of version 4.4'); } + + public function testAcceptHeader() + { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('GET', 'http://localhost:8057'); + $requestHeaders = $response->toArray(); + + $this->assertSame('*/*', $requestHeaders['HTTP_ACCEPT']); + + $response = $client->request('GET', 'http://localhost:8057', [ + 'headers' => [ + 'Accept' => 'foo/bar', + ], + ]); + $requestHeaders = $response->toArray(); + + $this->assertSame('foo/bar', $requestHeaders['HTTP_ACCEPT']); + + $response = $client->request('GET', 'http://localhost:8057', [ + 'headers' => [ + 'Accept' => null, + ], + ]); + $requestHeaders = $response->toArray(); + + $this->assertArrayNotHasKey('HTTP_ACCEPT', $requestHeaders); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php index 559f2b6227..e1636a5d5f 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php @@ -172,7 +172,7 @@ class HttpClientTraitTest extends TestCase public function testAuthBearerOption() { [, $options] = self::prepareRequest('POST', 'http://example.com', ['auth_bearer' => 'foobar'], HttpClientInterface::OPTIONS_DEFAULTS); - $this->assertSame(['Accept: *', 'Authorization: Bearer foobar'], $options['headers']); + $this->assertSame(['Accept: */*', 'Authorization: Bearer foobar'], $options['headers']); $this->assertSame(['Authorization: Bearer foobar'], $options['normalized_headers']['authorization']); } diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php index 8a3936b442..32b4ebe448 100644 --- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -36,6 +36,7 @@ class MockHttpClientTest extends HttpClientTestCase "SERVER_NAME": "127.0.0.1", "REQUEST_URI": "/", "REQUEST_METHOD": "GET", + "HTTP_ACCEPT": "*/*", "HTTP_FOO": "baR", "HTTP_HOST": "localhost:8057" }'; @@ -113,6 +114,12 @@ class MockHttpClientTest extends HttpClientTestCase $responses[] = $mock; break; + case 'testAcceptHeader': + $responses[] = new MockResponse($body, ['response_headers' => $headers]); + $responses[] = new MockResponse(str_replace('*/*', 'foo/bar', $body), ['response_headers' => $headers]); + $responses[] = new MockResponse(str_replace('"HTTP_ACCEPT": "*/*",', '', $body), ['response_headers' => $headers]); + break; + case 'testResolve': $responses[] = new MockResponse($body, ['response_headers' => $headers]); $responses[] = new MockResponse($body, ['response_headers' => $headers]); From a1f334c1b7160f9aac070abef1ead8910f2946ba Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 9 Oct 2019 19:31:25 +0200 Subject: [PATCH 4/7] [Cache] ignore unserialization failures in AbstractTagAwareAdapter::doDelete() --- .../Cache/Adapter/AbstractTagAwareAdapter.php | 10 +++++++--- .../Component/Cache/Adapter/RedisTagAwareAdapter.php | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php index 5e535e51fc..13a4968ac5 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php @@ -229,10 +229,14 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA unset($this->deferred[$key]); } - foreach ($this->doFetch($ids) as $id => $value) { - foreach ($value['tags'] ?? [] as $tag) { - $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id; + try { + foreach ($this->doFetch($ids) as $id => $value) { + foreach ($value['tags'] ?? [] as $tag) { + $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id; + } } + } catch (\Exception $e) { + // ignore unserialization failures } try { diff --git a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php index 3bc5d84b62..b83b09b013 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php @@ -97,7 +97,7 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter } // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op - $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData) { + $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) { // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one foreach ($serialized as $id => $value) { yield 'setEx' => [ @@ -109,11 +109,15 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter // Add and Remove Tags foreach ($addTagData as $tagId => $ids) { - yield 'sAdd' => array_merge([$tagId], $ids); + if (!$failed || $ids = array_diff($ids, $failed)) { + yield 'sAdd' => array_merge([$tagId], $ids); + } } foreach ($delTagData as $tagId => $ids) { - yield 'sRem' => array_merge([$tagId], $ids); + if (!$failed || $ids = array_diff($ids, $failed)) { + yield 'sRem' => array_merge([$tagId], $ids); + } } }); From 928363c408c036cd385e402f9a816da73db05f3e Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Wed, 9 Oct 2019 18:13:48 -0400 Subject: [PATCH 5/7] Keeping backward compatibility with legacy FlattenException usage --- UPGRADE-4.4.md | 27 +++++++++++++++++++ UPGRADE-5.0.md | 27 +++++++++++++++++++ .../Controller/ExceptionController.php | 2 +- .../Controller/PreviewErrorController.php | 2 +- .../Controller/ExceptionControllerTest.php | 2 +- .../Controller/PreviewErrorControllerTest.php | 2 +- .../Exception/FlattenException.php | 22 +++++---------- .../stubs/Exception/FlattenException.php | 25 +++++++++++++++++ .../Component/ErrorRenderer/composer.json | 1 + src/Symfony/Component/HttpKernel/CHANGELOG.md | 2 ++ .../HttpKernel/Controller/ErrorController.php | 10 +++---- .../EventListener/ExceptionListener.php | 15 ++++++++++- .../Tests/Controller/ErrorControllerTest.php | 2 +- 13 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/stubs/Exception/FlattenException.php diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index db46529072..dfbad35a97 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -155,6 +155,33 @@ HttpKernel current directory or with a glob pattern. The fallback directories have never been advocated so you likely do not use those in any app based on the SF Standard or Flex edition. * Getting the container from a non-booted kernel is deprecated + * Deprecated passing the `exception` attribute (instance of `Symfony\Component\Debug\Exception\FlattenException`) + to the configured controller of the `ExceptionListener`, use the `e` attribute + (instance of `Symfony\Component\ErrorRenderer\Exception\FlattenException`) instead + + before: + ```php + use Symfony\Component\Debug\Exception\FlattenException; + + class ExceptionController + { + public function __invoke(FlattenException $exception) + { + } + } + ``` + + after: + ```php + use Symfony\Component\ErrorRenderer\Exception\FlattenException; + + class ExceptionController + { + public function __invoke(FlattenException $e) + { + } + } + ``` Lock ---- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index a9c1e711e4..f7fbd19e82 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -341,6 +341,33 @@ HttpKernel * Removed the second and third argument of `FileLocator::__construct` * Removed loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as fallback directories. + * Removed passing the `exception` attribute (instance of `Symfony\Component\Debug\Exception\FlattenException`) + to the configured controller of the `ExceptionListener`, use the `e` attribute + (instance of `Symfony\Component\ErrorRenderer\Exception\FlattenException`) instead + + before: + ```php + use Symfony\Component\Debug\Exception\FlattenException; + + class ExceptionController + { + public function __invoke(FlattenException $exception) + { + } + } + ``` + + after: + ```php + use Symfony\Component\ErrorRenderer\Exception\FlattenException; + + class ExceptionController + { + public function __invoke(FlattenException $e) + { + } + } + ``` Intl ---- diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index a2458f1818..c679d9a772 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php index 7e82c3e68a..970e7f031c 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php index 4e48df0aeb..8665411796 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php @@ -13,7 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\Controller; use Symfony\Bundle\TwigBundle\Controller\ExceptionController; use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Twig\Environment; use Twig\Loader\ArrayLoader; diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php index 0178276ee5..63750cd2ab 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php @@ -13,7 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\Controller; use Symfony\Bundle\TwigBundle\Controller\PreviewErrorController; use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; diff --git a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php index 56a5221ed8..eb544d935f 100644 --- a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php +++ b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php @@ -22,6 +22,8 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; * Basically, this class removes all objects from the trace. * * @author Fabien Potencier + * + * @internal */ class FlattenException { @@ -37,6 +39,11 @@ class FlattenException private $file; private $line; + public static function create(\Exception $exception, int $statusCode = null, array $headers = []): self + { + return static::createFromThrowable($exception, $statusCode, $headers); + } + public static function createFromThrowable(\Throwable $exception, int $statusCode = null, array $headers = []): self { $e = new static(); @@ -374,18 +381,3 @@ class FlattenException return rtrim($message); } } - -namespace Symfony\Component\Debug\Exception; - -if (!class_exists(FlattenException::class, false)) { - class_alias(\Symfony\Component\ErrorRenderer\Exception\FlattenException::class, FlattenException::class); -} - -if (false) { - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorRenderer\Exception\FlattenException instead. - */ - class FlattenException extends \Symfony\Component\ErrorRenderer\Exception\FlattenException - { - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Resources/stubs/Exception/FlattenException.php b/src/Symfony/Component/ErrorRenderer/Resources/stubs/Exception/FlattenException.php new file mode 100644 index 0000000000..f17f5711a3 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/stubs/Exception/FlattenException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Exception; + +if (!class_exists(FlattenException::class, false)) { + class_alias(\Symfony\Component\ErrorRenderer\Exception\FlattenException::class, FlattenException::class); +} + +if (false) { + /** + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorRenderer\Exception\FlattenException instead. + */ + class FlattenException extends \Symfony\Component\ErrorRenderer\Exception\FlattenException + { + } +} diff --git a/src/Symfony/Component/ErrorRenderer/composer.json b/src/Symfony/Component/ErrorRenderer/composer.json index 804534aec6..61e052e536 100644 --- a/src/Symfony/Component/ErrorRenderer/composer.json +++ b/src/Symfony/Component/ErrorRenderer/composer.json @@ -33,6 +33,7 @@ }, "autoload": { "psr-4": { "Symfony\\Component\\ErrorRenderer\\": "" }, + "classmap": [ "Resources/stubs/Exception/FlattenException.php" ], "exclude-from-classmap": [ "/Tests/" ] diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 6a11a170b3..f635698347 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -15,6 +15,8 @@ CHANGELOG * Marked all dispatched event classes as `@final` * Added `ErrorController` to enable the preview and error rendering mechanism * Getting the container from a non-booted kernel is deprecated. + * Deprecated passing the `exception` attribute (instance of `Symfony\Component\Debug\Exception\FlattenException`) + to the configured controller of the `ExceptionListener` 4.3.0 ----- diff --git a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php index 3efa4e96dc..514ca597ee 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php +++ b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php @@ -37,12 +37,12 @@ class ErrorController $this->errorRenderer = $errorRenderer; } - public function __invoke(Request $request, FlattenException $exception): Response + public function __invoke(Request $request, FlattenException $e): Response { try { - return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode(), $exception->getHeaders()); - } catch (ErrorRendererNotFoundException $e) { - return new Response($this->errorRenderer->render($exception), $exception->getStatusCode(), $exception->getHeaders()); + return new Response($this->errorRenderer->render($e, $request->getPreferredFormat()), $e->getStatusCode(), $e->getHeaders()); + } catch (ErrorRendererNotFoundException $_) { + return new Response($this->errorRenderer->render($e), $e->getStatusCode(), $e->getHeaders()); } } @@ -57,7 +57,7 @@ class ErrorController */ $subRequest = $request->duplicate(null, null, [ '_controller' => $this->controller, - 'exception' => $exception, + 'e' => $exception, 'logger' => null, 'showException' => false, ]); diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index ae64374c6e..0c8c871e67 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; +use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -123,9 +124,21 @@ class ExceptionListener implements EventSubscriberInterface */ protected function duplicateRequest(\Exception $exception, Request $request) { + @trigger_error(sprintf('Passing the "exception" attribute (instance of "%s") to the configured controller of the "%s" class is deprecated since Symfony 4.4, use the passed "e" attribute (instance of "%s") instead.', LegacyFlattenException::class, self::class, FlattenException::class)); + + $flattenException = FlattenException::createFromThrowable($exception); + + // BC layer to be removed in 5.0 + if (class_exists(\Symfony\Component\Debug\Debug::class, false)) { + $legacyFlattenException = LegacyFlattenException::createFromThrowable($exception); + } else { + $legacyFlattenException = $flattenException; + } + $attributes = [ '_controller' => $this->controller, - 'exception' => FlattenException::createFromThrowable($exception), + 'exception' => $legacyFlattenException, // to be removed in 5.0 + 'e' => $flattenException, 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, ]; $request = $request->duplicate(null, null, $attributes); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php index bc37a0e15e..9f380732af 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php @@ -103,7 +103,7 @@ class ErrorControllerTest extends TestCase ->method('handle') ->with( $this->callback(function (Request $request) use ($_controller, $code) { - $exception = $request->attributes->get('exception'); + $exception = $request->attributes->get('e'); $this->assertSame($_controller, $request->attributes->get('_controller')); $this->assertInstanceOf(FlattenException::class, $exception); From 6e7f3257ceed38c3f1fdc361861976d5686692d8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 10 Oct 2019 10:55:24 +0200 Subject: [PATCH 6/7] [HttpClient] fix merge --- src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index a680b02b10..a0b3b1d096 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -47,6 +47,7 @@ abstract class HttpClientTestCase extends BaseHttpClientTestCase $requestHeaders = $response->toArray(); $this->assertArrayNotHasKey('HTTP_ACCEPT', $requestHeaders); + } public function testToStream() { From 5a4a30c6efbe31471ed915212563dbd37f23dfe9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 9 Oct 2019 20:15:16 +0200 Subject: [PATCH 7/7] [Cache] add TagAwareMarshaller to optimize data storage when using AbstractTagAwareAdapter --- .../Cache/Adapter/AbstractTagAwareAdapter.php | 14 ++- .../Adapter/FilesystemTagAwareAdapter.php | 38 +++++++- .../Cache/Adapter/RedisTagAwareAdapter.php | 39 +++++++- src/Symfony/Component/Cache/CHANGELOG.md | 1 + .../Cache/Marshaller/TagAwareMarshaller.php | 89 +++++++++++++++++++ .../Cache/Traits/AbstractAdapterTrait.php | 4 +- .../Component/Cache/Traits/RedisTrait.php | 11 +-- 7 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Component/Cache/Marshaller/TagAwareMarshaller.php diff --git a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php index ea72ad4e59..018c5192b5 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php @@ -149,6 +149,16 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA */ abstract protected function doInvalidate(array $tagIds): bool; + /** + * Returns the tags bound to the provided ids. + */ + protected function doFetchTags(array $ids): iterable + { + foreach ($this->doFetch($ids) as $id => $value) { + yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : []; + } + } + /** * {@inheritdoc} * @@ -233,8 +243,8 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA } try { - foreach ($this->doFetch($ids) as $id => $value) { - foreach ($value['tags'] ?? [] as $tag) { + foreach ($this->doFetchTags($ids) as $id => $tags) { + foreach ($tags as $tag) { $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id; } } diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php index 82c3960a84..c9f777ca53 100644 --- a/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Cache\Adapter; -use Symfony\Component\Cache\Marshaller\DefaultMarshaller; use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\Traits\FilesystemTrait; @@ -37,7 +37,7 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null) { - $this->marshaller = $marshaller ?? new DefaultMarshaller(); + $this->marshaller = new TagAwareMarshaller($marshaller); parent::__construct('', $defaultLifetime); $this->init($namespace, $directory); } @@ -130,6 +130,40 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune return $failed; } + /** + * {@inheritdoc} + */ + protected function doFetchTags(array $ids): iterable + { + foreach ($ids as $id) { + $file = $this->getFile($id); + if (!file_exists($file) || !$h = @fopen($file, 'rb')) { + continue; + } + + $meta = explode("\n", fread($h, 4096), 3)[2] ?? ''; + + // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) { + $meta[9] = "\0"; + $tagLen = unpack('Nlen', $meta, 9)['len']; + $meta = substr($meta, 13, $tagLen); + + if (0 < $tagLen -= \strlen($meta)) { + $meta .= fread($h, $tagLen); + } + + try { + yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta); + } catch (\Exception $e) { + yield $id => []; + } + } + + fclose($h); + } + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php index b5006eded2..fa4f8c8f13 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php @@ -16,6 +16,7 @@ use Predis\Connection\Aggregate\PredisCluster; use Predis\Response\Status; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; use Symfony\Component\Cache\Traits\RedisTrait; /** @@ -67,7 +68,7 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, \get_class($redisClient->getConnection()))); } - $this->init($redisClient, $namespace, $defaultLifetime, $marshaller); + $this->init($redisClient, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller)); } /** @@ -119,6 +120,42 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter return $failed; } + /** + * {@inheritdoc} + */ + protected function doFetchTags(array $ids): iterable + { + $lua = <<<'EOLUA' + local v = redis.call('GET', KEYS[1]) + + if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then + return '' + end + + return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536) +EOLUA; + + if ($this->redis instanceof \Predis\ClientInterface) { + $evalArgs = [$lua, 1, &$id]; + } else { + $evalArgs = [$lua, [&$id], 1]; + } + + $results = $this->pipeline(function () use ($ids, &$id, $evalArgs) { + foreach ($ids as $id) { + yield 'eval' => $evalArgs; + } + }); + + foreach ($results as $id => $result) { + try { + yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result); + } catch (\Exception $e) { + yield $id => []; + } + } + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index bbe316830b..ffe2bbeaa0 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * added support for connecting to Redis Sentinel clusters * added argument `$prefix` to `AdapterInterface::clear()` * improved `RedisTagAwareAdapter` to support Redis server >= 2.8 and up to 4B items per tag + * added `TagAwareMarshaller` for optimized data storage when using `AbstractTagAwareAdapter` * [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead 4.3.0 diff --git a/src/Symfony/Component/Cache/Marshaller/TagAwareMarshaller.php b/src/Symfony/Component/Cache/Marshaller/TagAwareMarshaller.php new file mode 100644 index 0000000000..5d1e303b47 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/TagAwareMarshaller.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +/** + * A marshaller optimized for data structures generated by AbstractTagAwareAdapter. + * + * @author Nicolas Grekas + */ +class TagAwareMarshaller implements MarshallerInterface +{ + private $marshaller; + + public function __construct(MarshallerInterface $marshaller = null) + { + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $failed = $notSerialized = $serialized = []; + + foreach ($values as $id => $value) { + if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) { + // if the value is an array with keys "tags", "value" and "meta", use a compact serialization format + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall() + + $v = $this->marshaller->marshall($value, $f); + + if ($f) { + $f = []; + $failed[] = $id; + } else { + if ([] === $value['tags']) { + $v['tags'] = ''; + } + + $serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value']; + $serialized[$id][9] = "\x5F"; + } + } else { + // other arbitratry values are serialized using the decorated marshaller below + $notSerialized[$id] = $value; + } + } + + if ($notSerialized) { + $serialized += $this->marshaller->marshall($notSerialized, $f); + $failed = array_merge($failed, $f); + } + + return $serialized; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) { + return $this->marshaller->unmarshall($value); + } + + // data consists of value, tags and metadata which we need to unpack + $meta = substr($value, 1, 12); + $meta[8] = "\0"; + $tagLen = unpack('Nlen', $meta, 8)['len']; + $meta = substr($meta, 0, 8); + + return [ + 'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)), + 'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [], + 'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta, + ]; + } +} diff --git a/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php b/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php index a8aa2acaf3..15b89f5faf 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php @@ -51,11 +51,13 @@ trait AbstractAdapterTrait foreach ($this->doFetch([$id]) as $value) { $isHit = true; } + + return $f($key, $value, $isHit); } catch (\Exception $e) { CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e]); } - return $f($key, $value, $isHit); + return $f($key, null, false); } /** diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index c526b971e3..1399f6b75e 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -445,25 +445,26 @@ trait RedisTrait $results = []; foreach ($generator() as $command => $args) { $results[] = $redis->{$command}(...$args); - $ids[] = $args[0]; + $ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface ? $args[2] : $args[1][0]) : $args[0]; } } elseif ($redis instanceof \Predis\ClientInterface) { $results = $redis->pipeline(static function ($redis) use ($generator, &$ids) { foreach ($generator() as $command => $args) { $redis->{$command}(...$args); - $ids[] = $args[0]; + $ids[] = 'eval' === $command ? $args[2] : $args[0]; } }); } elseif ($redis instanceof \RedisArray) { $connections = $results = $ids = []; foreach ($generator() as $command => $args) { - if (!isset($connections[$h = $redis->_target($args[0])])) { + $id = 'eval' === $command ? $args[1][0] : $args[0]; + if (!isset($connections[$h = $redis->_target($id)])) { $connections[$h] = [$redis->_instance($h), -1]; $connections[$h][0]->multi(\Redis::PIPELINE); } $connections[$h][0]->{$command}(...$args); $results[] = [$h, ++$connections[$h][1]]; - $ids[] = $args[0]; + $ids[] = $id; } foreach ($connections as $h => $c) { $connections[$h] = $c[0]->exec(); @@ -475,7 +476,7 @@ trait RedisTrait $redis->multi(\Redis::PIPELINE); foreach ($generator() as $command => $args) { $redis->{$command}(...$args); - $ids[] = $args[0]; + $ids[] = 'eval' === $command ? $args[1][0] : $args[0]; } $results = $redis->exec(); }