Skip to content

Commit

Permalink
Parse and render anonymous classes correctly on php 8.
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed May 23, 2020
1 parent a14d8f9 commit f2fccb4
Show file tree
Hide file tree
Showing 23 changed files with 164 additions and 55 deletions.
13 changes: 8 additions & 5 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -863,17 +863,20 @@ private function doActuallyRenderThrowable(\Throwable $e, OutputInterface $outpu
do {
$message = trim($e->getMessage());
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$class = \get_class($e);
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
$class = get_debug_type($e);
$title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$len = Helper::strlen($title);
} else {
$len = 0;
}

if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
if (str_contains($message, "@anonymous\0")) {
$message = preg_replace_callback('/([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
if ('class' === $m[1]) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: 'class').'@anonymous' : $m[0];
}

return $m[1].'@anonymous';
}, $message);
}

Expand Down
16 changes: 10 additions & 6 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Expand Up @@ -897,7 +897,8 @@ public function testRenderAnonymousException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new class('') extends \InvalidArgumentException { };
throw new class('') extends \InvalidArgumentException {
};
});
$tester = new ApplicationTester($application);

Expand All @@ -907,20 +908,22 @@ public function testRenderAnonymousException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { })));
throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() {
})));
});
$tester = new ApplicationTester($application);

$tester->run(['command' => 'foo'], ['decorated' => false]);
$this->assertStringContainsString('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true));
$this->assertStringContainsString('Dummy type "class@anonymous" is invalid.', $tester->getDisplay(true));
}

public function testRenderExceptionStackTraceContainsRootException()
{
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new class('') extends \InvalidArgumentException { };
throw new class('') extends \InvalidArgumentException {
};
});
$tester = new ApplicationTester($application);

Expand All @@ -930,12 +933,13 @@ public function testRenderExceptionStackTraceContainsRootException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { })));
throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() {
})));
});
$tester = new ApplicationTester($application);

$tester->run(['command' => 'foo'], ['decorated' => false]);
$this->assertStringContainsString('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true));
$this->assertStringContainsString('Dummy type "class@anonymous" is invalid.', $tester->getDisplay(true));
}

public function testRun()
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/composer.json
Expand Up @@ -19,6 +19,7 @@
"php": ">=7.1.3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8",
"symfony/polyfill-php80": "^1.17",
"symfony/service-contracts": "^1.1|^2"
},
"require-dev": {
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Debug/ErrorHandler.php
Expand Up @@ -415,7 +415,7 @@ public function handleError($type, $message, $file, $line)
$context = $e;
}

if (false !== strpos($message, "class@anonymous\0")) {
if (false !== strpos($message, "@anonymous\0")) {
$logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage();
} else {
$logMessage = $this->levels[$type].': '.$message;
Expand Down Expand Up @@ -540,7 +540,7 @@ public function handleException($exception, array $error = null)
$handlerException = null;

if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) {
if (false !== strpos($message = $exception->getMessage(), "@anonymous\0")) {
$message = (new FlattenException())->setMessage($message)->getMessage();
}
if ($exception instanceof FatalErrorException) {
Expand Down
Expand Up @@ -26,7 +26,7 @@ class FatalThrowableError extends FatalErrorException

public function __construct(\Throwable $e)
{
$this->originalClassName = \get_class($e);
$this->originalClassName = get_debug_type($e);

if ($e instanceof \ParseError) {
$severity = E_PARSE;
Expand Down
18 changes: 13 additions & 5 deletions src/Symfony/Component/Debug/Exception/FlattenException.php
Expand Up @@ -67,7 +67,7 @@ public static function createFromThrowable(\Throwable $exception, int $statusCod
$e->setStatusCode($statusCode);
$e->setHeaders($headers);
$e->setTraceFromThrowable($exception);
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception));
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : get_debug_type($exception));
$e->setFile($exception->getFile());
$e->setLine($exception->getLine());

Expand Down Expand Up @@ -134,7 +134,11 @@ public function getClass()
*/
public function setClass($class)
{
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
if (preg_match('/^([\\\\\w]+)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', $class, $matches)) {
$this->class = ('class' === $matches[1] ? get_parent_class($matches[0]) : $matches[1]).'@anonymous';
} else {
$this->class = $class;
}

return $this;
}
Expand Down Expand Up @@ -179,9 +183,13 @@ public function getMessage()
*/
public function setMessage($message)
{
if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
if (str_contains($message, "@anonymous\0")) {
$message = preg_replace_callback('/([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
if ('class' === $m[1]) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: 'class').'@anonymous' : $m[0];
}

return $m[1].'@anonymous';
}, $message);
}

Expand Down
18 changes: 18 additions & 0 deletions src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php
Expand Up @@ -325,6 +325,24 @@ public function testHandleUserError()
}
}

public function testHandleErrorWithAnonymousClass(): void
{
$handler = ErrorHandler::register();
$handler->throwAt(E_USER_WARNING, true);
try {
$handler->handleError(E_USER_WARNING, 'foo '.\get_class(new class() extends \stdClass {
}).' bar', 'foo.php', 12);
$this->fail('Exception expected.');
} catch (\ErrorException $e) {
$this->assertSame('User Warning: foo stdClass@anonymous bar', $e->getMessage());
$this->assertSame(E_USER_WARNING, $e->getSeverity());
$this->assertSame('foo.php', $e->getFile());
$this->assertSame(12, $e->getLine());
} finally {
restore_error_handler();
}
}

public function testHandleDeprecation()
{
$logArgCheck = function ($level, $message, $context) {
Expand Down
Expand Up @@ -355,6 +355,11 @@ public function testAnonymousClass()

$this->assertSame('RuntimeException@anonymous', $flattened->getClass());

$flattened->setClass(\get_class(new class('Oops') extends NotFoundHttpException {
}));

$this->assertSame('Symfony\Component\HttpKernel\Exception\NotFoundHttpException@anonymous', $flattened->getClass());

$flattened = FlattenException::create(new \Exception(sprintf('Class "%s" blah.', \get_class(new class() extends \RuntimeException {
}))));

Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Component/Debug/composer.json
Expand Up @@ -17,7 +17,8 @@
],
"require": {
"php": ">=7.1.3",
"psr/log": "~1.0"
"psr/log": "~1.0",
"symfony/polyfill-php80": "~1.17"
},
"conflict": {
"symfony/http-kernel": "<3.4"
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/ErrorHandler/DebugClassLoader.php
Expand Up @@ -407,7 +407,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
}
$deprecations = [];

$className = isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00") ? get_parent_class($class).'@anonymous' : $class;
$className = $refl->isAnonymous() ? (($parentRefl = $refl->getParentClass()) ? $parentRefl->name : 'class').'@anonymous' : $class;

// Don't trigger deprecations for classes in the same vendor
if ($class !== $className) {
Expand Down
12 changes: 8 additions & 4 deletions src/Symfony/Component/ErrorHandler/ErrorHandler.php
Expand Up @@ -435,7 +435,7 @@ public function handleError(int $type, string $message, string $file, int $line)
$context = $e;
}

if (false !== strpos($message, "class@anonymous\0")) {
if (false !== strpos($message, "@anonymous\0")) {
$logMessage = $this->parseAnonymousClass($message);
} else {
$logMessage = $this->levels[$type].': '.$message;
Expand Down Expand Up @@ -558,7 +558,7 @@ public function handleException(\Throwable $exception)
}

if ($this->loggedErrors & $type) {
if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) {
if (false !== strpos($message = $exception->getMessage(), "@anonymous\0")) {
$message = $this->parseAnonymousClass($message);
}

Expand Down Expand Up @@ -768,8 +768,12 @@ private function cleanTrace(array $backtrace, int $type, string $file, int $line
*/
private function parseAnonymousClass(string $message): string
{
return preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
return preg_replace_callback('/([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
if ('class' === $m[1]) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: 'class').'@anonymous' : $m[0];
}

return $m[1].'@anonymous';
}, $message);
}
}
18 changes: 13 additions & 5 deletions src/Symfony/Component/ErrorHandler/Exception/FlattenException.php
Expand Up @@ -71,7 +71,7 @@ public static function createFromThrowable(\Throwable $exception, int $statusCod
$e->setStatusCode($statusCode);
$e->setHeaders($headers);
$e->setTraceFromThrowable($exception);
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception));
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : get_debug_type($exception));
$e->setFile($exception->getFile());
$e->setLine($exception->getLine());

Expand Down Expand Up @@ -138,7 +138,11 @@ public function getClass(): string
*/
public function setClass($class): self
{
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
if (preg_match('/^([\\\\\w]+)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', $class, $matches)) {
$this->class = ('class' === $matches[1] ? get_parent_class($matches[0]) : $matches[1]).'@anonymous';
} else {
$this->class = $class;
}

return $this;
}
Expand Down Expand Up @@ -195,9 +199,13 @@ public function getMessage(): string
*/
public function setMessage($message): self
{
if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
if (str_contains($message, "@anonymous\0")) {
$message = preg_replace_callback('/([\\\\\w]+)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
if ('class' === $m[1]) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
}

return $m[1].'@anonymous';
}, $message);
}

Expand Down
22 changes: 22 additions & 0 deletions src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php
Expand Up @@ -365,6 +365,24 @@ public function testHandleUserError()
}
}

public function testHandleErrorWithAnonymousClass(): void
{
$handler = ErrorHandler::register();
$handler->throwAt(3, true);
try {
$handler->handleError(3, 'foo '.\get_class(new class() extends \stdClass {
}).' bar', 'foo.php', 12);
$this->fail('Exception expected.');
} catch (\ErrorException $e) {
$this->assertSame('foo stdClass@anonymous bar', $e->getMessage());
$this->assertSame(3, $e->getSeverity());
$this->assertSame('foo.php', $e->getFile());
$this->assertSame(12, $e->getLine());
} finally {
restore_error_handler();
}
}

public function testHandleDeprecation()
{
$logArgCheck = function ($level, $message, $context) {
Expand Down Expand Up @@ -433,6 +451,10 @@ public function handleExceptionProvider(): array
{
return [
['Uncaught Exception: foo', new \Exception('foo')],
['Uncaught Exception: foo', new class('foo') extends \RuntimeException {
}],
['Uncaught Exception: foo stdClass@anonymous bar', new \RuntimeException('foo '.\get_class(new class() extends \stdClass {
}).' bar')],
['Uncaught Error: bar', new \Error('bar')],
['Uncaught ccc', new \ErrorException('ccc')],
];
Expand Down
Expand Up @@ -373,6 +373,11 @@ public function testAnonymousClass()

$this->assertSame('RuntimeException@anonymous', $flattened->getClass());

$flattened->setClass(\get_class(new class('Oops') extends NotFoundHttpException {
}));

$this->assertSame('Symfony\Component\HttpKernel\Exception\NotFoundHttpException@anonymous', $flattened->getClass());

$flattened = FlattenException::createFromThrowable(new \Exception(sprintf('Class "%s" blah.', \get_class(new class() extends \RuntimeException {
}))));

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/ErrorHandler/composer.json
Expand Up @@ -19,6 +19,7 @@
"php": ">=7.1.3",
"psr/log": "~1.0",
"symfony/debug": "^4.4.5",
"symfony/polyfill-php80": "^1.17",
"symfony/var-dumper": "^4.4|^5.0"
},
"require-dev": {
Expand Down
9 changes: 3 additions & 6 deletions src/Symfony/Component/HttpKernel/Kernel.php
Expand Up @@ -228,10 +228,7 @@ public function getBundles()
public function getBundle($name)
{
if (!isset($this->bundles[$name])) {
$class = static::class;
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;

throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, $class));
throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, get_debug_type($this)));
}

return $this->bundles[$name];
Expand Down Expand Up @@ -473,8 +470,8 @@ protected function build(ContainerBuilder $container)
*/
protected function getContainerClass()
{
$class = static::class;
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class;
$class = get_debug_type($this);
$class = str_ends_with($class, '@anonymous') ? get_parent_class($this).str_replace('.', '_', ContainerBuilder::hash($class)) : $class;
$class = $this->name.str_replace('\\', '_', $class).ucfirst($this->environment).($this->debug ? 'Debug' : '').'Container';

if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) {
Expand Down
21 changes: 21 additions & 0 deletions src/Symfony/Component/HttpKernel/Tests/KernelTest.php
Expand Up @@ -640,6 +640,27 @@ public function testKernelStartTimeIsResetWhileBootingAlreadyBootedKernel()
$this->assertGreaterThan($preReBoot, $kernel->getStartTime());
}

public function testAnonymousKernelGeneratesValidContainerClass(): void
{
$kernel = new class('test', true) extends Kernel {
public function registerBundles(): iterable
{
return [];
}

public function registerContainerConfiguration(LoaderInterface $loader): void
{
}

public function getContainerClass(): string
{
return parent::getContainerClass();
}
};

$this->assertRegExp('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*TestDebugContainer$/', $kernel->getContainerClass());
}

/**
* Returns a mock for the BundleInterface.
*/
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/composer.json
Expand Up @@ -22,6 +22,7 @@
"symfony/http-foundation": "^4.4|^5.0",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php73": "^1.9",
"symfony/polyfill-php80": "^1.17",
"psr/log": "~1.0"
},
"require-dev": {
Expand Down

0 comments on commit f2fccb4

Please sign in to comment.