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

testWillNotForwardDynamicArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 7

Duplication

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