Completed
Pull Request — master (#359)
by Jefersson
06:36
created

testPropertyUnset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 3
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license.
17
 */
18
19
declare(strict_types=1);
20
21
namespace ProxyManagerTest\Functional;
22
23
use PHPUnit_Framework_TestCase;
24
use ProxyManager\Configuration;
25
use ProxyManager\Exception\UnsupportedProxiedClassException;
26
use ProxyManager\Factory\AccessInterceptorScopeLocalizerFactory;
27
use ProxyManager\Generator\ClassGenerator;
28
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
29
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
30
use ProxyManager\Proxy\AccessInterceptorInterface;
31
use ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator;
32
use ProxyManager\ProxyGenerator\Util\Properties;
33
use ProxyManagerTestAsset\BaseClass;
34
use ProxyManagerTestAsset\ClassWithCounterConstructor;
35
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
36
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
37
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
38
use ProxyManagerTestAsset\ClassWithParentHint;
39
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
40
use ProxyManagerTestAsset\ClassWithPublicProperties;
41
use ProxyManagerTestAsset\ClassWithSelfHint;
42
use ProxyManagerTestAsset\EmptyClass;
43
use ProxyManagerTestAsset\VoidCounter;
44
use ReflectionClass;
45
use stdClass;
46
47
/**
48
 * Tests for {@see \ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator} produced objects
49
 *
50
 * @author Marco Pivetta <[email protected]>
51
 * @license MIT
52
 *
53
 * @group Functional
54
 * @coversNothing
55
 */
56
class AccessInterceptorScopeLocalizerFunctionalTest extends PHPUnit_Framework_TestCase
57
{
58
    /**
59
     * @dataProvider getProxyMethods
60
     *
61
     * @param string  $className
62
     * @param object  $instance
63
     * @param string  $method
64
     * @param mixed[] $params
65
     * @param mixed   $expectedValue
66
     */
67
    public function testMethodCalls(string $className, $instance, string $method, array $params, $expectedValue) : void
68
    {
69
        $proxyName = $this->generateProxy($className);
70
71
        /* @var $proxy AccessInterceptorInterface */
72
        $proxy     = $proxyName::staticProxyConstructor($instance);
73
74
        $this->assertProxySynchronized($instance, $proxy);
75
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
76
77
        /* @var $listener callable|\PHPUnit_Framework_MockObject_MockObject */
78
        $listener = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
79
        $listener
80
            ->expects(self::once())
81
            ->method('__invoke')
82
            ->with($proxy, $proxy, $method, $params, false);
83
84
        $proxy->setMethodPrefixInterceptor(
85
            $method,
86
            function ($proxy, $instance, $method, $params, & $returnEarly) use ($listener) {
87
                $listener($proxy, $instance, $method, $params, $returnEarly);
88
            }
89
        );
90
91
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
92
93
        $random = uniqid('', true);
94
95
        $proxy->setMethodPrefixInterceptor(
96
            $method,
97
            function ($proxy, $instance, string $method, $params, & $returnEarly) use ($random) : string {
98
                $returnEarly = true;
99
100
                return $random;
101
            }
102
        );
103
104
        self::assertSame($random, call_user_func_array([$proxy, $method], $params));
105
        $this->assertProxySynchronized($instance, $proxy);
106
    }
107
108
    /**
109
     * @dataProvider getProxyMethods
110
     *
111
     * @param string  $className
112
     * @param object  $instance
113
     * @param string  $method
114
     * @param mixed[] $params
115
     * @param mixed   $expectedValue
116
     */
117
    public function testMethodCallsWithSuffixListener(
118
        string $className,
119
        $instance,
120
        string $method,
121
        array $params,
122
        $expectedValue
123
    ) : void {
124
        $proxyName = $this->generateProxy($className);
125
126
        /* @var $proxy AccessInterceptorInterface */
127
        $proxy     = $proxyName::staticProxyConstructor($instance);
128
        /* @var $listener callable|\PHPUnit_Framework_MockObject_MockObject */
129
        $listener  = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
130
        $listener
131
            ->expects(self::once())
132
            ->method('__invoke')
133
            ->with($proxy, $proxy, $method, $params, $expectedValue, false);
134
135
        $proxy->setMethodSuffixInterceptor(
136
            $method,
137
            function ($proxy, $instance, $method, $params, $returnValue, & $returnEarly) use ($listener) {
138
                $listener($proxy, $instance, $method, $params, $returnValue, $returnEarly);
139
            }
140
        );
141
142
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
143
144
        $random = uniqid('', true);
145
146
        $proxy->setMethodSuffixInterceptor(
147
            $method,
148
            function ($proxy, $instance, $method, $params, $returnValue, & $returnEarly) use ($random) : string {
149
                $returnEarly = true;
150
151
                return $random;
152
            }
153
        );
154
155
        self::assertSame($random, call_user_func_array([$proxy, $method], $params));
156
        $this->assertProxySynchronized($instance, $proxy);
157
    }
158
159
    /**
160
     * @dataProvider getProxyMethods
161
     *
162
     * @param string  $className
163
     * @param object  $instance
164
     * @param string  $method
165
     * @param mixed[] $params
166
     * @param mixed   $expectedValue
167
     */
168
    public function testMethodCallsAfterUnSerialization(
169
        string $className,
170
        $instance,
171
        string $method,
172
        array $params,
173
        $expectedValue
174
    ) : void {
175
        $proxyName = $this->generateProxy($className);
176
        /* @var $proxy AccessInterceptorInterface */
177
        $proxy     = unserialize(serialize($proxyName::staticProxyConstructor($instance)));
178
179
        self::assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
180
        $this->assertProxySynchronized($instance, $proxy);
181
    }
182
183
    /**
184
     * @dataProvider getProxyMethods
185
     *
186
     * @param string  $className
187
     * @param object  $instance
188
     * @param string  $method
189
     * @param mixed[] $params
190
     * @param mixed   $expectedValue
191
     */
192
    public function testMethodCallsAfterCloning(
193
        string $className,
194
        $instance,
195
        string $method,
196
        array $params,
197
        $expectedValue
198
    ) : void {
199
        $proxyName = $this->generateProxy($className);
200
201
        /* @var $proxy AccessInterceptorInterface */
202
        $proxy     = $proxyName::staticProxyConstructor($instance);
203
        $cloned    = clone $proxy;
204
205
        $this->assertProxySynchronized($instance, $proxy);
206
        self::assertSame($expectedValue, call_user_func_array([$cloned, $method], $params));
207
        $this->assertProxySynchronized($instance, $proxy);
208
    }
209
210
    /**
211
     * @dataProvider getPropertyAccessProxies
212
     *
213
     * @param object                     $instance
214
     * @param AccessInterceptorInterface $proxy
215
     * @param string                     $publicProperty
216
     * @param mixed                      $propertyValue
217
     */
218
    public function testPropertyReadAccess(
219
        $instance,
220
        AccessInterceptorInterface $proxy,
221
        string $publicProperty,
222
        $propertyValue
223
    ) : void {
224
        self::assertSame($propertyValue, $proxy->$publicProperty);
225
        $this->assertProxySynchronized($instance, $proxy);
226
    }
227
228
    /**
229
     * @dataProvider getPropertyAccessProxies
230
     *
231
     * @param object                     $instance
232
     * @param AccessInterceptorInterface $proxy
233
     * @param string                     $publicProperty
234
     */
235
    public function testPropertyWriteAccess($instance, AccessInterceptorInterface $proxy, string $publicProperty) : void
236
    {
237
        $newValue               = uniqid();
238
        $proxy->$publicProperty = $newValue;
239
240
        self::assertSame($newValue, $proxy->$publicProperty);
241
        $this->assertProxySynchronized($instance, $proxy);
242
    }
243
244
    /**
245
     * @dataProvider getPropertyAccessProxies
246
     *
247
     * @param object                     $instance
248
     * @param AccessInterceptorInterface $proxy
249
     * @param string                     $publicProperty
250
     */
251
    public function testPropertyExistence($instance, AccessInterceptorInterface $proxy, string $publicProperty) : void
252
    {
253
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
254
        $this->assertProxySynchronized($instance, $proxy);
255
256
        $instance->$publicProperty = null;
257
        self::assertFalse(isset($proxy->$publicProperty));
258
        $this->assertProxySynchronized($instance, $proxy);
259
    }
260
261
    /**
262
     * @dataProvider getPropertyAccessProxies
263
     *
264
     * @param object                     $instance
265
     * @param AccessInterceptorInterface $proxy
266
     * @param string                     $publicProperty
267
     */
268
    public function testPropertyUnset($instance, AccessInterceptorInterface $proxy, string $publicProperty) : void
269
    {
270
        self::markTestSkipped('It is currently not possible to synchronize properties un-setting');
271
        unset($proxy->$publicProperty);
272
273
        self::assertFalse(isset($instance->$publicProperty));
274
        self::assertFalse(isset($proxy->$publicProperty));
275
        $this->assertProxySynchronized($instance, $proxy);
276
    }
277
278
    /**
279
     * Verifies that accessing a public property containing an array behaves like in a normal context
280
     */
281
    public function testCanWriteToArrayKeysInPublicProperty() : void
282
    {
283
        $instance    = new ClassWithPublicArrayProperty();
284
        $className   = get_class($instance);
285
        $proxyName   = $this->generateProxy($className);
286
        /* @var $proxy ClassWithPublicArrayProperty|AccessInterceptorInterface */
287
        $proxy       = $proxyName::staticProxyConstructor($instance);
288
289
        $proxy->arrayProperty['foo'] = 'bar';
290
291
        self::assertSame('bar', $proxy->arrayProperty['foo']);
292
293
        $proxy->arrayProperty = ['tab' => 'taz'];
294
295
        self::assertSame(['tab' => 'taz'], $proxy->arrayProperty);
296
        $this->assertProxySynchronized($instance, $proxy);
297
    }
298
299
    /**
300
     * Verifies that public properties retrieved via `__get` don't get modified in the object state
301
     */
302
    public function testWillNotModifyRetrievedPublicProperties() : void
303
    {
304
        $instance    = new ClassWithPublicProperties();
305
        $className   = get_class($instance);
306
        $proxyName   = $this->generateProxy($className);
307
        /* @var $proxy ClassWithPublicProperties|AccessInterceptorInterface */
308
        $proxy       = $proxyName::staticProxyConstructor($instance);
309
        $variable    = $proxy->property0;
310
311
        self::assertSame('property0', $variable);
312
313
        $variable = 'foo';
314
315
        self::assertSame('property0', $proxy->property0);
316
        $this->assertProxySynchronized($instance, $proxy);
317
        self::assertSame('foo', $variable);
318
    }
319
320
    /**
321
     * Verifies that public properties references retrieved via `__get` modify in the object state
322
     */
323
    public function testWillModifyByRefRetrievedPublicProperties() : void
324
    {
325
        $instance    = new ClassWithPublicProperties();
326
        $proxyName   = $this->generateProxy(get_class($instance));
327
        /* @var $proxy ClassWithPublicProperties|AccessInterceptorInterface */
328
        $proxy       = $proxyName::staticProxyConstructor($instance);
329
        $variable    = & $proxy->property0;
330
331
        self::assertSame('property0', $variable);
332
333
        $variable = 'foo';
334
335
        self::assertSame('foo', $proxy->property0);
336
        $this->assertProxySynchronized($instance, $proxy);
337
        self::assertSame('foo', $variable);
338
    }
339
340
    /**
341
     * @group 115
342
     * @group 175
343
     */
344
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
345
    {
346
        $instance = new ClassWithCounterConstructor(10);
347
348
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
349
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
350
        $instance->__construct(3);
351
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
352
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
353
354
        $proxyName = $this->generateProxy(get_class($instance));
355
356
        /* @var $proxy ClassWithCounterConstructor */
357
        $proxy = new $proxyName(15);
358
359
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
360
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
361
        $proxy->__construct(5);
362
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
363
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
364
    }
365
366
    /**
367
     * Generates a proxy for the given class name, and retrieves its class name
368
     *
369
     * @param string $parentClassName
370
     *
371
     * @return string
372
     *
373
     * @throws UnsupportedProxiedClassException
374
     */
375
    private function generateProxy(string $parentClassName) : string
376
    {
377
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
378
        $generator          = new AccessInterceptorScopeLocalizerGenerator();
379
        $generatedClass     = new ClassGenerator($generatedClassName);
380
        $strategy           = new EvaluatingGeneratorStrategy();
381
382
        $generator->generate(new ReflectionClass($parentClassName), $generatedClass);
383
        $strategy->generate($generatedClass);
384
385
        return $generatedClassName;
386
    }
387
388
    /**
389
     * Generates a list of object | invoked method | parameters | expected result
390
     *
391
     * @return array
392
     */
393
    public function getProxyMethods() : array
394
    {
395
        $selfHintParam = new ClassWithSelfHint();
396
        $empty         = new EmptyClass();
397
398
        return [
399
            [
400
                BaseClass::class,
401
                new BaseClass(),
402
                'publicMethod',
403
                [],
404
                'publicMethodDefault'
405
            ],
406
            [
407
                BaseClass::class,
408
                new BaseClass(),
409
                'publicTypeHintedMethod',
410
                ['param' => new stdClass()],
411
                'publicTypeHintedMethodDefault'
412
            ],
413
            [
414
                BaseClass::class,
415
                new BaseClass(),
416
                'publicByReferenceMethod',
417
                [],
418
                'publicByReferenceMethodDefault'
419
            ],
420
            [
421
                ClassWithSelfHint::class,
422
                new ClassWithSelfHint(),
423
                'selfHintMethod',
424
                ['parameter' => $selfHintParam],
425
                $selfHintParam
426
            ],
427
            [
428
                ClassWithParentHint::class,
429
                new ClassWithParentHint(),
430
                'parentHintMethod',
431
                ['parameter' => $empty],
432
                $empty
433
            ],
434
        ];
435
    }
436
437
    /**
438
     * Generates proxies and instances with a public property to feed to the property accessor methods
439
     *
440
     * @return array
441
     */
442
    public function getPropertyAccessProxies() : array
443
    {
444
        $instance1  = new BaseClass();
445
        $proxyName1 = $this->generateProxy(get_class($instance1));
446
447
        return [
448
            [
449
                $instance1,
450
                $proxyName1::staticProxyConstructor($instance1),
451
                'publicProperty',
452
                'publicPropertyDefault',
453
            ],
454
        ];
455
    }
456
457
    /**
458
     * @param object                     $instance
459
     * @param AccessInterceptorInterface $proxy
460
     */
461
    private function assertProxySynchronized($instance, AccessInterceptorInterface $proxy)
462
    {
463
        $reflectionClass = new ReflectionClass($instance);
464
465
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
466
            $property->setAccessible(true);
467
468
            self::assertSame(
469
                $property->getValue($instance),
470
                $property->getValue($proxy),
471
                'Property "' . $property->getName() . '" is synchronized between instance and proxy'
472
            );
473
        }
474
    }
475
476
    public function testWillForwardVariadicArguments() : void
477
    {
478
        $configuration = new Configuration();
479
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
480
        $targetObject  = new ClassWithMethodWithVariadicFunction();
481
482
        /* @var $object ClassWithMethodWithVariadicFunction */
483
        $object = $factory->createProxy(
484
            $targetObject,
485
            [
486
                function () : string {
487
                    return 'Foo Baz';
488
                },
489
            ]
490
        );
491
492
        self::assertNull($object->bar);
493
        self::assertNull($object->baz);
494
495
        $object->foo('Ocramius', 'Malukenho', 'Danizord');
496
        self::assertSame('Ocramius', $object->bar);
497
        self::assertSame(['Malukenho', 'Danizord'], $object->baz);
498
    }
499
500
    /**
501
     * @group 265
502
     */
503
    public function testWillForwardVariadicByRefArguments() : void
504
    {
505
        $configuration = new Configuration();
506
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
507
        $targetObject  = new ClassWithMethodWithByRefVariadicFunction();
508
509
        /* @var $object ClassWithMethodWithByRefVariadicFunction */
510
        $object = $factory->createProxy(
511
            $targetObject,
512
            [
513
                function () : string {
514
                    return 'Foo Baz';
515
                },
516
            ]
517
        );
518
519
        $parameters = ['a', 'b', 'c'];
520
521
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
522
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
523
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
524
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
525
    }
526
527
    /**
528
     * This test documents a known limitation: `func_get_args()` (and similar) don't work in proxied APIs.
529
     * If you manage to make this test pass, then please do send a patch
530
     *
531
     * @group 265
532
     */
533
    public function testWillNotForwardDynamicArguments() : void
534
    {
535
        /* @var $object ClassWithDynamicArgumentsMethod */
536
        $object = (new AccessInterceptorScopeLocalizerFactory())
537
            ->createProxy(
538
                new ClassWithDynamicArgumentsMethod(),
539
                [
540
                    'dynamicArgumentsMethod' => function () : string {
541
                        return 'Foo Baz';
542
                    },
543
                ]
544
            );
545
546
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
547
548
        $this->expectException(\PHPUnit_Framework_ExpectationFailedException::class);
549
550
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
551
    }
552
553
    /**
554
     * @group 327
555
     */
556
    public function testWillInterceptAndReturnEarlyOnVoidMethod() : void
557
    {
558
        $skip      = random_int(100, 200);
559
        $addMore   = random_int(201, 300);
560
        $increment = random_int(301, 400);
561
562
        /* @var $object VoidCounter */
563
        $object = (new AccessInterceptorScopeLocalizerFactory())
564
            ->createProxy(
565
                new VoidCounter(),
566
                [
567
                    'increment' => function (
568
                        VoidCounter $proxy,
569
                        VoidCounter $instance,
570
                        string $method,
571
                        array $params,
572
                        ?bool & $returnEarly
573
                    ) use ($skip) : void {
574
                        if ($skip === $params['amount']) {
575
                            $returnEarly = true;
576
                        }
577
                    },
578
                ],
579
                [
580
                    'increment' => function (
581
                        VoidCounter $proxy,
582
                        VoidCounter $instance,
583
                        string $method,
584
                        array $params,
585
                        ?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...
586
                    ) use ($addMore) : void {
587
                        if ($addMore === $params['amount']) {
588
                            $instance->counter += 1;
589
                        }
590
                    },
591
                ]
592
            );
593
594
        $object->increment($skip);
595
        self::assertSame(0, $object->counter);
596
597
        $object->increment($increment);
598
        self::assertSame($increment, $object->counter);
599
600
        $object->increment($addMore);
601
        self::assertSame($increment + $addMore + 1, $object->counter);
602
    }
603
}
604