Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse and render anonymous classes correctly on php 8 #36914

Merged
merged 1 commit into from May 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 0 additions & 2 deletions .travis.yml
Expand Up @@ -31,8 +31,6 @@ matrix:
- php: nightly
services: [memcached]
fast_finish: true
allow_failures:
- php: nightly

cache:
directories:
Expand Down
9 changes: 4 additions & 5 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -863,17 +863,16 @@ 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 (false !== strpos($message, "@anonymous\0")) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $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.15",
"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
10 changes: 5 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,7 @@ public function getClass()
*/
public function setClass($class)
{
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
$this->class = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;

return $this;
}
Expand Down Expand Up @@ -179,9 +179,9 @@ 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 (false !== strpos($message, "@anonymous\0")) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
}

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

public function testHandleErrorWithAnonymousClass()
{
$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) {
} finally {
restore_error_handler();
restore_exception_handler();
}

$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());
}

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.15"
},
"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 = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;

// Don't trigger deprecations for classes in the same vendor
if ($class !== $className) {
Expand Down
8 changes: 4 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,8 @@ 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_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
}
}
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,7 @@ 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;
$this->class = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;

return $this;
}
Expand Down Expand Up @@ -195,9 +195,9 @@ 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 (false !== strpos($message, "@anonymous\0")) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
}

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

public function testHandleErrorWithAnonymousClass()
{
$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) {
} finally {
restore_error_handler();
restore_exception_handler();
}

$this->assertSame('foo stdClass@anonymous bar', $e->getMessage());
$this->assertSame(3, $e->getSeverity());
$this->assertSame('foo.php', $e->getFile());
$this->assertSame(12, $e->getLine());
}

public function testHandleDeprecation()
{
$logArgCheck = function ($level, $message, $context) {
Expand Down Expand Up @@ -433,6 +453,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.15",
"symfony/var-dumper": "^4.4|^5.0"
},
"require-dev": {
Expand Down
7 changes: 2 additions & 5 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 @@ -474,7 +471,7 @@ 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 = false !== strpos($class, "@anonymous\0") ? get_parent_class($class).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.15",
"psr/log": "~1.0"
},
"require-dev": {
Expand Down
Expand Up @@ -78,8 +78,7 @@ public function next(): MiddlewareInterface
if ($this->stack === $nextMiddleware = $this->stack->next()) {
$this->currentEvent = 'Tail';
} else {
$class = \get_class($nextMiddleware);
$this->currentEvent = sprintf('"%s"', 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class);
$this->currentEvent = sprintf('"%s"', get_debug_type($nextMiddleware));
}
$this->currentEvent .= sprintf(' on "%s"', $this->busName);

Expand Down