Completed
Pull Request — master (#266)
by Marco
04:32
created

testWillForwardVariadicArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 23
rs 9.0857
cc 1
eloc 13
nc 1
nop 0
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
namespace ProxyManagerTest\Functional;
20
21
use PHPUnit_Framework_TestCase;
22
use ProxyManager\Configuration;
23
use ProxyManager\Exception\UnsupportedProxiedClassException;
24
use ProxyManager\Factory\AccessInterceptorScopeLocalizerFactory;
25
use ProxyManager\Generator\ClassGenerator;
26
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
27
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
28
use ProxyManager\Proxy\AccessInterceptorInterface;
29
use ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator;
30
use ProxyManager\ProxyGenerator\Util\Properties;
31
use ProxyManagerTestAsset\BaseClass;
32
use ProxyManagerTestAsset\ClassWithCounterConstructor;
33
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
34
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
35
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
36
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
37
use ProxyManagerTestAsset\ClassWithPublicProperties;
38
use ProxyManagerTestAsset\ClassWithSelfHint;
39
use ReflectionClass;
40
use stdClass;
41
42
/**
43
 * Tests for {@see \ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator} produced objects
44
 *
45
 * @author Marco Pivetta <[email protected]>
46
 * @license MIT
47
 *
48
 * @group Functional
49
 * @coversNothing
50
 */
51
class AccessInterceptorScopeLocalizerFunctionalTest extends PHPUnit_Framework_TestCase
52
{
53
    /**
54
     * @dataProvider getProxyMethods
55
     *
56
     * @param string  $className
57
     * @param object  $instance
58
     * @param string  $method
59
     * @param mixed[] $params
60
     * @param mixed   $expectedValue
61
     */
62
    public function testMethodCalls($className, $instance, $method, array $params, $expectedValue)
63
    {
64
        $proxyName = $this->generateProxy($className);
65
66
        /* @var $proxy AccessInterceptorInterface */
67
        $proxy     = $proxyName::staticProxyConstructor($instance);
68
69
        $this->assertProxySynchronized($instance, $proxy);
70
        $this->assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
71
72
        /* @var $listener callable|\PHPUnit_Framework_MockObject_MockObject */
73
        $listener = $this->getMock('stdClass', ['__invoke']);
74
        $listener
75
            ->expects($this->once())
76
            ->method('__invoke')
77
            ->with($proxy, $proxy, $method, $params, false);
78
79
        $proxy->setMethodPrefixInterceptor(
80
            $method,
81
            function ($proxy, $instance, $method, $params, & $returnEarly) use ($listener) {
82
                $listener($proxy, $instance, $method, $params, $returnEarly);
83
            }
84
        );
85
86
        $this->assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
87
88
        $random = uniqid();
89
90
        $proxy->setMethodPrefixInterceptor(
91
            $method,
92
            function ($proxy, $instance, $method, $params, & $returnEarly) use ($random) {
93
                $returnEarly = true;
94
95
                return $random;
96
            }
97
        );
98
99
        $this->assertSame($random, call_user_func_array([$proxy, $method], $params));
100
        $this->assertProxySynchronized($instance, $proxy);
101
    }
102
103
    /**
104
     * @dataProvider getProxyMethods
105
     *
106
     * @param string  $className
107
     * @param object  $instance
108
     * @param string  $method
109
     * @param mixed[] $params
110
     * @param mixed   $expectedValue
111
     */
112
    public function testMethodCallsWithSuffixListener($className, $instance, $method, array $params, $expectedValue)
113
    {
114
        $proxyName = $this->generateProxy($className);
115
116
        /* @var $proxy AccessInterceptorInterface */
117
        $proxy     = $proxyName::staticProxyConstructor($instance);
118
        /* @var $listener callable|\PHPUnit_Framework_MockObject_MockObject */
119
        $listener  = $this->getMock(stdClass::class, ['__invoke']);
120
        $listener
121
            ->expects($this->once())
122
            ->method('__invoke')
123
            ->with($proxy, $proxy, $method, $params, $expectedValue, false);
124
125
        $proxy->setMethodSuffixInterceptor(
126
            $method,
127
            function ($proxy, $instance, $method, $params, $returnValue, & $returnEarly) use ($listener) {
128
                $listener($proxy, $instance, $method, $params, $returnValue, $returnEarly);
129
            }
130
        );
131
132
        $this->assertSame($expectedValue, call_user_func_array([$proxy, $method], $params));
133
134
        $random = uniqid();
135
136
        $proxy->setMethodSuffixInterceptor(
137
            $method,
138
            function ($proxy, $instance, $method, $params, $returnValue, & $returnEarly) use ($random) {
139
                $returnEarly = true;
140
141
                return $random;
142
            }
143
        );
144
145
        $this->assertSame($random, call_user_func_array([$proxy, $method], $params));
146
        $this->assertProxySynchronized($instance, $proxy);
147
    }
148
149
    /**
150
     * @dataProvider getProxyMethods
151
     *
152
     * @param string  $className
153
     * @param object  $instance
154
     * @param string  $method
155
     * @param mixed[] $params
156
     * @param mixed   $expectedValue
157
     */
158
    public function testMethodCallsAfterUnSerialization($className, $instance, $method, array $params, $expectedValue)
159
    {
160
        $proxyName = $this->generateProxy($className);
161
        /* @var $proxy AccessInterceptorInterface */
162
        $proxy     = unserialize(serialize($proxyName::staticProxyConstructor($instance)));
163
164
        $this->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($className, $instance, $method, array $params, $expectedValue)
178
    {
179
        $proxyName = $this->generateProxy($className);
180
181
        /* @var $proxy AccessInterceptorInterface */
182
        $proxy     = $proxyName::staticProxyConstructor($instance);
183
        $cloned    = clone $proxy;
184
185
        $this->assertProxySynchronized($instance, $proxy);
186
        $this->assertSame($expectedValue, call_user_func_array([$cloned, $method], $params));
187
        $this->assertProxySynchronized($instance, $proxy);
188
    }
189
190
    /**
191
     * @dataProvider getPropertyAccessProxies
192
     *
193
     * @param object                     $instance
194
     * @param AccessInterceptorInterface $proxy
195
     * @param string                     $publicProperty
196
     * @param mixed                      $propertyValue
197
     */
198
    public function testPropertyReadAccess(
199
        $instance,
200
        AccessInterceptorInterface $proxy,
201
        $publicProperty,
202
        $propertyValue
203
    ) {
204
        $this->assertSame($propertyValue, $proxy->$publicProperty);
205
        $this->assertProxySynchronized($instance, $proxy);
206
    }
207
208
    /**
209
     * @dataProvider getPropertyAccessProxies
210
     *
211
     * @param object                     $instance
212
     * @param AccessInterceptorInterface $proxy
213
     * @param string                     $publicProperty
214
     */
215
    public function testPropertyWriteAccess($instance, AccessInterceptorInterface $proxy, $publicProperty)
216
    {
217
        $newValue               = uniqid();
218
        $proxy->$publicProperty = $newValue;
219
220
        $this->assertSame($newValue, $proxy->$publicProperty);
221
        $this->assertProxySynchronized($instance, $proxy);
222
    }
223
224
    /**
225
     * @dataProvider getPropertyAccessProxies
226
     *
227
     * @param object                     $instance
228
     * @param AccessInterceptorInterface $proxy
229
     * @param string                     $publicProperty
230
     */
231
    public function testPropertyExistence($instance, AccessInterceptorInterface $proxy, $publicProperty)
232
    {
233
        $this->assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
234
        $this->assertProxySynchronized($instance, $proxy);
235
236
        $instance->$publicProperty = null;
237
        $this->assertFalse(isset($proxy->$publicProperty));
238
        $this->assertProxySynchronized($instance, $proxy);
239
    }
240
241
    /**
242
     * @dataProvider getPropertyAccessProxies
243
     *
244
     * @param object                     $instance
245
     * @param AccessInterceptorInterface $proxy
246
     * @param string                     $publicProperty
247
     */
248
    public function testPropertyUnset($instance, AccessInterceptorInterface $proxy, $publicProperty)
249
    {
250
        $this->markTestSkipped('It is currently not possible to synchronize properties un-setting');
251
        unset($proxy->$publicProperty);
252
253
        $this->assertFalse(isset($instance->$publicProperty));
254
        $this->assertFalse(isset($proxy->$publicProperty));
255
        $this->assertProxySynchronized($instance, $proxy);
256
    }
257
258
    /**
259
     * Verifies that accessing a public property containing an array behaves like in a normal context
260
     */
261
    public function testCanWriteToArrayKeysInPublicProperty()
262
    {
263
        $instance    = new ClassWithPublicArrayProperty();
264
        $className   = get_class($instance);
265
        $proxyName   = $this->generateProxy($className);
266
        /* @var $proxy ClassWithPublicArrayProperty|AccessInterceptorInterface */
267
        $proxy       = $proxyName::staticProxyConstructor($instance);
268
269
        $proxy->arrayProperty['foo'] = 'bar';
270
271
        $this->assertSame('bar', $proxy->arrayProperty['foo']);
272
273
        $proxy->arrayProperty = ['tab' => 'taz'];
274
275
        $this->assertSame(['tab' => 'taz'], $proxy->arrayProperty);
276
        $this->assertProxySynchronized($instance, $proxy);
277
    }
278
279
    /**
280
     * Verifies that public properties retrieved via `__get` don't get modified in the object state
281
     */
282
    public function testWillNotModifyRetrievedPublicProperties()
283
    {
284
        $instance    = new ClassWithPublicProperties();
285
        $className   = get_class($instance);
286
        $proxyName   = $this->generateProxy($className);
287
        /* @var $proxy ClassWithPublicProperties|AccessInterceptorInterface */
288
        $proxy       = $proxyName::staticProxyConstructor($instance);
289
        $variable    = $proxy->property0;
290
291
        $this->assertSame('property0', $variable);
292
293
        $variable = 'foo';
294
295
        $this->assertSame('property0', $proxy->property0);
296
        $this->assertProxySynchronized($instance, $proxy);
297
        $this->assertSame('foo', $variable);
298
    }
299
300
    /**
301
     * Verifies that public properties references retrieved via `__get` modify in the object state
302
     */
303
    public function testWillModifyByRefRetrievedPublicProperties()
304
    {
305
        $instance    = new ClassWithPublicProperties();
306
        $proxyName   = $this->generateProxy(get_class($instance));
307
        /* @var $proxy ClassWithPublicProperties|AccessInterceptorInterface */
308
        $proxy       = $proxyName::staticProxyConstructor($instance);
309
        $variable    = & $proxy->property0;
310
311
        $this->assertSame('property0', $variable);
312
313
        $variable = 'foo';
314
315
        $this->assertSame('foo', $proxy->property0);
316
        $this->assertProxySynchronized($instance, $proxy);
317
        $this->assertSame('foo', $variable);
318
    }
319
320
    /**
321
     * @group 115
322
     * @group 175
323
     */
324
    public function testWillBehaveLikeObjectWithNormalConstructor()
325
    {
326
        $instance = new ClassWithCounterConstructor(10);
327
328
        $this->assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
329
        $this->assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
330
        $instance->__construct(3);
331
        $this->assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
332
        $this->assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
333
334
        $proxyName = $this->generateProxy(get_class($instance));
335
336
        /* @var $proxy ClassWithCounterConstructor */
337
        $proxy = new $proxyName(15);
338
339
        $this->assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
340
        $this->assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
341
        $proxy->__construct(5);
342
        $this->assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
343
        $this->assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
344
    }
345
346
    /**
347
     * Generates a proxy for the given class name, and retrieves its class name
348
     *
349
     * @param string $parentClassName
350
     *
351
     * @return string
352
     *
353
     * @throws UnsupportedProxiedClassException
354
     */
355
    private function generateProxy($parentClassName)
356
    {
357
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
358
        $generator          = new AccessInterceptorScopeLocalizerGenerator();
359
        $generatedClass     = new ClassGenerator($generatedClassName);
360
        $strategy           = new EvaluatingGeneratorStrategy();
361
362
        $generator->generate(new ReflectionClass($parentClassName), $generatedClass);
363
        $strategy->generate($generatedClass);
364
365
        return $generatedClassName;
366
    }
367
368
    /**
369
     * Generates a list of object | invoked method | parameters | expected result
370
     *
371
     * @return array
372
     */
373
    public function getProxyMethods()
374
    {
375
        $selfHintParam = new ClassWithSelfHint();
376
377
        return [
378
            [
379
                BaseClass::class,
380
                new BaseClass(),
381
                'publicMethod',
382
                [],
383
                'publicMethodDefault'
384
            ],
385
            [
386
                BaseClass::class,
387
                new BaseClass(),
388
                'publicTypeHintedMethod',
389
                ['param' => new stdClass()],
390
                'publicTypeHintedMethodDefault'
391
            ],
392
            [
393
                BaseClass::class,
394
                new BaseClass(),
395
                'publicByReferenceMethod',
396
                [],
397
                'publicByReferenceMethodDefault'
398
            ],
399
            [
400
                ClassWithSelfHint::class,
401
                new ClassWithSelfHint(),
402
                'selfHintMethod',
403
                ['parameter' => $selfHintParam],
404
                $selfHintParam
405
            ],
406
        ];
407
    }
408
409
    /**
410
     * Generates proxies and instances with a public property to feed to the property accessor methods
411
     *
412
     * @return array
413
     */
414
    public function getPropertyAccessProxies()
415
    {
416
        $instance1  = new BaseClass();
417
        $proxyName1 = $this->generateProxy(get_class($instance1));
418
419
        return [
420
            [
421
                $instance1,
422
                $proxyName1::staticProxyConstructor($instance1),
423
                'publicProperty',
424
                'publicPropertyDefault',
425
            ],
426
        ];
427
    }
428
429
    /**
430
     * @param object                     $instance
431
     * @param AccessInterceptorInterface $proxy
432
     */
433
    private function assertProxySynchronized($instance, AccessInterceptorInterface $proxy)
434
    {
435
        $reflectionClass = new ReflectionClass($instance);
436
437
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
438
            $property->setAccessible(true);
439
440
            $this->assertSame(
441
                $property->getValue($instance),
442
                $property->getValue($proxy),
443
                'Property "' . $property->getName() . '" is synchronized between instance and proxy'
444
            );
445
        }
446
    }
447
448
    public function testWillForwardVariadicArguments()
449
    {
450
        $configuration = new Configuration();
451
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
452
        $targetObject  = new ClassWithMethodWithVariadicFunction();
453
454
        /* @var $object ClassWithMethodWithVariadicFunction */
455
        $object = $factory->createProxy(
456
            $targetObject,
457
            [
458
                function ($paratemers) {
0 ignored issues
show
Unused Code introduced by
The parameter $paratemers 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...
459
                    return 'Foo Baz';
460
                },
461
            ]
462
        );
463
464
        $this->assertNull($object->bar);
465
        $this->assertNull($object->baz);
466
467
        $object->foo('Ocramius', 'Malukenho', 'Danizord');
468
        $this->assertSame('Ocramius', $object->bar);
469
        $this->assertSame(['Malukenho', 'Danizord'], $object->baz);
470
    }
471
472
    /**
473
     * @group 265
474
     */
475
    public function testWillForwardVariadicByRefArguments()
476
    {
477
        $configuration = new Configuration();
478
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
479
        $targetObject  = new ClassWithMethodWithByRefVariadicFunction();
480
481
        /* @var $object ClassWithMethodWithByRefVariadicFunction */
482
        $object = $factory->createProxy(
483
            $targetObject,
484
            [
485
                function ($paratemers) {
0 ignored issues
show
Unused Code introduced by
The parameter $paratemers 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...
486
                    return 'Foo Baz';
487
                },
488
            ]
489
        );
490
491
        $parameters = ['a', 'b', 'c'];
492
493
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
494
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
495
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
496
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
497
    }
498
499
    /**
500
     * This test documents a known limitation: `func_get_args()` (and similars) don't work in proxied APIs.
501
     * If you manage to make this test pass, then please do send a patch
502
     *
503
     * @group 265
504
     */
505
    public function testWillNotForwardDynamicArguments()
506
    {
507
        /* @var $object ClassWithDynamicArgumentsMethod */
508
        $object = (new AccessInterceptorScopeLocalizerFactory())
509
            ->createProxy(
510
                new ClassWithDynamicArgumentsMethod(),
511
                [
512
                    'dynamicArgumentsMethod' => function ($paratemers) {
0 ignored issues
show
Unused Code introduced by
The parameter $paratemers 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...
513
                        return 'Foo Baz';
514
                    },
515
                ]
516
            );
517
518
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
519
520
        $this->setExpectedException(\PHPUnit_Framework_ExpectationFailedException::class);
521
522
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
523
    }
524
}
525