Completed
Pull Request — master (#381)
by Marco
06:15
created

testWillNotModifyRetrievedPublicProperties()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
rs 8.9713
cc 2
eloc 13
nc 2
nop 0
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
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
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
0 ignored issues
show
Unused Code introduced by
The parameter $returnEarly is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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