Skip to content

Commit 6d02565

Browse files
bug #36914 Parse and render anonymous classes correctly on php 8 (derrabus)
This PR was merged into the 4.4 branch. Discussion ---------- Parse and render anonymous classes correctly on php 8 | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | #36872 | License | MIT | Doc PR | N/A The format of the value that `get_class()` returns for anonymous classes has changed in php 8. This PR attempts to detect both formats, with the help of the PHP80 polyfill where possible. Commits ------- 9d702fd Parse and render anonymous classes correctly on php 8
2 parents 7224c1d + 9d702fd commit 6d02565

24 files changed

+135
-56
lines changed

.travis.yml

-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ matrix:
3131
- php: nightly
3232
services: [memcached]
3333
fast_finish: true
34-
allow_failures:
35-
- php: nightly
3634

3735
cache:
3836
directories:

src/Symfony/Component/Console/Application.php

+4-5
Original file line numberDiff line numberDiff line change
@@ -863,17 +863,16 @@ private function doActuallyRenderThrowable(\Throwable $e, OutputInterface $outpu
863863
do {
864864
$message = trim($e->getMessage());
865865
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
866-
$class = \get_class($e);
867-
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
866+
$class = get_debug_type($e);
868867
$title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
869868
$len = Helper::strlen($title);
870869
} else {
871870
$len = 0;
872871
}
873872

874-
if (false !== strpos($message, "class@anonymous\0")) {
875-
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
876-
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
873+
if (false !== strpos($message, "@anonymous\0")) {
874+
$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) {
875+
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
877876
}, $message);
878877
}
879878

src/Symfony/Component/Console/Tests/ApplicationTest.php

+10-6
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,8 @@ public function testRenderAnonymousException()
897897
$application = new Application();
898898
$application->setAutoExit(false);
899899
$application->register('foo')->setCode(function () {
900-
throw new class('') extends \InvalidArgumentException { };
900+
throw new class('') extends \InvalidArgumentException {
901+
};
901902
});
902903
$tester = new ApplicationTester($application);
903904

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

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

918920
public function testRenderExceptionStackTraceContainsRootException()
919921
{
920922
$application = new Application();
921923
$application->setAutoExit(false);
922924
$application->register('foo')->setCode(function () {
923-
throw new class('') extends \InvalidArgumentException { };
925+
throw new class('') extends \InvalidArgumentException {
926+
};
924927
});
925928
$tester = new ApplicationTester($application);
926929

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

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

941945
public function testRun()

src/Symfony/Component/Console/composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"php": ">=7.1.3",
2020
"symfony/polyfill-mbstring": "~1.0",
2121
"symfony/polyfill-php73": "^1.8",
22+
"symfony/polyfill-php80": "^1.15",
2223
"symfony/service-contracts": "^1.1|^2"
2324
},
2425
"require-dev": {

src/Symfony/Component/Debug/ErrorHandler.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ public function handleError($type, $message, $file, $line)
415415
$context = $e;
416416
}
417417

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

542542
if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
543-
if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) {
543+
if (false !== strpos($message = $exception->getMessage(), "@anonymous\0")) {
544544
$message = (new FlattenException())->setMessage($message)->getMessage();
545545
}
546546
if ($exception instanceof FatalErrorException) {

src/Symfony/Component/Debug/Exception/FatalThrowableError.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class FatalThrowableError extends FatalErrorException
2626

2727
public function __construct(\Throwable $e)
2828
{
29-
$this->originalClassName = \get_class($e);
29+
$this->originalClassName = get_debug_type($e);
3030

3131
if ($e instanceof \ParseError) {
3232
$severity = E_PARSE;

src/Symfony/Component/Debug/Exception/FlattenException.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static function createFromThrowable(\Throwable $exception, int $statusCod
6767
$e->setStatusCode($statusCode);
6868
$e->setHeaders($headers);
6969
$e->setTraceFromThrowable($exception);
70-
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception));
70+
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : get_debug_type($exception));
7171
$e->setFile($exception->getFile());
7272
$e->setLine($exception->getLine());
7373

@@ -134,7 +134,7 @@ public function getClass()
134134
*/
135135
public function setClass($class)
136136
{
137-
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
137+
$this->class = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;
138138

139139
return $this;
140140
}
@@ -179,9 +179,9 @@ public function getMessage()
179179
*/
180180
public function setMessage($message)
181181
{
182-
if (false !== strpos($message, "class@anonymous\0")) {
183-
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
184-
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
182+
if (false !== strpos($message, "@anonymous\0")) {
183+
$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) {
184+
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
185185
}, $message);
186186
}
187187

src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php

+20
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,26 @@ public function testHandleUserError()
325325
}
326326
}
327327

328+
public function testHandleErrorWithAnonymousClass()
329+
{
330+
$handler = ErrorHandler::register();
331+
$handler->throwAt(E_USER_WARNING, true);
332+
try {
333+
$handler->handleError(E_USER_WARNING, 'foo '.\get_class(new class() extends \stdClass {
334+
}).' bar', 'foo.php', 12);
335+
$this->fail('Exception expected.');
336+
} catch (\ErrorException $e) {
337+
} finally {
338+
restore_error_handler();
339+
restore_exception_handler();
340+
}
341+
342+
$this->assertSame('User Warning: foo stdClass@anonymous bar', $e->getMessage());
343+
$this->assertSame(E_USER_WARNING, $e->getSeverity());
344+
$this->assertSame('foo.php', $e->getFile());
345+
$this->assertSame(12, $e->getLine());
346+
}
347+
328348
public function testHandleDeprecation()
329349
{
330350
$logArgCheck = function ($level, $message, $context) {

src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@ public function testAnonymousClass()
355355

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

358+
$flattened->setClass(\get_class(new class('Oops') extends NotFoundHttpException {
359+
}));
360+
361+
$this->assertSame('Symfony\Component\HttpKernel\Exception\NotFoundHttpException@anonymous', $flattened->getClass());
362+
358363
$flattened = FlattenException::create(new \Exception(sprintf('Class "%s" blah.', \get_class(new class() extends \RuntimeException {
359364
}))));
360365

src/Symfony/Component/Debug/composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
],
1818
"require": {
1919
"php": ">=7.1.3",
20-
"psr/log": "~1.0"
20+
"psr/log": "~1.0",
21+
"symfony/polyfill-php80": "^1.15"
2122
},
2223
"conflict": {
2324
"symfony/http-kernel": "<3.4"

src/Symfony/Component/ErrorHandler/DebugClassLoader.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
407407
}
408408
$deprecations = [];
409409

410-
$className = isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00") ? get_parent_class($class).'@anonymous' : $class;
410+
$className = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;
411411

412412
// Don't trigger deprecations for classes in the same vendor
413413
if ($class !== $className) {

src/Symfony/Component/ErrorHandler/ErrorHandler.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ public function handleError(int $type, string $message, string $file, int $line)
435435
$context = $e;
436436
}
437437

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

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

@@ -768,8 +768,8 @@ private function cleanTrace(array $backtrace, int $type, string $file, int $line
768768
*/
769769
private function parseAnonymousClass(string $message): string
770770
{
771-
return preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
772-
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
771+
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) {
772+
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
773773
}, $message);
774774
}
775775
}

src/Symfony/Component/ErrorHandler/Exception/FlattenException.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public static function createFromThrowable(\Throwable $exception, int $statusCod
7171
$e->setStatusCode($statusCode);
7272
$e->setHeaders($headers);
7373
$e->setTraceFromThrowable($exception);
74-
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception));
74+
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : get_debug_type($exception));
7575
$e->setFile($exception->getFile());
7676
$e->setLine($exception->getLine());
7777

@@ -138,7 +138,7 @@ public function getClass(): string
138138
*/
139139
public function setClass($class): self
140140
{
141-
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
141+
$this->class = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;
142142

143143
return $this;
144144
}
@@ -195,9 +195,9 @@ public function getMessage(): string
195195
*/
196196
public function setMessage($message): self
197197
{
198-
if (false !== strpos($message, "class@anonymous\0")) {
199-
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
200-
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
198+
if (false !== strpos($message, "@anonymous\0")) {
199+
$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) {
200+
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
201201
}, $message);
202202
}
203203

src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,26 @@ public function testHandleUserError()
365365
}
366366
}
367367

368+
public function testHandleErrorWithAnonymousClass()
369+
{
370+
$handler = ErrorHandler::register();
371+
$handler->throwAt(3, true);
372+
try {
373+
$handler->handleError(3, 'foo '.\get_class(new class() extends \stdClass {
374+
}).' bar', 'foo.php', 12);
375+
$this->fail('Exception expected.');
376+
} catch (\ErrorException $e) {
377+
} finally {
378+
restore_error_handler();
379+
restore_exception_handler();
380+
}
381+
382+
$this->assertSame('foo stdClass@anonymous bar', $e->getMessage());
383+
$this->assertSame(3, $e->getSeverity());
384+
$this->assertSame('foo.php', $e->getFile());
385+
$this->assertSame(12, $e->getLine());
386+
}
387+
368388
public function testHandleDeprecation()
369389
{
370390
$logArgCheck = function ($level, $message, $context) {
@@ -433,6 +453,10 @@ public function handleExceptionProvider(): array
433453
{
434454
return [
435455
['Uncaught Exception: foo', new \Exception('foo')],
456+
['Uncaught Exception: foo', new class('foo') extends \RuntimeException {
457+
}],
458+
['Uncaught Exception: foo stdClass@anonymous bar', new \RuntimeException('foo '.\get_class(new class() extends \stdClass {
459+
}).' bar')],
436460
['Uncaught Error: bar', new \Error('bar')],
437461
['Uncaught ccc', new \ErrorException('ccc')],
438462
];

src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,11 @@ public function testAnonymousClass()
373373

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

376+
$flattened->setClass(\get_class(new class('Oops') extends NotFoundHttpException {
377+
}));
378+
379+
$this->assertSame('Symfony\Component\HttpKernel\Exception\NotFoundHttpException@anonymous', $flattened->getClass());
380+
376381
$flattened = FlattenException::createFromThrowable(new \Exception(sprintf('Class "%s" blah.', \get_class(new class() extends \RuntimeException {
377382
}))));
378383

src/Symfony/Component/ErrorHandler/composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"php": ">=7.1.3",
2020
"psr/log": "~1.0",
2121
"symfony/debug": "^4.4.5",
22+
"symfony/polyfill-php80": "^1.15",
2223
"symfony/var-dumper": "^4.4|^5.0"
2324
},
2425
"require-dev": {

src/Symfony/Component/HttpKernel/Kernel.php

+2-5
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,7 @@ public function getBundles()
228228
public function getBundle($name)
229229
{
230230
if (!isset($this->bundles[$name])) {
231-
$class = static::class;
232-
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
233-
234-
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));
231+
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)));
235232
}
236233

237234
return $this->bundles[$name];
@@ -474,7 +471,7 @@ protected function build(ContainerBuilder $container)
474471
protected function getContainerClass()
475472
{
476473
$class = static::class;
477-
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class;
474+
$class = false !== strpos($class, "@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class;
478475
$class = $this->name.str_replace('\\', '_', $class).ucfirst($this->environment).($this->debug ? 'Debug' : '').'Container';
479476

480477
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) {

src/Symfony/Component/HttpKernel/Tests/KernelTest.php

+21
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,27 @@ public function testKernelStartTimeIsResetWhileBootingAlreadyBootedKernel()
640640
$this->assertGreaterThan($preReBoot, $kernel->getStartTime());
641641
}
642642

643+
public function testAnonymousKernelGeneratesValidContainerClass(): void
644+
{
645+
$kernel = new class('test', true) extends Kernel {
646+
public function registerBundles(): iterable
647+
{
648+
return [];
649+
}
650+
651+
public function registerContainerConfiguration(LoaderInterface $loader): void
652+
{
653+
}
654+
655+
public function getContainerClass(): string
656+
{
657+
return parent::getContainerClass();
658+
}
659+
};
660+
661+
$this->assertRegExp('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*TestDebugContainer$/', $kernel->getContainerClass());
662+
}
663+
643664
/**
644665
* Returns a mock for the BundleInterface.
645666
*/

src/Symfony/Component/HttpKernel/composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"symfony/http-foundation": "^4.4|^5.0",
2323
"symfony/polyfill-ctype": "^1.8",
2424
"symfony/polyfill-php73": "^1.9",
25+
"symfony/polyfill-php80": "^1.15",
2526
"psr/log": "~1.0"
2627
},
2728
"require-dev": {

src/Symfony/Component/Messenger/Middleware/TraceableMiddleware.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ public function next(): MiddlewareInterface
7878
if ($this->stack === $nextMiddleware = $this->stack->next()) {
7979
$this->currentEvent = 'Tail';
8080
} else {
81-
$class = \get_class($nextMiddleware);
82-
$this->currentEvent = sprintf('"%s"', 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class);
81+
$this->currentEvent = sprintf('"%s"', get_debug_type($nextMiddleware));
8382
}
8483
$this->currentEvent .= sprintf(' on "%s"', $this->busName);
8584

0 commit comments

Comments
 (0)