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 d362a7c
Show file tree
Hide file tree
Showing 22 changed files with 146 additions and 49 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('/([\\\\\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
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
17 changes: 17 additions & 0 deletions src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php
Expand Up @@ -325,6 +325,23 @@ 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
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_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
return 'class' === $m[1] ? (class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0]) : $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
19 changes: 19 additions & 0 deletions src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php
Expand Up @@ -365,6 +365,23 @@ 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 +450,8 @@ 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
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
Expand Up @@ -30,14 +30,16 @@ public function testHandle()
$busId = 'command_bus';
$envelope = new Envelope(new DummyMessage('Hello'));

$middleware = $this->getMockBuilder(MiddlewareInterface::class)->getMock();
$middleware->expects($this->once())
->method('handle')
->with($envelope, $this->anything())
->willReturnCallback(function ($envelope, StackInterface $stack) {
$middleware = new class implements MiddlewareInterface {
public $calls = 0;

public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
++$this->calls;

return $stack->next()->handle($envelope, $stack);
})
;
}
};

$stopwatch = $this->createMock(Stopwatch::class);
$stopwatch->expects($this->once())->method('isStarted')->willReturn(true);
Expand All @@ -51,14 +53,15 @@ public function testHandle()
$stopwatch->expects($this->exactly(2))
->method('stop')
->withConsecutive(
[$this->matches('"%sMiddlewareInterface%s" on "command_bus"')],
['"Symfony\Component\Messenger\Middleware\MiddlewareInterface@anonymous" on "command_bus"'],
['Tail on "command_bus"']
)
;

$traced = new TraceableMiddleware($stopwatch, $busId);

$traced->handle($envelope, new StackMiddleware(new \ArrayIterator([null, $middleware])));
$this->assertSame(1, $middleware->calls);
}

public function testHandleWithException()
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Component/Messenger/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"
},
"require-dev": {
"doctrine/dbal": "^2.6",
Expand Down

0 comments on commit d362a7c

Please sign in to comment.