Skip to content

Commit ce65c06

Browse files
committed
Fix serialization issue of InvalidTag
1 parent f256f0b commit ce65c06

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

src/DocBlock/Tags/InvalidTag.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace phpDocumentor\Reflection\DocBlock\Tags;
66

7+
use Closure;
78
use phpDocumentor\Reflection\DocBlock\Tag;
9+
use ReflectionClass;
10+
use ReflectionFunction;
811
use Throwable;
912

1013
/**
@@ -56,12 +59,54 @@ public static function create(string $body, string $name = '')
5659

5760
public function withError(Throwable $exception) : self
5861
{
62+
$this->flattenExceptionBacktrace($exception);
5963
$tag = new self($this->name, $this->body);
6064
$tag->throwable = $exception;
6165

6266
return $tag;
6367
}
6468

69+
/**
70+
* Removes all complex types from backtrace
71+
*
72+
* Not all objects are serializable. So we need to remove them from the
73+
* stored exception to be sure that we do not break existing library usage.
74+
*/
75+
private function flattenExceptionBacktrace(Throwable $exception) : void
76+
{
77+
$traceProperty = (new ReflectionClass('Exception'))->getProperty('trace');
78+
$traceProperty->setAccessible(true);
79+
80+
$flatten = static function (&$value) {
81+
if ($value instanceof Closure) {
82+
$closureReflection = new ReflectionFunction($value);
83+
$value = sprintf(
84+
'(Closure at %s:%s)',
85+
$closureReflection->getFileName(),
86+
$closureReflection->getStartLine()
87+
);
88+
} elseif (is_object($value)) {
89+
$value = sprintf('object(%s)', get_class($value));
90+
} elseif (is_resource($value)) {
91+
$value = sprintf('resource(%s)', get_resource_type($value));
92+
}
93+
};
94+
95+
do {
96+
$trace = array_map(
97+
static function($call) use ($flatten) {
98+
array_walk_recursive($call['args'], $flatten);
99+
100+
return $call;
101+
},
102+
$exception->getTrace()
103+
);
104+
$traceProperty->setValue($exception, $trace);
105+
} while ($exception = $exception->getPrevious());
106+
107+
$traceProperty->setAccessible(false);
108+
}
109+
65110
public function render(?Formatter $formatter = null) : string
66111
{
67112
if ($formatter === null) {

tests/unit/DocBlock/Tags/InvalidTagTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace phpDocumentor\Reflection\DocBlock\Tags;
66

77
use Exception;
8+
use InvalidArgumentException;
89
use PHPUnit\Framework\TestCase;
910

1011
/**
@@ -28,6 +29,7 @@ public function testCreationWithoutError() : void
2829

2930
/**
3031
* @covers ::withError
32+
* @covers ::__toString
3133
*/
3234
public function testCreationWithError() : void
3335
{
@@ -36,6 +38,59 @@ public function testCreationWithError() : void
3638

3739
self::assertSame('name', $tag->getName());
3840
self::assertSame('@name Body', $tag->render());
41+
self::assertSame('Body', (string)$tag);
3942
self::assertSame($exception, $tag->getException());
4043
}
44+
45+
public function testCreationWithErrorContainingClosure() : void
46+
{
47+
try {
48+
$this->throwExceptionFromClosureWithClosureArgument();
49+
} catch (Exception $e) {
50+
$parentException = new Exception('test', 0, $e);
51+
$tag = InvalidTag::create('Body', 'name')->withError($parentException);
52+
self::assertSame('name', $tag->getName());
53+
self::assertSame('@name Body', $tag->render());
54+
self::assertSame($parentException, $tag->getException());
55+
self::assertStringStartsWith('(Closure at', $tag->getException()->getPrevious()->getTrace()[0]['args'][0]);
56+
self::assertStringContainsString(__FILE__, $tag->getException()->getPrevious()->getTrace()[0]['args'][0]);
57+
self::assertEquals($parentException, unserialize(serialize($parentException)));
58+
}
59+
}
60+
61+
private function throwExceptionFromClosureWithClosureArgument()
62+
{
63+
$function = function() {
64+
throw new InvalidArgumentException();
65+
};
66+
67+
$function($function);
68+
}
69+
70+
public function testCreationWithErrorContainingResource()
71+
{
72+
try {
73+
$this->throwExceptionWithResourceArgument();
74+
} catch (Exception $e) {
75+
$parentException = new Exception('test', 0, $e);
76+
$tag = InvalidTag::create('Body', 'name')->withError($parentException);
77+
self::assertSame('name', $tag->getName());
78+
self::assertSame('@name Body', $tag->render());
79+
self::assertSame($parentException, $tag->getException());
80+
self::assertStringStartsWith(
81+
'resource(stream)',
82+
$tag->getException()->getPrevious()->getTrace()[0]['args'][0])
83+
;
84+
self::assertEquals($parentException, unserialize(serialize($parentException)));
85+
}
86+
}
87+
88+
private function throwExceptionWithResourceArgument()
89+
{
90+
$function = function() {
91+
throw new InvalidArgumentException();
92+
};
93+
94+
$function(fopen(__FILE__, 'r'));
95+
}
4196
}

0 commit comments

Comments
 (0)