Completed
Push — master ( 9f8061...f01068 )
by Marco
13s
created

AccessInterceptorScopeLocalizerFunctionalTest.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace ProxyManagerTest\Functional;
6
7
use PHPUnit\Framework\ExpectationFailedException;
8
use PHPUnit\Framework\TestCase;
9
use ProxyManager\Configuration;
10
use ProxyManager\Exception\UnsupportedProxiedClassException;
11
use ProxyManager\Factory\AccessInterceptorScopeLocalizerFactory;
12
use ProxyManager\Generator\ClassGenerator;
13
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
14
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
15
use ProxyManager\Proxy\AccessInterceptorInterface;
16
use ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator;
17
use ProxyManager\ProxyGenerator\Util\Properties;
18
use ProxyManagerTestAsset\BaseClass;
19
use ProxyManagerTestAsset\ClassWithCounterConstructor;
20
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
21
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
22
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
23
use ProxyManagerTestAsset\ClassWithParentHint;
24
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
25
use ProxyManagerTestAsset\ClassWithPublicProperties;
26
use ProxyManagerTestAsset\ClassWithSelfHint;
27
use ProxyManagerTestAsset\EmptyClass;
28
use ProxyManagerTestAsset\VoidCounter;
29
use ReflectionClass;
30
use stdClass;
31
32
/**
33
 * Tests for {@see \ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator} produced objects
34
 *
35
 * @author Marco Pivetta <[email protected]>
36
 * @license MIT
37
 *
38
 * @group Functional
39
 * @coversNothing
40
 */
41
class AccessInterceptorScopeLocalizerFunctionalTest extends TestCase
42
{
43
    /**
44
     * @dataProvider getProxyMethods
45
     *
46
     * @param string  $className
47
     * @param object  $instance
48
     * @param string  $method
49
     * @param mixed[] $params
50
     * @param mixed   $expectedValue
51
     */
52
    public function testMethodCalls(string $className, $instance, string $method, array $params, $expectedValue) : void
53
    {
54
        $proxyName = $this->generateProxy($className);
55
56
        /* @var $proxy AccessInterceptorInterface */
57
        $proxy     = $proxyName::staticProxyConstructor($instance);
58
59
        $this->assertProxySynchronized($instance, $proxy);
60
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
61
62
        /* @var $listener callable|\PHPUnit_Framework_MockObject_MockObject */
63
        $listener = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
64
        $listener
0 ignored issues
show
The method expects cannot be called on $listener (of type callable).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
65
            ->expects(self::once())
66
            ->method('__invoke')
67
            ->with($proxy, $proxy, $method, $params, false);
68
69
        $proxy->setMethodPrefixInterceptor(
70
            $method,
71
            function ($proxy, $instance, $method, $params, & $returnEarly) use ($listener) {
72
                $listener($proxy, $instance, $method, $params, $returnEarly);
73
            }
74
        );
75
76
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
77
78
        $random = uniqid('', true);
79
80
        $proxy->setMethodPrefixInterceptor(
81
            $method,
82
            function ($proxy, $instance, string $method, $params, & $returnEarly) use ($random) : string {
83
                $returnEarly = true;
84
85
                return $random;
86
            }
87
        );
88
89
        self::assertSame($random, call_user_func_array([$proxy, $method], $params));
90
        $this->assertProxySynchronized($instance, $proxy);
91
    }
92
93
    /**
94
     * @dataProvider getProxyMethods
95
     *
96
     * @param string  $className
97
     * @param object  $instance
98
     * @param string  $method
99
     * @param mixed[] $params
100
     * @param mixed   $expectedValue
101
     */
102
    public function testMethodCallsWithSuffixListener(
103
        string $className,
104
        $instance,
105
        string $method,
106
        array $params,
107
        $expectedValue
108
    ) : void {
109
        $proxyName = $this->generateProxy($className);
110
111
        /* @var $proxy AccessInterceptorInterface */
112
        $proxy     = $proxyName::staticProxyConstructor($instance);
113
        /* @var $listener callable|\PHPUnit_Framework_MockObject_MockObject */
114
        $listener  = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
115
        $listener
0 ignored issues
show
The method expects cannot be called on $listener (of type callable).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
116
            ->expects(self::once())
117
            ->method('__invoke')
118
            ->with($proxy, $proxy, $method, $params, $expectedValue, false);
119
120
        $proxy->setMethodSuffixInterceptor(
121
            $method,
122
            function ($proxy, $instance, $method, $params, $returnValue, & $returnEarly) use ($listener) {
123
                $listener($proxy, $instance, $method, $params, $returnValue, $returnEarly);
124
            }
125
        );
126
127
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
128
129
        $random = uniqid('', true);
130
131
        $proxy->setMethodSuffixInterceptor(
132
            $method,
133
            function ($proxy, $instance, $method, $params, $returnValue, & $returnEarly) use ($random) : string {
134
                $returnEarly = true;
135
136
                return $random;
137
            }
138
        );
139
140
        self::assertSame($random, call_user_func_array([$proxy, $method], $params));
141
        $this->assertProxySynchronized($instance, $proxy);
142
    }
143
144
    /**
145
     * @dataProvider getProxyMethods
146
     *
147
     * @param string  $className
148
     * @param object  $instance
149
     * @param string  $method
150
     * @param mixed[] $params
151
     * @param mixed   $expectedValue
152
     */
153
    public function testMethodCallsAfterUnSerialization(
154
        string $className,
155
        $instance,
156
        string $method,
157
        array $params,
158
        $expectedValue
159
    ) : void {
160
        $proxyName = $this->generateProxy($className);
161
        /* @var $proxy AccessInterceptorInterface */
162
        $proxy     = unserialize(serialize($proxyName::staticProxyConstructor($instance)));
163
164
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
165
        $this->assertProxySynchronized($instance, $proxy);
166
    }
167
168
    /**
169
     * @dataProvider getProxyMethods
170
     *
171
     * @param string  $className
172
     * @param object  $instance
173
     * @param string  $method
174
     * @param mixed[] $params
175
     * @param mixed   $expectedValue
176
     */
177
    public function testMethodCallsAfterCloning(
178
        string $className,
179
        $instance,
180
        string $method,
181
        array $params,
182
        $expectedValue
183
    ) : void {
184
        $proxyName = $this->generateProxy($className);
185
186
        /* @var $proxy AccessInterceptorInterface */
187
        $proxy     = $proxyName::staticProxyConstructor($instance);
188
        $cloned    = clone $proxy;
189
190
        $this->assertProxySynchronized($instance, $proxy);
191
        self::assertSame($expectedValue, call_user_func_array([$cloned, $method], $params));
192
        $this->assertProxySynchronized($instance, $proxy);
193
    }
194
195
    /**
196
     * @dataProvider getPropertyAccessProxies
197
     *
198
     * @param object                     $instance
199
     * @param AccessInterceptorInterface $proxy
200
     * @param string                     $publicProperty
201
     * @param mixed                      $propertyValue
202
     */
203
    public function testPropertyReadAccess(
204
        $instance,
205
        AccessInterceptorInterface $proxy,
206
        string $publicProperty,
207
        $propertyValue
208
    ) : void {
209
        self::assertSame($propertyValue, $proxy->$publicProperty);
210
        $this->assertProxySynchronized($instance, $proxy);
211
    }
212
213
    /**
214
     * @dataProvider getPropertyAccessProxies
215
     *
216
     * @param object                     $instance
217
     * @param AccessInterceptorInterface $proxy
218
     * @param string                     $publicProperty
219
     */
220
    public function testPropertyWriteAccess($instance, AccessInterceptorInterface $proxy, string $publicProperty) : void
221
    {
222
        $newValue               = uniqid();
223
        $proxy->$publicProperty = $newValue;
224
225
        self::assertSame($newValue, $proxy->$publicProperty);
226
        $this->assertProxySynchronized($instance, $proxy);
227
    }
228
229
    /**
230
     * @dataProvider getPropertyAccessProxies
231
     *
232
     * @param object                     $instance
233
     * @param AccessInterceptorInterface $proxy
234
     * @param string                     $publicProperty
235
     */
236
    public function testPropertyExistence($instance, AccessInterceptorInterface $proxy, string $publicProperty) : void
237
    {
238
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
239
        $this->assertProxySynchronized($instance, $proxy);
240
241
        $instance->$publicProperty = null;
242
        self::assertFalse(isset($proxy->$publicProperty));
243
        $this->assertProxySynchronized($instance, $proxy);
244
    }
245
246
    /**
247
     * @dataProvider getPropertyAccessProxies
248
     *
249
     * @param object                     $instance
250
     * @param AccessInterceptorInterface $proxy
251
     * @param string                     $publicProperty
252
     */
253
    public function testPropertyUnset($instance, AccessInterceptorInterface $proxy, string $publicProperty) : void
254
    {
255
        self::markTestSkipped('It is currently not possible to synchronize properties un-setting');
256
        unset($proxy->$publicProperty);
257
258
        self::assertFalse(isset($instance->$publicProperty));
259
        self::assertFalse(isset($proxy->$publicProperty));
260
        $this->assertProxySynchronized($instance, $proxy);
261
    }
262
263
    /**
264
     * Verifies that accessing a public property containing an array behaves like in a normal context
265
     */
266
    public function testCanWriteToArrayKeysInPublicProperty() : void
267
    {
268
        $instance    = new ClassWithPublicArrayProperty();
269
        $className   = get_class($instance);
270
        $proxyName   = $this->generateProxy($className);
271
        /* @var $proxy ClassWithPublicArrayProperty|AccessInterceptorInterface */
272
        $proxy       = $proxyName::staticProxyConstructor($instance);
273
274
        $proxy->arrayProperty['foo'] = 'bar';
275
276
        self::assertSame('bar', $proxy->arrayProperty['foo']);
277
278
        $proxy->arrayProperty = ['tab' => 'taz'];
279
280
        self::assertSame(['tab' => 'taz'], $proxy->arrayProperty);
281
        self::assertInstanceOf(AccessInterceptorInterface::class, $proxy);
282
283
        if ($proxy instanceof AccessInterceptorInterface) {
284
            // @TODO use intersection types when available - ref https://twitter.com/Ocramius/status/931252644190015489
285
            $this->assertProxySynchronized($instance, $proxy);
286
        }
287
    }
288
289
    /**
290
     * Verifies that public properties retrieved via `__get` don't get modified in the object state
291
     */
292
    public function testWillNotModifyRetrievedPublicProperties() : void
293
    {
294
        $instance    = new ClassWithPublicProperties();
295
        $className   = get_class($instance);
296
        $proxyName   = $this->generateProxy($className);
297
        /* @var $proxy ClassWithPublicProperties|AccessInterceptorInterface */
298
        $proxy       = $proxyName::staticProxyConstructor($instance);
299
        $variable    = $proxy->property0;
300
301
        self::assertSame('property0', $variable);
302
303
        $variable = 'foo';
304
305
        self::assertSame('property0', $proxy->property0);
306
307
        self::assertInstanceOf(AccessInterceptorInterface::class, $proxy);
308
309
        if ($proxy instanceof AccessInterceptorInterface) {
310
            // @TODO use intersection types when available - ref https://twitter.com/Ocramius/status/931252644190015489
311
            $this->assertProxySynchronized($instance, $proxy);
312
        }
313
314
        self::assertSame('foo', $variable);
315
    }
316
317
318
    /**
319
     * Verifies that public properties references retrieved via `__get` modify in the object state
320
     */
321
    public function testWillModifyByRefRetrievedPublicProperties() : void
322
    {
323
        $instance    = new ClassWithPublicProperties();
324
        $proxyName   = $this->generateProxy(get_class($instance));
325
        /* @var $proxy ClassWithPublicProperties|AccessInterceptorInterface */
326
        $proxy       = $proxyName::staticProxyConstructor($instance);
327
        $variable    = & $proxy->property0;
328
329
        self::assertSame('property0', $variable);
330
331
        $variable = 'foo';
332
333
        self::assertSame('foo', $proxy->property0);
334
335
        self::assertInstanceOf(AccessInterceptorInterface::class, $proxy);
336
337
        if ($proxy instanceof AccessInterceptorInterface) {
338
            // @TODO use intersection types when available - ref https://twitter.com/Ocramius/status/931252644190015489
339
            $this->assertProxySynchronized($instance, $proxy);
340
        }
341
342
        self::assertSame('foo', $variable);
343
    }
344
345
    /**
346
     * @group 115
347
     * @group 175
348
     */
349
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
350
    {
351
        $instance = new ClassWithCounterConstructor(10);
352
353
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
354
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
355
        $instance->__construct(3);
356
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
357
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
358
359
        $proxyName = $this->generateProxy(get_class($instance));
360
361
        /* @var $proxy ClassWithCounterConstructor */
362
        $proxy = new $proxyName(15);
363
364
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
365
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
366
        $proxy->__construct(5);
367
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
368
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
369
    }
370
371
    /**
372
     * Generates a proxy for the given class name, and retrieves its class name
373
     *
374
     * @throws UnsupportedProxiedClassException
375
     */
376
    private function generateProxy(string $parentClassName) : string
377
    {
378
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
379
        $generator          = new AccessInterceptorScopeLocalizerGenerator();
380
        $generatedClass     = new ClassGenerator($generatedClassName);
381
        $strategy           = new EvaluatingGeneratorStrategy();
382
383
        $generator->generate(new ReflectionClass($parentClassName), $generatedClass);
384
        $strategy->generate($generatedClass);
385
386
        return $generatedClassName;
387
    }
388
389
    /**
390
     * Generates a list of object | invoked method | parameters | expected result
391
     *
392
     * @return array
393
     */
394
    public function getProxyMethods() : array
395
    {
396
        $selfHintParam = new ClassWithSelfHint();
397
        $empty         = new EmptyClass();
398
399
        return [
400
            [
401
                BaseClass::class,
402
                new BaseClass(),
403
                'publicMethod',
404
                [],
405
                'publicMethodDefault'
406
            ],
407
            [
408
                BaseClass::class,
409
                new BaseClass(),
410
                'publicTypeHintedMethod',
411
                ['param' => new stdClass()],
412
                'publicTypeHintedMethodDefault'
413
            ],
414
            [
415
                BaseClass::class,
416
                new BaseClass(),
417
                'publicByReferenceMethod',
418
                [],
419
                'publicByReferenceMethodDefault'
420
            ],
421
            [
422
                ClassWithSelfHint::class,
423
                new ClassWithSelfHint(),
424
                'selfHintMethod',
425
                ['parameter' => $selfHintParam],
426
                $selfHintParam
427
            ],
428
            [
429
                ClassWithParentHint::class,
430
                new ClassWithParentHint(),
431
                'parentHintMethod',
432
                ['parameter' => $empty],
433
                $empty
434
            ],
435
        ];
436
    }
437
438
    /**
439
     * Generates proxies and instances with a public property to feed to the property accessor methods
440
     */
441
    public function getPropertyAccessProxies() : array
442
    {
443
        $instance1  = new BaseClass();
444
        $proxyName1 = $this->generateProxy(get_class($instance1));
445
446
        return [
447
            [
448
                $instance1,
449
                $proxyName1::staticProxyConstructor($instance1),
450
                'publicProperty',
451
                'publicPropertyDefault',
452
            ],
453
        ];
454
    }
455
456
    /**
457
     * @param object                     $instance
458
     * @param AccessInterceptorInterface $proxy
459
     */
460
    private function assertProxySynchronized($instance, AccessInterceptorInterface $proxy)
461
    {
462
        $reflectionClass = new ReflectionClass($instance);
463
464
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
465
            $property->setAccessible(true);
466
467
            self::assertSame(
468
                $property->getValue($instance),
469
                $property->getValue($proxy),
470
                'Property "' . $property->getName() . '" is synchronized between instance and proxy'
471
            );
472
        }
473
    }
474
475
    public function testWillForwardVariadicArguments() : void
476
    {
477
        $configuration = new Configuration();
478
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
479
        $targetObject  = new ClassWithMethodWithVariadicFunction();
480
481
        /* @var $object ClassWithMethodWithVariadicFunction */
482
        $object = $factory->createProxy(
483
            $targetObject,
484
            [
485
                function () : string {
486
                    return 'Foo Baz';
487
                },
488
            ]
489
        );
490
491
        self::assertNull($object->bar);
492
        self::assertNull($object->baz);
493
494
        $object->foo('Ocramius', 'Malukenho', 'Danizord');
495
        self::assertSame('Ocramius', $object->bar);
496
        self::assertSame(['Malukenho', 'Danizord'], $object->baz);
497
    }
498
499
    /**
500
     * @group 265
501
     */
502
    public function testWillForwardVariadicByRefArguments() : void
503
    {
504
        $configuration = new Configuration();
505
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
506
        $targetObject  = new ClassWithMethodWithByRefVariadicFunction();
507
508
        /* @var $object ClassWithMethodWithByRefVariadicFunction */
509
        $object = $factory->createProxy(
510
            $targetObject,
511
            [
512
                function () : string {
513
                    return 'Foo Baz';
514
                },
515
            ]
516
        );
517
518
        $parameters = ['a', 'b', 'c'];
519
520
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
521
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
522
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
523
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
524
    }
525
526
    /**
527
     * This test documents a known limitation: `func_get_args()` (and similar) don't work in proxied APIs.
528
     * If you manage to make this test pass, then please do send a patch
529
     *
530
     * @group 265
531
     */
532
    public function testWillNotForwardDynamicArguments() : void
533
    {
534
        /* @var $object ClassWithDynamicArgumentsMethod */
535
        $object = (new AccessInterceptorScopeLocalizerFactory())
536
            ->createProxy(
537
                new ClassWithDynamicArgumentsMethod(),
538
                [
539
                    'dynamicArgumentsMethod' => function () : string {
540
                        return 'Foo Baz';
541
                    },
542
                ]
543
            );
544
545
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
546
547
        $this->expectException(ExpectationFailedException::class);
548
549
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
550
    }
551
552
    /**
553
     * @group 327
554
     */
555
    public function testWillInterceptAndReturnEarlyOnVoidMethod() : void
556
    {
557
        $skip      = random_int(100, 200);
558
        $addMore   = random_int(201, 300);
559
        $increment = random_int(301, 400);
560
561
        /* @var $object VoidCounter */
562
        $object = (new AccessInterceptorScopeLocalizerFactory())
563
            ->createProxy(
564
                new VoidCounter(),
565
                [
566
                    'increment' => function (
567
                        VoidCounter $proxy,
568
                        VoidCounter $instance,
569
                        string $method,
570
                        array $params,
571
                        ?bool & $returnEarly
572
                    ) use ($skip) : void {
573
                        if ($skip === $params['amount']) {
574
                            $returnEarly = true;
575
                        }
576
                    },
577
                ],
578
                [
579
                    'increment' => function (
580
                        VoidCounter $proxy,
581
                        VoidCounter $instance,
582
                        string $method,
583
                        array $params,
584
                        ?bool & $returnEarly
585
                    ) use ($addMore) : void {
586
                        if ($addMore === $params['amount']) {
587
                            $instance->counter += 1;
588
                        }
589
                    },
590
                ]
591
            );
592
593
        $object->increment($skip);
594
        self::assertSame(0, $object->counter);
595
596
        $object->increment($increment);
597
        self::assertSame($increment, $object->counter);
598
599
        $object->increment($addMore);
600
        self::assertSame($increment + $addMore + 1, $object->counter);
601
    }
602
}
603