From ce65c06bb699fe31b43aaddfe9292c2b8f55e784 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Thu, 16 Jan 2020 09:38:58 +0100 Subject: [PATCH] Fix serialization issue of InvalidTag --- src/DocBlock/Tags/InvalidTag.php | 45 +++++++++++++++++ tests/unit/DocBlock/Tags/InvalidTagTest.php | 55 +++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/DocBlock/Tags/InvalidTag.php b/src/DocBlock/Tags/InvalidTag.php index 373d665..a3b68d4 100644 --- a/src/DocBlock/Tags/InvalidTag.php +++ b/src/DocBlock/Tags/InvalidTag.php @@ -4,7 +4,10 @@ declare(strict_types=1); namespace phpDocumentor\Reflection\DocBlock\Tags; +use Closure; use phpDocumentor\Reflection\DocBlock\Tag; +use ReflectionClass; +use ReflectionFunction; use Throwable; /** @@ -56,12 +59,54 @@ final class InvalidTag implements Tag public function withError(Throwable $exception) : self { + $this->flattenExceptionBacktrace($exception); $tag = new self($this->name, $this->body); $tag->throwable = $exception; return $tag; } + /** + * Removes all complex types from backtrace + * + * Not all objects are serializable. So we need to remove them from the + * stored exception to be sure that we do not break existing library usage. + */ + private function flattenExceptionBacktrace(Throwable $exception) : void + { + $traceProperty = (new ReflectionClass('Exception'))->getProperty('trace'); + $traceProperty->setAccessible(true); + + $flatten = static function (&$value) { + if ($value instanceof Closure) { + $closureReflection = new ReflectionFunction($value); + $value = sprintf( + '(Closure at %s:%s)', + $closureReflection->getFileName(), + $closureReflection->getStartLine() + ); + } elseif (is_object($value)) { + $value = sprintf('object(%s)', get_class($value)); + } elseif (is_resource($value)) { + $value = sprintf('resource(%s)', get_resource_type($value)); + } + }; + + do { + $trace = array_map( + static function($call) use ($flatten) { + array_walk_recursive($call['args'], $flatten); + + return $call; + }, + $exception->getTrace() + ); + $traceProperty->setValue($exception, $trace); + } while ($exception = $exception->getPrevious()); + + $traceProperty->setAccessible(false); + } + public function render(?Formatter $formatter = null) : string { if ($formatter === null) { diff --git a/tests/unit/DocBlock/Tags/InvalidTagTest.php b/tests/unit/DocBlock/Tags/InvalidTagTest.php index 2fc045b..c562ef3 100644 --- a/tests/unit/DocBlock/Tags/InvalidTagTest.php +++ b/tests/unit/DocBlock/Tags/InvalidTagTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace phpDocumentor\Reflection\DocBlock\Tags; use Exception; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -28,6 +29,7 @@ final class InvalidTagTest extends TestCase /** * @covers ::withError + * @covers ::__toString */ public function testCreationWithError() : void { @@ -36,6 +38,59 @@ final class InvalidTagTest extends TestCase self::assertSame('name', $tag->getName()); self::assertSame('@name Body', $tag->render()); + self::assertSame('Body', (string)$tag); self::assertSame($exception, $tag->getException()); } + + public function testCreationWithErrorContainingClosure() : void + { + try { + $this->throwExceptionFromClosureWithClosureArgument(); + } catch (Exception $e) { + $parentException = new Exception('test', 0, $e); + $tag = InvalidTag::create('Body', 'name')->withError($parentException); + self::assertSame('name', $tag->getName()); + self::assertSame('@name Body', $tag->render()); + self::assertSame($parentException, $tag->getException()); + self::assertStringStartsWith('(Closure at', $tag->getException()->getPrevious()->getTrace()[0]['args'][0]); + self::assertStringContainsString(__FILE__, $tag->getException()->getPrevious()->getTrace()[0]['args'][0]); + self::assertEquals($parentException, unserialize(serialize($parentException))); + } + } + + private function throwExceptionFromClosureWithClosureArgument() + { + $function = function() { + throw new InvalidArgumentException(); + }; + + $function($function); + } + + public function testCreationWithErrorContainingResource() + { + try { + $this->throwExceptionWithResourceArgument(); + } catch (Exception $e) { + $parentException = new Exception('test', 0, $e); + $tag = InvalidTag::create('Body', 'name')->withError($parentException); + self::assertSame('name', $tag->getName()); + self::assertSame('@name Body', $tag->render()); + self::assertSame($parentException, $tag->getException()); + self::assertStringStartsWith( + 'resource(stream)', + $tag->getException()->getPrevious()->getTrace()[0]['args'][0]) + ; + self::assertEquals($parentException, unserialize(serialize($parentException))); + } + } + + private function throwExceptionWithResourceArgument() + { + $function = function() { + throw new InvalidArgumentException(); + }; + + $function(fopen(__FILE__, 'r')); + } }