1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Config\Tests\Resource;
13
14use PHPUnit\Framework\TestCase;
15use Symfony\Component\Config\Resource\ReflectionClassResource;
16use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
18use Symfony\Contracts\Service\ServiceSubscriberInterface;
19
20class ReflectionClassResourceTest extends TestCase
21{
22    public function testToString()
23    {
24        $res = new ReflectionClassResource(new \ReflectionClass('ErrorException'));
25
26        $this->assertSame('reflection.ErrorException', (string) $res);
27    }
28
29    public function testSerializeUnserialize()
30    {
31        $res = new ReflectionClassResource(new \ReflectionClass(DummyInterface::class));
32        $ser = unserialize(serialize($res));
33
34        $this->assertTrue($res->isFresh(0));
35        $this->assertTrue($ser->isFresh(0));
36
37        $this->assertSame((string) $res, (string) $ser);
38    }
39
40    public function testIsFresh()
41    {
42        $res = new ReflectionClassResource(new \ReflectionClass(__CLASS__));
43        $mtime = filemtime(__FILE__);
44
45        $this->assertTrue($res->isFresh($mtime), '->isFresh() returns true if the resource has not changed in same second');
46        $this->assertTrue($res->isFresh($mtime + 10), '->isFresh() returns true if the resource has not changed');
47        $this->assertTrue($res->isFresh($mtime - 86400), '->isFresh() returns true if the resource has not changed');
48    }
49
50    public function testIsFreshForDeletedResources()
51    {
52        $now = time();
53        $tmp = sys_get_temp_dir().'/tmp.php';
54        file_put_contents($tmp, '<?php class ReflectionClassResourceTestClass {}');
55        require $tmp;
56
57        $res = new ReflectionClassResource(new \ReflectionClass('ReflectionClassResourceTestClass'));
58        $this->assertTrue($res->isFresh($now));
59
60        unlink($tmp);
61        $this->assertFalse($res->isFresh($now), '->isFresh() returns false if the resource does not exist');
62    }
63
64    /**
65     * @dataProvider provideHashedSignature
66     */
67    public function testHashedSignature($changeExpected, $changedLine, $changedCode, $setContext = null)
68    {
69        if ($setContext) {
70            $setContext();
71        }
72
73        $code = <<<'EOPHP'
74/* 0*/
75/* 1*/  class %s extends ErrorException
76/* 2*/  {
77/* 3*/      const FOO = 123;
78/* 4*/
79/* 5*/      public $pub = [];
80/* 6*/
81/* 7*/      protected $prot;
82/* 8*/
83/* 9*/      private $priv;
84/*10*/
85/*11*/      public function pub($arg = null) {}
86/*12*/
87/*13*/      protected function prot($a = []) {}
88/*14*/
89/*15*/      private function priv() {}
90/*16*/
91/*17*/      public function ccc($bar = A_CONSTANT_THAT_FOR_SURE_WILL_NEVER_BE_DEFINED_CCCCCC) {}
92/*18*/  }
93EOPHP;
94
95        static $expectedSignature, $generateSignature;
96
97        if (null === $expectedSignature) {
98            eval(sprintf($code, $class = 'Foo'.str_replace('.', '_', uniqid('', true))));
99            $r = new \ReflectionClass(ReflectionClassResource::class);
100            $generateSignature = $r->getMethod('generateSignature');
101            $generateSignature->setAccessible(true);
102            $generateSignature = $generateSignature->getClosure($r->newInstanceWithoutConstructor());
103            $expectedSignature = implode("\n", iterator_to_array($generateSignature(new \ReflectionClass($class))));
104        }
105
106        $code = explode("\n", $code);
107        if (null !== $changedCode) {
108            $code[$changedLine] = $changedCode;
109        }
110        eval(sprintf(implode("\n", $code), $class = 'Foo'.str_replace('.', '_', uniqid('', true))));
111        $signature = implode("\n", iterator_to_array($generateSignature(new \ReflectionClass($class))));
112
113        if ($changeExpected) {
114            $this->assertNotSame($expectedSignature, $signature);
115        } else {
116            $this->assertSame($expectedSignature, $signature);
117        }
118    }
119
120    public function provideHashedSignature()
121    {
122        yield [0, 0, "// line change\n\n"];
123        yield [1, 0, '/** class docblock */'];
124        yield [1, 1, 'abstract class %s'];
125        yield [1, 1, 'final class %s'];
126        yield [1, 1, 'class %s extends Exception'];
127        yield [1, 1, 'class %s implements '.DummyInterface::class];
128        yield [1, 3, 'const FOO = 456;'];
129        yield [1, 3, 'const BAR = 123;'];
130        yield [1, 4, '/** pub docblock */'];
131        yield [1, 5, 'protected $pub = [];'];
132        yield [1, 5, 'public $pub = [123];'];
133        yield [1, 6, '/** prot docblock */'];
134        yield [1, 7, 'private $prot;'];
135        yield [0, 8, '/** priv docblock */'];
136        yield [0, 9, 'private $priv = 123;'];
137        yield [1, 10, '/** pub docblock */'];
138        yield [1, 11, 'public function pub(...$arg) {}'];
139        yield [1, 11, 'public function pub($arg = null): Foo {}'];
140        yield [0, 11, "public function pub(\$arg = null) {\nreturn 123;\n}"];
141        yield [1, 12, '/** prot docblock */'];
142        yield [1, 13, 'protected function prot($a = [123]) {}'];
143        yield [0, 14, '/** priv docblock */'];
144        yield [0, 15, ''];
145
146        if (\PHP_VERSION_ID >= 70400) {
147            // PHP7.4 typed properties without default value are
148            // undefined, make sure this doesn't throw an error
149            yield [1, 5, 'public array $pub;'];
150            yield [0, 7, 'protected int $prot;'];
151            yield [0, 9, 'private string $priv;'];
152        }
153
154        yield [1, 17, 'public function ccc($bar = 187) {}'];
155        yield [1, 17, 'public function ccc($bar = ANOTHER_ONE_THAT_WILL_NEVER_BE_DEFINED_CCCCCCCCC) {}'];
156        yield [1, 17, null, static function () { \define('A_CONSTANT_THAT_FOR_SURE_WILL_NEVER_BE_DEFINED_CCCCCC', 'foo'); }];
157    }
158
159    public function testEventSubscriber()
160    {
161        $res = new ReflectionClassResource(new \ReflectionClass(TestEventSubscriber::class));
162        $this->assertTrue($res->isFresh(0));
163
164        TestEventSubscriber::$subscribedEvents = [123];
165        $this->assertFalse($res->isFresh(0));
166
167        $res = new ReflectionClassResource(new \ReflectionClass(TestEventSubscriber::class));
168        $this->assertTrue($res->isFresh(0));
169    }
170
171    public function testMessageSubscriber()
172    {
173        $res = new ReflectionClassResource(new \ReflectionClass(TestMessageSubscriber::class));
174        $this->assertTrue($res->isFresh(0));
175
176        TestMessageSubscriberConfigHolder::$handledMessages = ['SomeMessageClass' => []];
177        $this->assertFalse($res->isFresh(0));
178
179        $res = new ReflectionClassResource(new \ReflectionClass(TestMessageSubscriber::class));
180        $this->assertTrue($res->isFresh(0));
181
182        TestMessageSubscriberConfigHolder::$handledMessages = ['OtherMessageClass' => []];
183        $this->assertFalse($res->isFresh(0));
184
185        $res = new ReflectionClassResource(new \ReflectionClass(TestMessageSubscriber::class));
186        $this->assertTrue($res->isFresh(0));
187    }
188
189    public function testServiceSubscriber()
190    {
191        $res = new ReflectionClassResource(new \ReflectionClass(TestServiceSubscriber::class));
192        $this->assertTrue($res->isFresh(0));
193
194        TestServiceSubscriber::$subscribedServices = [123];
195        $this->assertFalse($res->isFresh(0));
196
197        $res = new ReflectionClassResource(new \ReflectionClass(TestServiceSubscriber::class));
198        $this->assertTrue($res->isFresh(0));
199    }
200
201    public function testIgnoresObjectsInSignature()
202    {
203        $res = new ReflectionClassResource(new \ReflectionClass(TestServiceWithStaticProperty::class));
204        $this->assertTrue($res->isFresh(0));
205
206        TestServiceWithStaticProperty::$initializedObject = new TestServiceWithStaticProperty();
207        $this->assertTrue($res->isFresh(0));
208    }
209}
210
211interface DummyInterface
212{
213}
214
215class TestEventSubscriber implements EventSubscriberInterface
216{
217    public static $subscribedEvents = [];
218
219    public static function getSubscribedEvents(): array
220    {
221        return self::$subscribedEvents;
222    }
223}
224
225class TestMessageSubscriber implements MessageSubscriberInterface
226{
227    public static function getHandledMessages(): iterable
228    {
229        foreach (TestMessageSubscriberConfigHolder::$handledMessages as $key => $subscribedMessage) {
230            yield $key => $subscribedMessage;
231        }
232    }
233}
234class TestMessageSubscriberConfigHolder
235{
236    public static $handledMessages = [];
237}
238
239class TestServiceSubscriber implements ServiceSubscriberInterface
240{
241    public static $subscribedServices = [];
242
243    public static function getSubscribedServices(): array
244    {
245        return self::$subscribedServices;
246    }
247}
248
249class TestServiceWithStaticProperty
250{
251    public static $initializedObject;
252}
253