Completed
Pull Request — master (#266)
by Marco
15:13
created

generateProxy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

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