diff --git a/BufferingLogger.php b/BufferingLogger.php index b33e079..cbbe499 100644 --- a/BufferingLogger.php +++ b/BufferingLogger.php @@ -40,10 +40,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - /** - * @return void - */ - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } @@ -53,7 +50,7 @@ public function __destruct() foreach ($this->logs as [$level, $message, $context]) { if (str_contains($message, '{')) { foreach ($context as $key => $val) { - if (null === $val || \is_scalar($val) || (\is_object($val) && \is_callable([$val, '__toString']))) { + if (null === $val || \is_scalar($val) || $val instanceof \Stringable) { $message = str_replace("{{$key}}", $val, $message); } elseif ($val instanceof \DateTimeInterface) { $message = str_replace("{{$key}}", $val->format(\DateTimeInterface::RFC3339), $message); @@ -65,7 +62,7 @@ public function __destruct() } } - error_log(sprintf('%s [%s] %s', date(\DateTimeInterface::RFC3339), $level, $message)); + error_log(\sprintf('%s [%s] %s', date(\DateTimeInterface::RFC3339), $level, $message)); } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5e8fb..cd8d07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +7.3 +--- + + * Add `error:dump` command + +7.1 +--- + + * Increase log level to "error" at least for all PHP errors + 6.4 --- diff --git a/Command/ErrorDumpCommand.php b/Command/ErrorDumpCommand.php new file mode 100644 index 0000000..95b6f44 --- /dev/null +++ b/Command/ErrorDumpCommand.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; + +/** + * Dump error pages to plain HTML files that can be directly served by a web server. + * + * @author Loïck Piera + */ +#[AsCommand( + name: 'error:dump', + description: 'Dump error pages to plain HTML files that can be directly served by a web server', +)] +final class ErrorDumpCommand extends Command +{ + public function __construct( + private readonly Filesystem $filesystem, + private readonly ErrorRendererInterface $errorRenderer, + private readonly ?EntrypointLookupInterface $entrypointLookup = null, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('path', InputArgument::REQUIRED, 'Path where to dump the error pages in') + ->addArgument('status-codes', InputArgument::IS_ARRAY, 'Status codes to dump error pages for, all of them by default') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force directory removal before dumping new error pages') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $path = $input->getArgument('path'); + + $io = new SymfonyStyle($input, $output); + $io->title('Dumping error pages'); + + $this->dump($io, $path, $input->getArgument('status-codes'), (bool) $input->getOption('force')); + $io->success(\sprintf('Error pages have been dumped in "%s".', $path)); + + return Command::SUCCESS; + } + + private function dump(SymfonyStyle $io, string $path, array $statusCodes, bool $force = false): void + { + if (!$statusCodes) { + $statusCodes = array_filter(array_keys(Response::$statusTexts), fn ($statusCode) => $statusCode >= 400); + } + + if ($force || ($this->filesystem->exists($path) && $io->confirm(\sprintf('The "%s" directory already exists. Do you want to remove it before dumping the error pages?', $path), false))) { + $this->filesystem->remove($path); + } + + foreach ($statusCodes as $statusCode) { + // Avoid assets to be included only on the first dumped page + $this->entrypointLookup?->reset(); + + $this->filesystem->dumpFile($path.\DIRECTORY_SEPARATOR.$statusCode.'.html', $this->errorRenderer->render(new HttpException((int) $statusCode))->getAsString()); + } + } +} diff --git a/DebugClassLoader.php b/DebugClassLoader.php index 3f2a136..fa90296 100644 --- a/DebugClassLoader.php +++ b/DebugClassLoader.php @@ -70,12 +70,14 @@ class DebugClassLoader 'iterable' => 'iterable', 'object' => 'object', 'string' => 'string', + 'non-empty-string' => 'string', 'self' => 'self', 'parent' => 'parent', 'mixed' => 'mixed', 'static' => 'static', '$this' => 'static', 'list' => 'array', + 'non-empty-list' => 'array', 'class-string' => 'string', 'never' => 'never', ]; @@ -106,6 +108,10 @@ class DebugClassLoader '__toString' => 'string', '__debugInfo' => 'array', '__serialize' => 'array', + '__set' => 'void', + '__unset' => 'void', + '__unserialize' => 'void', + '__wakeup' => 'void', ]; /** @@ -156,13 +162,13 @@ public function __construct(callable $classLoader) $test = realpath($dir.$test); if (false === $test || false === $i) { - // filesystem is case sensitive + // filesystem is case-sensitive self::$caseCheck = 0; } elseif (str_ends_with($test, $file)) { - // filesystem is case insensitive and realpath() normalizes the case of characters + // filesystem is case-insensitive and realpath() normalizes the case of characters self::$caseCheck = 1; } elseif ('Darwin' === \PHP_OS_FAMILY) { - // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters + // on MacOSX, HFS+ is case-insensitive but realpath() doesn't normalize the case of characters self::$caseCheck = 2; } else { // filesystem case checks failed, fallback to disabling them @@ -182,7 +188,7 @@ public function getClassLoader(): callable public static function enable(): void { // Ensures we don't hit https://bugs.php.net/42098 - class_exists(\Symfony\Component\ErrorHandler\ErrorHandler::class); + class_exists(ErrorHandler::class); class_exists(\Psr\Log\LogLevel::class); if (!\is_array($functions = spl_autoload_functions())) { @@ -332,7 +338,7 @@ private function checkClass(string $class, ?string $file = null): void $name = $refl->getName(); if ($name !== $class && 0 === strcasecmp($name, $class)) { - throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); + throw new \RuntimeException(\sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); } $deprecations = $this->checkAnnotations($refl, $name); @@ -348,14 +354,14 @@ private function checkClass(string $class, ?string $file = null): void if (!$exists) { if (str_contains($class, '/')) { - throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); + throw new \RuntimeException(\sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); } - throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); + throw new \RuntimeException(\sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); } if (self::$caseCheck && $message = $this->checkCase($refl, $file, $class)) { - throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2])); + throw new \RuntimeException(\sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2])); } } @@ -416,7 +422,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array } if (isset(self::$final[$parent])) { - $deprecations[] = sprintf('The "%s" class is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $className); + $deprecations[] = \sprintf('The "%s" class is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $className); } } @@ -429,10 +435,10 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait'); $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); - $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s', $className, $type, $verb, $use, self::$deprecated[$use]); + $deprecations[] = \sprintf('The "%s" %s %s "%s" that is deprecated%s', $className, $type, $verb, $use, self::$deprecated[$use]); } if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) { - $deprecations[] = sprintf('The "%s" %s is considered internal%s It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $className); + $deprecations[] = \sprintf('The "%s" %s is considered internal%s It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $className); } if (isset(self::$method[$use])) { if ($refl->isAbstract()) { @@ -458,7 +464,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array } $realName = substr($name, 0, strpos($name, '(')); if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) { - $deprecations[] = sprintf('Class "%s" should implement method "%s::%s%s"%s', $className, ($static ? 'static ' : '').$interface, $name, $returnType ? ': '.$returnType : '', null === $description ? '.' : ': '.$description); + $deprecations[] = \sprintf('Class "%s" should implement method "%s::%s%s"%s', $className, ($static ? 'static ' : '').$interface, $name, $returnType ? ': '.$returnType : '', null === $description ? '.' : ': '.$description); } } } @@ -522,13 +528,13 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array if ($parent && isset(self::$finalMethods[$parent][$method->name])) { [$declaringClass, $message] = self::$finalMethods[$parent][$method->name]; - $deprecations[] = sprintf('The "%s::%s()" method is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className); + $deprecations[] = \sprintf('The "%s::%s()" method is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className); } if (isset(self::$internalMethods[$class][$method->name])) { [$declaringClass, $message] = self::$internalMethods[$class][$method->name]; if (strncmp($ns, $declaringClass, $len)) { - $deprecations[] = sprintf('The "%s::%s()" method is considered internal%s It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className); + $deprecations[] = \sprintf('The "%s::%s()" method is considered internal%s It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className); } } @@ -547,7 +553,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) { if (!isset($definedParameters[$parameterName]) && !isset($doc['param'][$parameterName])) { - $deprecations[] = sprintf($deprecation, $className); + $deprecations[] = \sprintf($deprecation, $className); } } } @@ -555,9 +561,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array $forcePatchTypes = $this->patchTypes['force']; if ($canAddReturnType = null !== $forcePatchTypes && !str_contains($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) { - if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) { - $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock'; - } + $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock'; $canAddReturnType = 2 === (int) $forcePatchTypes || false !== stripos($method->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR) @@ -585,7 +589,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array if ('docblock' === $this->patchTypes['force']) { $this->patchMethod($method, $returnType, $declaringFile, $normalizedType); } elseif ('' !== $declaringClass && $this->patchTypes['deprecations']) { - $deprecations[] = sprintf('Method "%s::%s()" might add "%s" as a native return type declaration in the future. Do the same in %s "%s" now to avoid errors or add an explicit @return annotation to suppress this message.', $declaringClass, $method->name, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className); + $deprecations[] = \sprintf('Method "%s::%s()" might add "%s" as a native return type declaration in the future. Do the same in %s "%s" now to avoid errors or add an explicit @return annotation to suppress this message.', $declaringClass, $method->name, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className); } } } @@ -596,7 +600,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array continue; } - if (isset($doc['return']) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) { + if (isset($doc['return'])) { $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType()); if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) { @@ -634,7 +638,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array } foreach ($doc['param'] as $parameterName => $parameterType) { if (!isset($definedParameters[$parameterName])) { - self::$annotatedParameters[$class][$method->name][$parameterName] = sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its %s "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType.' ' : '', $parameterName, interface_exists($className) ? 'interface' : 'parent class', $className); + self::$annotatedParameters[$class][$method->name][$parameterName] = \sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its %s "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType.' ' : '', $parameterName, interface_exists($className) ? 'interface' : 'parent class', $className); } } } @@ -654,7 +658,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array foreach ($parentAndOwnInterfaces as $use) { if (isset(self::${$type}[$use][$r->name]) && !isset($doc['deprecated']) && ('finalConstants' === $type || substr($use, 0, strrpos($use, '\\')) !== substr($use, 0, strrpos($class, '\\')))) { $msg = 'finalConstants' === $type ? '%s" constant' : '$%s" property'; - $deprecations[] = sprintf('The "%s::'.$msg.' is considered final. You should not override it in "%s".', self::${$type}[$use][$r->name], $r->name, $class); + $deprecations[] = \sprintf('The "%s::'.$msg.' is considered final. You should not override it in "%s".', self::${$type}[$use][$r->name], $r->name, $class); } } @@ -851,6 +855,30 @@ private function setReturnType(string $types, string $class, string $method, str $docTypes = []; foreach ($typesMap as $n => $t) { + if (str_contains($n, '::')) { + [$definingClass, $constantName] = explode('::', $n, 2); + $definingClass = match ($definingClass) { + 'self', 'static', 'parent' => $class, + default => $definingClass, + }; + + if (!\defined($definingClass.'::'.$constantName)) { + return; + } + + $constant = new \ReflectionClassConstant($definingClass, $constantName); + + if (\PHP_VERSION_ID >= 80300 && $constantType = $constant->getType()) { + if ($constantType instanceof \ReflectionNamedType) { + $n = $constantType->getName(); + } else { + return; + } + } else { + $n = \gettype($constant->getValue()); + } + } + if ('null' === $n) { $nullable = true; continue; @@ -874,7 +902,7 @@ private function setReturnType(string $types, string $class, string $method, str continue; } - if (!isset($phpTypes[''])) { + if (!isset($phpTypes['']) && !\in_array($n, $phpTypes, true)) { $phpTypes[] = $n; } } @@ -895,8 +923,8 @@ private function setReturnType(string $types, string $class, string $method, str } } - $phpType = sprintf($nullable ? (1 < \count($phpTypes) ? '%s|null' : '?%s') : '%s', implode($glue, $phpTypes)); - $docType = sprintf($nullable ? '%s|null' : '%s', implode($glue, $docTypes)); + $phpType = \sprintf($nullable ? (1 < \count($phpTypes) ? '%s|null' : '?%s') : '%s', implode($glue, $phpTypes)); + $docType = \sprintf($nullable ? '%s|null' : '%s', implode($glue, $docTypes)); self::$returnTypes[$class][$method] = [$phpType, $docType, $class, $filename]; } @@ -1028,7 +1056,7 @@ private function patchMethod(\ReflectionMethod $method, string $returnType, stri ++$fileOffset; } - $returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias; + $returnType[$i] = null !== $format ? \sprintf($format, $alias) : $alias; } if ('docblock' === $this->patchTypes['force'] || ('object' === $normalizedType && '7.1' === $this->patchTypes['php'])) { @@ -1137,7 +1165,7 @@ private function fixReturnStatements(\ReflectionMethod $method, string $returnTy $braces = 0; for (; $i < $end; ++$i) { if (!$inClosure) { - $inClosure = false !== strpos($code[$i], 'function ('); + $inClosure = str_contains($code[$i], 'function ('); } if ($inClosure) { diff --git a/Error/FatalError.php b/Error/FatalError.php index a0657b7..b80e3fa 100644 --- a/Error/FatalError.php +++ b/Error/FatalError.php @@ -13,17 +13,19 @@ class FatalError extends \Error { - private array $error; - /** * @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) - { + public function __construct( + string $message, + int $code, + private array $error, + ?int $traceOffset = null, + bool $traceArgs = true, + ?array $trace = null, + ) { parent::__construct($message, $code); - $this->error = $error; - if (null !== $trace) { if (!$traceArgs) { foreach ($trace as &$frame) { diff --git a/ErrorEnhancer/ClassNotFoundErrorEnhancer.php b/ErrorEnhancer/ClassNotFoundErrorEnhancer.php index b4623cf..fc243a6 100644 --- a/ErrorEnhancer/ClassNotFoundErrorEnhancer.php +++ b/ErrorEnhancer/ClassNotFoundErrorEnhancer.php @@ -34,11 +34,11 @@ public function enhance(\Throwable $error): ?\Throwable if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); - $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); + $message = \sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); $tail = ' for another namespace?'; } else { $className = $fullyQualifiedClassName; - $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); + $message = \sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); $tail = '?'; } diff --git a/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php b/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php index 0458c26..41d7af3 100644 --- a/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php +++ b/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php @@ -47,10 +47,10 @@ public function enhance(\Throwable $error): ?\Throwable if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); - $message = sprintf('Attempted to call function "%s" from namespace "%s".', $functionName, $namespacePrefix); + $message = \sprintf('Attempted to call undefined function "%s" from namespace "%s".', $functionName, $namespacePrefix); } else { $functionName = $fullyQualifiedFunctionName; - $message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName); + $message = \sprintf('Attempted to call undefined function "%s" from the global namespace.', $functionName); } $candidates = []; diff --git a/ErrorEnhancer/UndefinedMethodErrorEnhancer.php b/ErrorEnhancer/UndefinedMethodErrorEnhancer.php index 80eaec9..e331c17 100644 --- a/ErrorEnhancer/UndefinedMethodErrorEnhancer.php +++ b/ErrorEnhancer/UndefinedMethodErrorEnhancer.php @@ -34,7 +34,7 @@ public function enhance(\Throwable $error): ?\Throwable $className = $matches[1]; $methodName = $matches[2]; - $message = sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); + $message = \sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); if ('' === $methodName || !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) diff --git a/ErrorHandler.php b/ErrorHandler.php index 052baf2..5ffe75e 100644 --- a/ErrorHandler.php +++ b/ErrorHandler.php @@ -70,12 +70,12 @@ class ErrorHandler private array $loggers = [ \E_DEPRECATED => [null, LogLevel::INFO], \E_USER_DEPRECATED => [null, LogLevel::INFO], - \E_NOTICE => [null, LogLevel::WARNING], - \E_USER_NOTICE => [null, LogLevel::WARNING], - \E_WARNING => [null, LogLevel::WARNING], - \E_USER_WARNING => [null, LogLevel::WARNING], - \E_COMPILE_WARNING => [null, LogLevel::WARNING], - \E_CORE_WARNING => [null, LogLevel::WARNING], + \E_NOTICE => [null, LogLevel::ERROR], + \E_USER_NOTICE => [null, LogLevel::ERROR], + \E_WARNING => [null, LogLevel::ERROR], + \E_USER_WARNING => [null, LogLevel::ERROR], + \E_COMPILE_WARNING => [null, LogLevel::ERROR], + \E_CORE_WARNING => [null, LogLevel::ERROR], \E_USER_ERROR => [null, LogLevel::CRITICAL], \E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], \E_COMPILE_ERROR => [null, LogLevel::CRITICAL], @@ -90,7 +90,6 @@ class ErrorHandler private int $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE private int $loggedErrors = 0; private \Closure $configureException; - private bool $debug; private bool $isRecursive = false; private bool $isRoot = false; @@ -177,11 +176,13 @@ public static function call(callable $function, mixed ...$arguments): mixed } } - public function __construct(?BufferingLogger $bootstrappingLogger = null, bool $debug = false) - { + public function __construct( + ?BufferingLogger $bootstrappingLogger = null, + private bool $debug = false, + ) { if (\PHP_VERSION_ID < 80400) { $this->levels[\E_STRICT] = 'Runtime Notice'; - $this->loggers[\E_STRICT] = [null, LogLevel::WARNING]; + $this->loggers[\E_STRICT] = [null, LogLevel::ERROR]; } if ($bootstrappingLogger) { @@ -193,9 +194,8 @@ public function __construct(?BufferingLogger $bootstrappingLogger = null, bool $ $traceReflector->setValue($e, $trace); $e->file = $file ?? $e->file; $e->line = $line ?? $e->line; - }, null, new class() extends \Exception { + }, null, new class extends \Exception { }); - $this->debug = $debug; } /** @@ -435,7 +435,7 @@ public function handleError(int $type, string $message, string $file, int $line) return true; } } else { - if (PHP_VERSION_ID < 80303 && str_contains($message, '@anonymous')) { + if (\PHP_VERSION_ID < 80303 && str_contains($message, '@anonymous')) { $backtrace = debug_backtrace(false, 5); for ($i = 1; isset($backtrace[$i]); ++$i) { @@ -451,7 +451,7 @@ public function handleError(int $type, string $message, string $file, int $line) } } - if (false !== strpos($message, "@anonymous\0")) { + if (str_contains($message, "@anonymous\0")) { $message = $this->parseAnonymousClass($message); $logMessage = $this->levels[$type].': '.$message; } diff --git a/ErrorRenderer/CliErrorRenderer.php b/ErrorRenderer/CliErrorRenderer.php index 04b3edb..c414c83 100644 --- a/ErrorRenderer/CliErrorRenderer.php +++ b/ErrorRenderer/CliErrorRenderer.php @@ -26,7 +26,7 @@ class CliErrorRenderer implements ErrorRendererInterface public function render(\Throwable $exception): FlattenException { $cloner = new VarCloner(); - $dumper = new class() extends CliDumper { + $dumper = new class extends CliDumper { protected function supportsColors(): bool { $outputStream = $this->outputStream; diff --git a/ErrorRenderer/FileLinkFormatter.php b/ErrorRenderer/FileLinkFormatter.php index ca793b0..30b8663 100644 --- a/ErrorRenderer/FileLinkFormatter.php +++ b/ErrorRenderer/FileLinkFormatter.php @@ -25,15 +25,16 @@ class FileLinkFormatter { private array|false $fileLinkFormat; - private ?RequestStack $requestStack = null; - private ?string $baseDir = null; - private \Closure|string|null $urlFormat; /** * @param string|\Closure $urlFormat The URL format, or a closure that returns it on-demand */ - public function __construct(string|array|null $fileLinkFormat = null, ?RequestStack $requestStack = null, ?string $baseDir = null, string|\Closure|null $urlFormat = null) - { + public function __construct( + string|array|null $fileLinkFormat = null, + private ?RequestStack $requestStack = null, + private ?string $baseDir = null, + private string|\Closure|null $urlFormat = null, + ) { $fileLinkFormat ??= $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? ''; if (!\is_array($f = $fileLinkFormat)) { @@ -43,15 +44,9 @@ public function __construct(string|array|null $fileLinkFormat = null, ?RequestSt } $this->fileLinkFormat = $fileLinkFormat; - $this->requestStack = $requestStack; - $this->baseDir = $baseDir; - $this->urlFormat = $urlFormat; } - /** - * @return string|false - */ - public function format(string $file, int $line): string|bool + public function format(string $file, int $line): string|false { if ($fmt = $this->getFileLinkFormat()) { for ($i = 1; isset($fmt[$i]); ++$i) { @@ -109,7 +104,3 @@ private function getFileLinkFormat(): array|false return false; } } - -if (!class_exists(\Symfony\Component\HttpKernel\Debug\FileLinkFormatter::class, false)) { - class_alias(FileLinkFormatter::class, \Symfony\Component\HttpKernel\Debug\FileLinkFormatter::class); -} diff --git a/ErrorRenderer/HtmlErrorRenderer.php b/ErrorRenderer/HtmlErrorRenderer.php index 2572a8a..7bd9a08 100644 --- a/ErrorRenderer/HtmlErrorRenderer.php +++ b/ErrorRenderer/HtmlErrorRenderer.php @@ -37,9 +37,7 @@ class HtmlErrorRenderer implements ErrorRendererInterface private bool|\Closure $debug; private string $charset; private FileLinkFormatter $fileLinkFormat; - private ?string $projectDir; private string|\Closure $outputBuffer; - private ?LoggerInterface $logger; private static string $template = 'views/error.html.php'; @@ -47,14 +45,18 @@ class HtmlErrorRenderer implements ErrorRendererInterface * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it * @param string|callable $outputBuffer The output buffer as a string or a callable that should return it */ - public function __construct(bool|callable $debug = false, ?string $charset = null, string|FileLinkFormatter|null $fileLinkFormat = null, ?string $projectDir = null, string|callable $outputBuffer = '', ?LoggerInterface $logger = null) - { + public function __construct( + bool|callable $debug = false, + ?string $charset = null, + string|FileLinkFormatter|null $fileLinkFormat = null, + private ?string $projectDir = null, + string|callable $outputBuffer = '', + private ?LoggerInterface $logger = null, + ) { $this->debug = \is_bool($debug) ? $debug : $debug(...); $this->charset = $charset ?: (\ini_get('default_charset') ?: 'UTF-8'); $this->fileLinkFormat = $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : new FileLinkFormatter($fileLinkFormat); - $this->projectDir = $projectDir; $this->outputBuffer = \is_string($outputBuffer) ? $outputBuffer : $outputBuffer(...); - $this->logger = $logger; } public function render(\Throwable $exception): FlattenException @@ -158,9 +160,9 @@ private function formatArgs(array $args): string $result = []; foreach ($args as $key => $item) { if ('object' === $item[0]) { - $formattedValue = sprintf('object(%s)', $this->abbrClass($item[1])); + $formattedValue = \sprintf('object(%s)', $this->abbrClass($item[1])); } elseif ('array' === $item[0]) { - $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); + $formattedValue = \sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); } elseif ('null' === $item[0]) { $formattedValue = 'null'; } elseif ('boolean' === $item[0]) { @@ -173,7 +175,7 @@ private function formatArgs(array $args): string $formattedValue = str_replace("\n", '', $this->escape(var_export($item[1], true))); } - $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escape($key), $formattedValue); + $result[] = \is_int($key) ? $formattedValue : \sprintf("'%s' => %s", $this->escape($key), $formattedValue); } return implode(', ', $result); @@ -194,7 +196,7 @@ private function abbrClass(string $class): string $parts = explode('\\', $class); $short = array_pop($parts); - return sprintf('%s', $class, $short); + return \sprintf('%s', $class, $short); } private function getFileRelative(string $file): ?string @@ -223,7 +225,7 @@ private function formatFile(string $file, int $line, ?string $text = null): stri $text = $file; if (null !== $rel = $this->getFileRelative($text)) { $rel = explode('/', $rel, 2); - $text = sprintf('%s%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? '')); + $text = \sprintf('%s%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? '')); } } @@ -237,7 +239,7 @@ private function formatFile(string $file, int $line, ?string $text = null): stri $link = $this->fileLinkFormat->format($file, $line); - return sprintf('%s', $this->escape($link), $text); + return \sprintf('%s', $this->escape($link), $text); } /** diff --git a/ErrorRenderer/SerializerErrorRenderer.php b/ErrorRenderer/SerializerErrorRenderer.php index b09a6e0..3cc6b8e 100644 --- a/ErrorRenderer/SerializerErrorRenderer.php +++ b/ErrorRenderer/SerializerErrorRenderer.php @@ -24,7 +24,6 @@ */ class SerializerErrorRenderer implements ErrorRendererInterface { - private SerializerInterface $serializer; private string|\Closure $format; private ErrorRendererInterface $fallbackErrorRenderer; private bool|\Closure $debug; @@ -34,9 +33,12 @@ class SerializerErrorRenderer implements ErrorRendererInterface * formats not supported by Request::getMimeTypes() should be given as mime types * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it */ - public function __construct(SerializerInterface $serializer, string|callable $format, ?ErrorRendererInterface $fallbackErrorRenderer = null, bool|callable $debug = false) - { - $this->serializer = $serializer; + public function __construct( + private SerializerInterface $serializer, + string|callable $format, + ?ErrorRendererInterface $fallbackErrorRenderer = null, + bool|callable $debug = false, + ) { $this->format = \is_string($format) ? $format : $format(...); $this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer(); $this->debug = \is_bool($debug) ? $debug : $debug(...); diff --git a/Exception/SilencedErrorContext.php b/Exception/SilencedErrorContext.php index ac19e63..b67a2bc 100644 --- a/Exception/SilencedErrorContext.php +++ b/Exception/SilencedErrorContext.php @@ -18,19 +18,15 @@ */ class SilencedErrorContext implements \JsonSerializable { - public $count = 1; - - private int $severity; - private string $file; - private int $line; - private array $trace; - - public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1) - { - $this->severity = $severity; - $this->file = $file; - $this->line = $line; - $this->trace = $trace; + public int $count = 1; + + public function __construct( + private int $severity, + private string $file, + private int $line, + private array $trace = [], + int $count = 1, + ) { $this->count = $count; } diff --git a/README.md b/README.md index 12c0bfa..68904dd 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ The ErrorHandler component provides tools to manage errors and ease debugging PH Getting Started --------------- -``` -$ composer require symfony/error-handler +```bash +composer require symfony/error-handler ``` ```php diff --git a/Resources/assets/css/exception.css b/Resources/assets/css/exception.css index 8c36907..6fbe7c0 100644 --- a/Resources/assets/css/exception.css +++ b/Resources/assets/css/exception.css @@ -132,7 +132,7 @@ table th { background-color: var(--base-2); font-weight: bold; text-align: left; .prewrap { white-space: pre-wrap; } .nowrap { white-space: nowrap; } .newline { display: block; } -.break-long-words { word-wrap: break-word; overflow-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; min-width: 0; } +.break-long-words { word-wrap: break-word; overflow-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; hyphenate-character: ''; min-width: 0; } .text-small { font-size: 12px !important; } .text-muted { color: #999; } .text-bold { font-weight: bold; } diff --git a/Resources/bin/patch-type-declarations b/Resources/bin/patch-type-declarations index f24c996..5be1db4 100755 --- a/Resources/bin/patch-type-declarations +++ b/Resources/bin/patch-type-declarations @@ -19,7 +19,7 @@ if (\in_array('-h', $argv) || \in_array('--help', $argv)) { ' Patches type declarations based on "@return" PHPDoc and triggers deprecations for', ' incompatible method declarations.', '', - ' This assists you to make your package compatible with Symfony 6, but it can be used', + ' This assists you to make your package compatible with Symfony 7, but it can be used', ' for any class/package.', '', ' Available configuration via environment variables:', diff --git a/Resources/views/exception_full.html.php b/Resources/views/exception_full.html.php index af04db1..a865b1c 100644 --- a/Resources/views/exception_full.html.php +++ b/Resources/views/exception_full.html.php @@ -1,4 +1,4 @@ - + diff --git a/Tests/Command/ErrorDumpCommandTest.php b/Tests/Command/ErrorDumpCommandTest.php new file mode 100644 index 0000000..670adbd --- /dev/null +++ b/Tests/Command/ErrorDumpCommandTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\Command; + +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\TwigBundle\Tests\TestCase; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\ErrorHandler\Command\ErrorDumpCommand; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; + +class ErrorDumpCommandTest extends TestCase +{ + private string $tmpDir = ''; + + protected function setUp(): void + { + $this->tmpDir = sys_get_temp_dir().'/error_pages'; + + $fs = new Filesystem(); + $fs->remove($this->tmpDir); + } + + public function testDumpPages() + { + $tester = $this->getCommandTester($this->getKernel(), []); + $tester->execute([ + 'path' => $this->tmpDir, + ]); + + $this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html'); + $this->assertStringContainsString('Error 404', file_get_contents($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html')); + } + + public function testDumpPagesOnlyForGivenStatusCodes() + { + $fs = new Filesystem(); + $fs->mkdir($this->tmpDir); + $fs->touch($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html'); + + $tester = $this->getCommandTester($this->getKernel()); + $tester->execute([ + 'path' => $this->tmpDir, + 'status-codes' => ['400', '500'], + ]); + + $this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html'); + $this->assertFileDoesNotExist($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html'); + + $this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'400.html'); + $this->assertStringContainsString('Error 400', file_get_contents($this->tmpDir.\DIRECTORY_SEPARATOR.'400.html')); + } + + public function testForceRemovalPages() + { + $fs = new Filesystem(); + $fs->mkdir($this->tmpDir); + $fs->touch($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html'); + + $tester = $this->getCommandTester($this->getKernel()); + $tester->execute([ + 'path' => $this->tmpDir, + '--force' => true, + ]); + + $this->assertFileDoesNotExist($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html'); + $this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html'); + } + + private function getKernel(): MockObject&KernelInterface + { + return $this->createMock(KernelInterface::class); + } + + private function getCommandTester(KernelInterface $kernel): CommandTester + { + $errorRenderer = $this->createStub(ErrorRendererInterface::class); + $errorRenderer + ->method('render') + ->willReturnCallback(function (HttpException $e) { + $exception = FlattenException::createFromThrowable($e); + $exception->setAsString(\sprintf('Error %s', $e->getStatusCode())); + + return $exception; + }) + ; + + $entrypointLookup = $this->createMock(EntrypointLookupInterface::class); + + $application = new Application($kernel); + $application->add(new ErrorDumpCommand( + new Filesystem(), + $errorRenderer, + $entrypointLookup, + )); + + return new CommandTester($application->find('error:dump')); + } +} diff --git a/Tests/DebugClassLoaderTest.php b/Tests/DebugClassLoaderTest.php index ac08e69..8575f89 100644 --- a/Tests/DebugClassLoaderTest.php +++ b/Tests/DebugClassLoaderTest.php @@ -87,7 +87,7 @@ public function testFileCaseMismatch() $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Case mismatch between class and real file names'); if (!file_exists(__DIR__.'/Fixtures/CaseMismatch.php')) { - $this->markTestSkipped('Can only be run on case insensitive filesystems'); + $this->markTestSkipped('Can only be run on case-insensitive filesystems'); } class_exists(Fixtures\CaseMismatch::class, true); @@ -401,6 +401,26 @@ class_exists('Test\\'.ReturnType::class, true); 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::true()" might add "true" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::never()" might add "never" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::null()" might add "null" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', + 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParent::classConstant()" might add "string" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnType" now to avoid errors or add an explicit @return annotation to suppress this message.', + ], $deprecations); + } + + /** + * @requires PHP >= 8.3 + */ + public function testReturnTypePhp83() + { + $deprecations = []; + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists('Test\\'.ReturnTypePhp83::class, true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame([ + 'Method "Symfony\Component\ErrorHandler\Tests\Fixtures\ReturnTypeParentPhp83::classConstantWithType()" might add "string" as a native return type declaration in the future. Do the same in child class "Test\Symfony\Component\ErrorHandler\Tests\ReturnTypePhp83" now to avoid errors or add an explicit @return annotation to suppress this message.', ], $deprecations); } @@ -421,7 +441,7 @@ class_exists('Test\\'.OverrideOutsideFinalProperty::class, true); 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalProperty\FinalProperty::$pub" property is considered final. You should not override it in "Symfony\Component\ErrorHandler\Tests\Fixtures\OverrideFinalProperty".', 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalProperty\FinalProperty::$prot" property is considered final. You should not override it in "Symfony\Component\ErrorHandler\Tests\Fixtures\OverrideFinalProperty".', 'The "Symfony\Component\ErrorHandler\Tests\Fixtures\FinalProperty\FinalProperty::$implicitlyFinal" property is considered final. You should not override it in "Symfony\Component\ErrorHandler\Tests\Fixtures\OverrideFinalProperty".', - 'The "Test\Symfony\Component\ErrorHandler\Tests\FinalProperty\OutsideFinalProperty::$final" property is considered final. You should not override it in "Test\Symfony\Component\ErrorHandler\Tests\OverrideOutsideFinalProperty".' + 'The "Test\Symfony\Component\ErrorHandler\Tests\FinalProperty\OutsideFinalProperty::$final" property is considered final. You should not override it in "Test\Symfony\Component\ErrorHandler\Tests\OverrideOutsideFinalProperty".', ], $deprecations); } @@ -542,6 +562,8 @@ public function ownAbstractBaseMethod() { } }'); } elseif ('Test\\'.ReturnType::class === $class) { return $fixtureDir.\DIRECTORY_SEPARATOR.'ReturnType.php'; + } elseif ('Test\\'.ReturnTypePhp83::class === $class) { + return $fixtureDir.\DIRECTORY_SEPARATOR.'ReturnTypePhp83.php'; } elseif ('Test\\'.Fixtures\OutsideInterface::class === $class) { return $fixtureDir.\DIRECTORY_SEPARATOR.'OutsideInterface.php'; } elseif ('Test\\'.OverrideOutsideFinalProperty::class === $class) { diff --git a/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php b/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php index 72ee199..38b0423 100644 --- a/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php +++ b/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php @@ -156,7 +156,7 @@ public function testEnhanceWithFatalError() public function testCannotRedeclareClass() { if (!file_exists(__DIR__.'/../FIXTURES2/REQUIREDTWICE.PHP')) { - $this->markTestSkipped('Can only be run on case insensitive filesystems'); + $this->markTestSkipped('Can only be run on case-insensitive filesystems'); } require_once __DIR__.'/../FIXTURES2/REQUIREDTWICE.PHP'; diff --git a/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php b/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php index 547e333..b5a0d91 100644 --- a/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php +++ b/Tests/ErrorEnhancer/UndefinedFunctionErrorEnhancerTest.php @@ -28,7 +28,7 @@ public function testEnhance(string $originalMessage, string $enhancedMessage) $error = $enhancer->enhance(new \Error($originalMessage)); $this->assertInstanceOf(UndefinedFunctionError::class, $error); - // class names are case insensitive and PHP do not return the same + // 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()); @@ -39,19 +39,19 @@ public static 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\"?", + "Attempted to call undefined 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\"?", + "Attempted to call undefined 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.', + 'Attempted to call undefined function "foo" from the global namespace.', ], [ 'Call to undefined function Foo\\Bar\\Baz\\foo()', - 'Attempted to call function "foo" from namespace "Foo\Bar\Baz".', + 'Attempted to call undefined function "foo" from namespace "Foo\Bar\Baz".', ], ]; } diff --git a/Tests/ErrorHandlerTest.php b/Tests/ErrorHandlerTest.php index 55a2a6b..5f55cfb 100644 --- a/Tests/ErrorHandlerTest.php +++ b/Tests/ErrorHandlerTest.php @@ -34,7 +34,6 @@ class ErrorHandlerTest extends TestCase protected function tearDown(): void { $r = new \ReflectionProperty(ErrorHandler::class, 'exitCode'); - $r->setAccessible(true); $r->setValue(null, 0); } @@ -201,12 +200,12 @@ public function testDefaultLogger() $loggers = [ \E_DEPRECATED => [null, LogLevel::INFO], \E_USER_DEPRECATED => [null, LogLevel::INFO], - \E_NOTICE => [$logger, LogLevel::WARNING], + \E_NOTICE => [$logger, LogLevel::ERROR], \E_USER_NOTICE => [$logger, LogLevel::CRITICAL], - \E_WARNING => [null, LogLevel::WARNING], - \E_USER_WARNING => [null, LogLevel::WARNING], - \E_COMPILE_WARNING => [null, LogLevel::WARNING], - \E_CORE_WARNING => [null, LogLevel::WARNING], + \E_WARNING => [null, LogLevel::ERROR], + \E_USER_WARNING => [null, LogLevel::ERROR], + \E_COMPILE_WARNING => [null, LogLevel::ERROR], + \E_CORE_WARNING => [null, LogLevel::ERROR], \E_USER_ERROR => [null, LogLevel::CRITICAL], \E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], \E_COMPILE_ERROR => [null, LogLevel::CRITICAL], @@ -216,7 +215,7 @@ public function testDefaultLogger() ]; if (\PHP_VERSION_ID < 80400) { - $loggers[\E_STRICT] = [null, LogLevel::WARNING]; + $loggers[\E_STRICT] = [null, LogLevel::ERROR]; } $this->assertSame($loggers, $handler->setLoggers([])); @@ -333,7 +332,7 @@ public function testHandleError() public function testHandleErrorWithAnonymousClass() { - $anonymousObject = new class() extends \stdClass { + $anonymousObject = new class extends \stdClass { }; $handler = ErrorHandler::register(); @@ -422,7 +421,7 @@ public static function handleExceptionProvider(): array ['Uncaught Exception: foo', new \Exception('foo')], ['Uncaught Exception: foo', new class('foo') extends \RuntimeException { }], - ['Uncaught Exception: foo stdClass@anonymous bar', new \RuntimeException('foo '.(new class() extends \stdClass { + ['Uncaught Exception: foo stdClass@anonymous bar', new \RuntimeException('foo '.(new class extends \stdClass { })::class.' bar')], ['Uncaught Error: bar', new \Error('bar')], ['Uncaught ccc', new \ErrorException('ccc')], @@ -442,12 +441,12 @@ public function testBootstrappingLogger() $loggers = [ \E_DEPRECATED => [$bootLogger, LogLevel::INFO], \E_USER_DEPRECATED => [$bootLogger, LogLevel::INFO], - \E_NOTICE => [$bootLogger, LogLevel::WARNING], - \E_USER_NOTICE => [$bootLogger, LogLevel::WARNING], - \E_WARNING => [$bootLogger, LogLevel::WARNING], - \E_USER_WARNING => [$bootLogger, LogLevel::WARNING], - \E_COMPILE_WARNING => [$bootLogger, LogLevel::WARNING], - \E_CORE_WARNING => [$bootLogger, LogLevel::WARNING], + \E_NOTICE => [$bootLogger, LogLevel::ERROR], + \E_USER_NOTICE => [$bootLogger, LogLevel::ERROR], + \E_WARNING => [$bootLogger, LogLevel::ERROR], + \E_USER_WARNING => [$bootLogger, LogLevel::ERROR], + \E_COMPILE_WARNING => [$bootLogger, LogLevel::ERROR], + \E_CORE_WARNING => [$bootLogger, LogLevel::ERROR], \E_USER_ERROR => [$bootLogger, LogLevel::CRITICAL], \E_RECOVERABLE_ERROR => [$bootLogger, LogLevel::CRITICAL], \E_COMPILE_ERROR => [$bootLogger, LogLevel::CRITICAL], @@ -457,7 +456,7 @@ public function testBootstrappingLogger() ]; if (\PHP_VERSION_ID < 80400) { - $loggers[\E_STRICT] = [$bootLogger, LogLevel::WARNING]; + $loggers[\E_STRICT] = [$bootLogger, LogLevel::ERROR]; } $this->assertSame($loggers, $handler->setLoggers([])); @@ -672,7 +671,7 @@ public function testAssertQuietEval() $logs = $logger->cleanLogs(); - $this->assertSame('warning', $logs[0][0]); + $this->assertSame('error', $logs[0][0]); $this->assertSame('Warning: assert(): assert(false) failed', $logs[0][1]); } diff --git a/Tests/Exception/FlattenExceptionTest.php b/Tests/Exception/FlattenExceptionTest.php index a3ff2f6..c6efb3c 100644 --- a/Tests/Exception/FlattenExceptionTest.php +++ b/Tests/Exception/FlattenExceptionTest.php @@ -245,7 +245,7 @@ public static function stringAndIntDataProvider(): array public function testAnonymousClass() { - $flattened = FlattenException::createFromThrowable(new class() extends \RuntimeException { + $flattened = FlattenException::createFromThrowable(new class extends \RuntimeException { }); $this->assertSame('RuntimeException@anonymous', $flattened->getClass()); @@ -255,7 +255,7 @@ public function testAnonymousClass() $this->assertSame('Symfony\Component\HttpKernel\Exception\NotFoundHttpException@anonymous', $flattened->getClass()); - $flattened = FlattenException::createFromThrowable(new \Exception(sprintf('Class "%s" blah.', (new class() extends \RuntimeException { + $flattened = FlattenException::createFromThrowable(new \Exception(\sprintf('Class "%s" blah.', (new class extends \RuntimeException { })::class))); $this->assertSame('Class "RuntimeException@anonymous" blah.', $flattened->getMessage()); diff --git a/Tests/Fixtures/ReturnType.php b/Tests/Fixtures/ReturnType.php index 1b81380..72570cf 100644 --- a/Tests/Fixtures/ReturnType.php +++ b/Tests/Fixtures/ReturnType.php @@ -51,4 +51,5 @@ public function true() { } public function never() { } public function null() { } public function outsideMethod() { } + public function classConstant() { } } diff --git a/Tests/Fixtures/ReturnTypeParent.php b/Tests/Fixtures/ReturnTypeParent.php index d42c7c8..3803a74 100644 --- a/Tests/Fixtures/ReturnTypeParent.php +++ b/Tests/Fixtures/ReturnTypeParent.php @@ -4,6 +4,8 @@ abstract class ReturnTypeParent extends ReturnTypeGrandParent implements ReturnTypeParentInterface { + const FOO = 'foo'; + /** * @return void */ @@ -254,4 +256,11 @@ public function null() public function notExtended() { } + + /** + * @return self::FOO + */ + public function classConstant() + { + } } diff --git a/Tests/Fixtures/ReturnTypeParentPhp83.php b/Tests/Fixtures/ReturnTypeParentPhp83.php new file mode 100644 index 0000000..c7d5ca8 --- /dev/null +++ b/Tests/Fixtures/ReturnTypeParentPhp83.php @@ -0,0 +1,23 @@ +=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/var-dumper": "^6.4|^7.0" }, "require-dev": { + "symfony/console": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0", - "symfony/deprecation-contracts": "^2.5|^3" + "symfony/serializer": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5",