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

getPropertyAccessProxies()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 24
rs 8.9714
cc 1
eloc 15
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\GhostObjectInterface;
27
use ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator;
28
use ProxyManager\ProxyGenerator\Util\Properties;
29
use ProxyManagerTestAsset\BaseClass;
30
use ProxyManagerTestAsset\ClassWithAbstractPublicMethod;
31
use ProxyManagerTestAsset\ClassWithCollidingPrivateInheritedProperties;
32
use ProxyManagerTestAsset\ClassWithCounterConstructor;
33
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
34
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
35
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
36
use ProxyManagerTestAsset\ClassWithMixedProperties;
37
use ProxyManagerTestAsset\ClassWithMixedPropertiesAndAccessorMethods;
38
use ProxyManagerTestAsset\ClassWithPrivateProperties;
39
use ProxyManagerTestAsset\ClassWithProtectedProperties;
40
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
41
use ProxyManagerTestAsset\ClassWithPublicProperties;
42
use ProxyManagerTestAsset\ClassWithSelfHint;
43
use ProxyManagerTestAsset\EmptyClass;
44
use ReflectionClass;
45
use ReflectionProperty;
46
47
/**
48
 * Tests for {@see \ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator} produced objects
49
 *
50
 * @author Marco Pivetta <[email protected]>
51
 * @license MIT
52
 *
53
 * @group Functional
54
 * @coversNothing
55
 */
56
class LazyLoadingGhostFunctionalTest extends PHPUnit_Framework_TestCase
57
{
58
    /**
59
     * @dataProvider getProxyInitializingMethods
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 testMethodCallsThatLazyLoadTheObject($className, $instance, $method, array $params, $expectedValue)
68
    {
69
        $proxyName = $this->generateProxy($className);
70
71
        /* @var $proxy GhostObjectInterface */
72
        $proxy = $proxyName::staticProxyConstructor($this->createInitializer($className, $instance));
73
74
        $this->assertFalse($proxy->isProxyInitialized());
75
76
        /* @var $callProxyMethod callable */
77
        $callProxyMethod = [$proxy, $method];
78
        $parameterValues = array_values($params);
79
80
        self::assertInternalType('callable', $callProxyMethod);
81
        $this->assertSame($expectedValue, $callProxyMethod(...$parameterValues));
82
        $this->assertTrue($proxy->isProxyInitialized());
83
    }
84
85
    /**
86
     * @dataProvider getProxyNonInitializingMethods
87
     *
88
     * @param string  $className
89
     * @param object  $instance
90
     * @param string  $method
91
     * @param mixed[] $params
92
     * @param mixed   $expectedValue
93
     */
94
    public function testMethodCallsThatDoNotLazyLoadTheObject(
95
        $className,
96
        $instance,
97
        $method,
98
        array $params,
99
        $expectedValue
100
    ) {
101
        $proxyName         = $this->generateProxy($className);
102
        $initializeMatcher = $this->getMock('stdClass', ['__invoke']);
103
104
        $initializeMatcher->expects($this->never())->method('__invoke'); // should not initialize the proxy
105
106
        /* @var $proxy GhostObjectInterface */
107
        $proxy = $proxyName::staticProxyConstructor(
108
            $this->createInitializer($className, $instance, $initializeMatcher)
109
        );
110
111
        self::assertFalse($proxy->isProxyInitialized());
112
113
        /* @var $callProxyMethod callable */
114
        $callProxyMethod = [$proxy, $method];
115
        $parameterValues = array_values($params);
116
117
        self::assertInternalType('callable', $callProxyMethod);
118
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
119
        self::assertFalse($proxy->isProxyInitialized());
120
    }
121
122
    /**
123
     * @dataProvider getProxyMethods
124
     *
125
     * @param string  $className
126
     * @param object  $instance
127
     * @param string  $method
128
     * @param mixed[] $params
129
     * @param mixed   $expectedValue
130
     */
131
    public function testMethodCallsAfterUnSerialization($className, $instance, $method, array $params, $expectedValue)
132
    {
133
        $proxyName = $this->generateProxy($className);
134
135
        /* @var $proxy GhostObjectInterface */
136
        $proxy = unserialize(serialize($proxyName::staticProxyConstructor(
137
            $this->createInitializer($className, $instance)
138
        )));
139
140
        $this->assertTrue($proxy->isProxyInitialized());
141
142
        /* @var $callProxyMethod callable */
143
        $callProxyMethod = [$proxy, $method];
144
        $parameterValues = array_values($params);
145
146
        self::assertInternalType('callable', $callProxyMethod);
147
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
148
    }
149
150
    /**
151
     * @dataProvider getProxyMethods
152
     *
153
     * @param string  $className
154
     * @param object  $instance
155
     * @param string  $method
156
     * @param mixed[] $params
157
     * @param mixed   $expectedValue
158
     */
159
    public function testMethodCallsAfterCloning($className, $instance, $method, array $params, $expectedValue)
160
    {
161
        $proxyName = $this->generateProxy($className);
162
163
        /* @var $proxy GhostObjectInterface */
164
        $proxy  = $proxyName::staticProxyConstructor($this->createInitializer($className, $instance));
165
        $cloned = clone $proxy;
166
167
        $this->assertTrue($cloned->isProxyInitialized());
168
169
        /* @var $callProxyMethod callable */
170
        $callProxyMethod = [$proxy, $method];
171
        $parameterValues = array_values($params);
172
173
        self::assertInternalType('callable', $callProxyMethod);
174
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
175
    }
176
177
    /**
178
     * @dataProvider getPropertyAccessProxies
179
     *
180
     * @param object               $instance
181
     * @param GhostObjectInterface $proxy
182
     * @param string               $publicProperty
183
     * @param mixed                $propertyValue
184
     */
185
    public function testPropertyReadAccess($instance, GhostObjectInterface $proxy, $publicProperty, $propertyValue)
186
    {
187
        $this->assertSame($propertyValue, $proxy->$publicProperty);
188
        $this->assertTrue($proxy->isProxyInitialized());
189
    }
190
191
    /**
192
     * @dataProvider getPropertyAccessProxies
193
     *
194
     * @param object               $instance
195
     * @param GhostObjectInterface $proxy
196
     * @param string               $publicProperty
197
     */
198
    public function testPropertyWriteAccess($instance, GhostObjectInterface $proxy, $publicProperty)
199
    {
200
        $newValue               = uniqid();
201
        $proxy->$publicProperty = $newValue;
202
203
        $this->assertTrue($proxy->isProxyInitialized());
204
        $this->assertSame($newValue, $proxy->$publicProperty);
205
    }
206
207
    /**
208
     * @dataProvider getPropertyAccessProxies
209
     *
210
     * @param object               $instance
211
     * @param GhostObjectInterface $proxy
212
     * @param string               $publicProperty
213
     */
214
    public function testPropertyExistence($instance, GhostObjectInterface $proxy, $publicProperty)
215
    {
216
        $this->assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
217
        $this->assertTrue($proxy->isProxyInitialized());
218
    }
219
220
    /**
221
     * @dataProvider getPropertyAccessProxies
222
     *
223
     * @param object               $instance
224
     * @param GhostObjectInterface $proxy
225
     * @param string               $publicProperty
226
     */
227
    public function testPropertyAbsence($instance, GhostObjectInterface $proxy, $publicProperty)
228
    {
229
        $proxy->$publicProperty = null;
230
        $this->assertFalse(isset($proxy->$publicProperty));
231
        $this->assertTrue($proxy->isProxyInitialized());
232
    }
233
234
    /**
235
     * @dataProvider getPropertyAccessProxies
236
     *
237
     * @param object               $instance
238
     * @param GhostObjectInterface $proxy
239
     * @param string               $publicProperty
240
     */
241
    public function testPropertyUnset($instance, GhostObjectInterface $proxy, $publicProperty)
242
    {
243
        unset($proxy->$publicProperty);
244
245
        $this->assertTrue($proxy->isProxyInitialized());
246
        $this->assertTrue(isset($instance->$publicProperty));
247
        $this->assertFalse(isset($proxy->$publicProperty));
248
    }
249
250
    /**
251
     * Verifies that accessing a public property containing an array behaves like in a normal context
252
     */
253
    public function testCanWriteToArrayKeysInPublicProperty()
254
    {
255
        $instance    = new ClassWithPublicArrayProperty();
256
        $className   = get_class($instance);
257
        $initializer = $this->createInitializer($className, $instance);
258
        $proxyName   = $this->generateProxy($className);
259
        /* @var $proxy ClassWithPublicArrayProperty */
260
        $proxy       = $proxyName::staticProxyConstructor($initializer);
261
262
        $proxy->arrayProperty['foo'] = 'bar';
263
264
        $this->assertSame('bar', $proxy->arrayProperty['foo']);
265
266
        $proxy->arrayProperty = ['tab' => 'taz'];
267
268
        $this->assertSame(['tab' => 'taz'], $proxy->arrayProperty);
269
    }
270
271
    /**
272
     * Verifies that public properties retrieved via `__get` don't get modified in the object itself
273
     */
274
    public function testWillNotModifyRetrievedPublicProperties()
275
    {
276
        $instance    = new ClassWithPublicProperties();
277
        $className   = get_class($instance);
278
        $initializer = $this->createInitializer($className, $instance);
279
        $proxyName   = $this->generateProxy($className);
280
        /* @var $proxy ClassWithPublicProperties */
281
        $proxy       = $proxyName::staticProxyConstructor($initializer);
282
        $variable    = $proxy->property0;
283
284
        $this->assertSame('property0', $variable);
285
286
        $variable = 'foo';
287
288
        $this->assertSame('property0', $proxy->property0);
289
        $this->assertSame('foo', $variable);
290
    }
291
292
    /**
293
     * Verifies that public properties references retrieved via `__get` modify in the object state
294
     */
295
    public function testWillModifyByRefRetrievedPublicProperties()
296
    {
297
        $instance    = new ClassWithPublicProperties();
298
        $className   = get_class($instance);
299
        $initializer = $this->createInitializer($className, $instance);
300
        $proxyName   = $this->generateProxy($className);
301
        /* @var $proxy ClassWithPublicProperties */
302
        $proxy       = $proxyName::staticProxyConstructor($initializer);
303
        $variable    = & $proxy->property0;
304
305
        $this->assertSame('property0', $variable);
306
307
        $variable = 'foo';
308
309
        $this->assertSame('foo', $proxy->property0);
310
        $this->assertSame('foo', $variable);
311
    }
312
313
    public function testKeepsInitializerWhenNotOverwitten()
314
    {
315
        $instance    = new BaseClass();
316
        $proxyName   = $this->generateProxy(get_class($instance));
317
        $initializer = function () {
318
        };
319
        /* @var $proxy GhostObjectInterface */
320
        $proxy       = $proxyName::staticProxyConstructor($initializer);
321
322
        $proxy->initializeProxy();
323
324
        $this->assertSame($initializer, $proxy->getProxyInitializer());
325
    }
326
327
    /**
328
     * Verifies that public properties are not being initialized multiple times
329
     */
330
    public function testKeepsInitializedPublicProperties()
331
    {
332
        $instance    = new BaseClass();
333
        $proxyName   = $this->generateProxy(get_class($instance));
334
        $initializer = function (BaseClass $proxy, $method, $parameters, & $initializer) {
335
            $initializer           = null;
336
            $proxy->publicProperty = 'newValue';
337
        };
338
        /* @var $proxy GhostObjectInterface|BaseClass */
339
        $proxy       = $proxyName::staticProxyConstructor($initializer);
340
341
        $proxy->initializeProxy();
342
        $this->assertSame('newValue', $proxy->publicProperty);
343
344
        $proxy->publicProperty = 'otherValue';
345
346
        $proxy->initializeProxy();
347
348
        $this->assertSame('otherValue', $proxy->publicProperty);
349
    }
350
351
    /**
352
     * Verifies that properties' default values are preserved
353
     */
354
    public function testPublicPropertyDefaultWillBePreserved()
355
    {
356
        $instance    = new ClassWithPublicProperties();
357
        $proxyName   = $this->generateProxy(get_class($instance));
358
        /* @var $proxy ClassWithPublicProperties */
359
        $proxy       = $proxyName::staticProxyConstructor(function () {
360
        });
361
362
        $this->assertSame('property0', $proxy->property0);
363
    }
364
365
    /**
366
     * Verifies that protected properties' default values are preserved
367
     */
368
    public function testProtectedPropertyDefaultWillBePreserved()
369
    {
370
        $instance    = new ClassWithProtectedProperties();
371
        $proxyName   = $this->generateProxy(get_class($instance));
372
        /* @var $proxy ClassWithProtectedProperties */
373
        $proxy       = $proxyName::staticProxyConstructor(function () {
374
        });
375
376
        // Check protected property via reflection
377
        $reflectionProperty = new ReflectionProperty($instance, 'property0');
378
        $reflectionProperty->setAccessible(true);
379
380
        $this->assertSame('property0', $reflectionProperty->getValue($proxy));
381
    }
382
383
    /**
384
     * Verifies that private properties' default values are preserved
385
     */
386
    public function testPrivatePropertyDefaultWillBePreserved()
387
    {
388
        $instance  = new ClassWithPrivateProperties();
389
        $proxyName = $this->generateProxy(get_class($instance));
390
        /* @var $proxy ClassWithPrivateProperties */
391
        $proxy     = $proxyName::staticProxyConstructor(function () {
392
        });
393
394
        // Check protected property via reflection
395
        $reflectionProperty = new ReflectionProperty($instance, 'property0');
396
        $reflectionProperty->setAccessible(true);
397
398
        $this->assertSame('property0', $reflectionProperty->getValue($proxy));
399
    }
400
401
    /**
402
     * @group 159
403
     * @group 192
404
     */
405
    public function testMultiLevelPrivatePropertiesDefaultsWillBePreserved()
406
    {
407
        $instance  = new ClassWithCollidingPrivateInheritedProperties();
408
        $proxyName = $this->generateProxy(ClassWithCollidingPrivateInheritedProperties::class);
409
        /* @var $proxy ClassWithPrivateProperties */
410
        $proxy     = $proxyName::staticProxyConstructor(function () {
411
        });
412
413
        $childProperty  = new ReflectionProperty($instance, 'property0');
414
        $parentProperty = new ReflectionProperty(get_parent_class($instance), 'property0');
415
416
        $childProperty->setAccessible(true);
417
        $parentProperty->setAccessible(true);
418
419
        $this->assertSame('childClassProperty0', $childProperty->getValue($proxy));
420
        $this->assertSame('property0', $parentProperty->getValue($proxy));
421
    }
422
423
    /**
424
     * @group 159
425
     * @group 192
426
     */
427
    public function testMultiLevelPrivatePropertiesByRefInitialization()
428
    {
429
        $class     = ClassWithCollidingPrivateInheritedProperties::class;
430
        $proxyName = $this->generateProxy($class);
431
        /* @var $proxy ClassWithPrivateProperties */
432
        $proxy     = $proxyName::staticProxyConstructor(
433
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) {
434
                $initializer = null;
435
                $properties["\0" . $class . "\0property0"] = 'foo';
436
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'bar';
437
            }
438
        );
439
440
        $childProperty  = new ReflectionProperty($class, 'property0');
441
        $parentProperty = new ReflectionProperty(get_parent_class($class), 'property0');
442
443
        $childProperty->setAccessible(true);
444
        $parentProperty->setAccessible(true);
445
446
        $this->assertSame('foo', $childProperty->getValue($proxy));
447
        $this->assertSame('bar', $parentProperty->getValue($proxy));
448
    }
449
450
    /**
451
     * @group 159
452
     * @group 192
453
     *
454
     * Test designed to verify that the cached logic does take into account the fact that
455
     * proxies are different instances
456
     */
457
    public function testGetPropertyFromDifferentProxyInstances()
458
    {
459
        $class     = ClassWithCollidingPrivateInheritedProperties::class;
460
        $proxyName = $this->generateProxy($class);
461
462
        /* @var $proxy1 ClassWithPrivateProperties */
463
        $proxy1    = $proxyName::staticProxyConstructor(
464
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) {
465
                $initializer = null;
466
                $properties["\0" . $class . "\0property0"] = 'foo';
467
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'bar';
468
            }
469
        );
470
        /* @var $proxy2 ClassWithPrivateProperties */
471
        $proxy2    = $proxyName::staticProxyConstructor(
472
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) {
473
                $initializer = null;
474
                $properties["\0" . $class . "\0property0"] = 'baz';
475
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'tab';
476
            }
477
        );
478
479
        $childProperty  = new ReflectionProperty($class, 'property0');
480
        $parentProperty = new ReflectionProperty(get_parent_class($class), 'property0');
481
482
        $childProperty->setAccessible(true);
483
        $parentProperty->setAccessible(true);
484
485
        $this->assertSame('foo', $childProperty->getValue($proxy1));
486
        $this->assertSame('bar', $parentProperty->getValue($proxy1));
487
488
        $this->assertSame('baz', $childProperty->getValue($proxy2));
489
        $this->assertSame('tab', $parentProperty->getValue($proxy2));
490
    }
491
492
    /**
493
     * @group 159
494
     * @group 192
495
     *
496
     * Test designed to verify that the cached logic does take into account the fact that
497
     * proxies are different instances
498
     */
499
    public function testSetPrivatePropertyOnDifferentProxyInstances()
500
    {
501
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
502
        $proxyName = $this->generateProxy($class);
503
504
        /* @var $proxy1 ClassWithMixedPropertiesAndAccessorMethods */
505
        $proxy1    = $proxyName::staticProxyConstructor(
506
            function ($proxy, $method, $params, & $initializer, array $properties) {
0 ignored issues
show
Unused Code introduced by
The parameter $properties 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...
507
                $initializer = null;
508
            }
509
        );
510
        /* @var $proxy2 ClassWithMixedPropertiesAndAccessorMethods */
511
        $proxy2    = $proxyName::staticProxyConstructor(
512
            function ($proxy, $method, $params, & $initializer, array $properties) {
0 ignored issues
show
Unused Code introduced by
The parameter $properties 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
                $initializer = null;
514
            }
515
        );
516
517
        $proxy1->set('privateProperty', 'private1');
518
        $proxy2->set('privateProperty', 'private2');
519
        $this->assertSame('private1', $proxy1->get('privateProperty'));
520
        $this->assertSame('private2', $proxy2->get('privateProperty'));
521
    }
522
523
    /**
524
     * @group 159
525
     * @group 192
526
     *
527
     * Test designed to verify that the cached logic does take into account the fact that
528
     * proxies are different instances
529
     */
530
    public function testIssetPrivatePropertyOnDifferentProxyInstances()
531
    {
532
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
533
        $proxyName = $this->generateProxy($class);
534
535
        /* @var $proxy1 ClassWithMixedPropertiesAndAccessorMethods */
536
        $proxy1    = $proxyName::staticProxyConstructor(
537
            function ($proxy, $method, $params, & $initializer, array $properties) {
0 ignored issues
show
Unused Code introduced by
The parameter $properties 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...
538
                $initializer = null;
539
            }
540
        );
541
        /* @var $proxy2 ClassWithMixedPropertiesAndAccessorMethods */
542
        $proxy2    = $proxyName::staticProxyConstructor(
543
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) {
544
                $initializer = null;
545
                $properties["\0" . $class . "\0" . "privateProperty"] = null;
546
            }
547
        );
548
549
        $this->assertTrue($proxy1->has('privateProperty'));
550
        $this->assertFalse($proxy2->has('privateProperty'));
551
        $this->assertTrue($proxy1->has('privateProperty'));
552
        $this->assertFalse($proxy2->has('privateProperty'));
553
    }
554
555
    /**
556
     * @group 159
557
     * @group 192
558
     *
559
     * Test designed to verify that the cached logic does take into account the fact that
560
     * proxies are different instances
561
     */
562
    public function testUnsetPrivatePropertyOnDifferentProxyInstances()
563
    {
564
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
565
        $proxyName = $this->generateProxy($class);
566
567
        /* @var $proxy1 ClassWithMixedPropertiesAndAccessorMethods */
568
        $proxy1    = $proxyName::staticProxyConstructor(
569
            function ($proxy, $method, $params, & $initializer, array $properties) {
0 ignored issues
show
Unused Code introduced by
The parameter $properties 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...
570
                $initializer = null;
571
            }
572
        );
573
        /* @var $proxy2 ClassWithMixedPropertiesAndAccessorMethods */
574
        $proxy2    = $proxyName::staticProxyConstructor(
575
            function ($proxy, $method, $params, & $initializer, array $properties) {
0 ignored issues
show
Unused Code introduced by
The parameter $properties 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...
576
                $initializer = null;
577
            }
578
        );
579
580
        $this->assertTrue($proxy1->has('privateProperty'));
581
        $proxy2->remove('privateProperty');
582
        $this->assertFalse($proxy2->has('privateProperty'));
583
        $this->assertTrue($proxy1->has('privateProperty'));
584
        $proxy1->remove('privateProperty');
585
        $this->assertFalse($proxy1->has('privateProperty'));
586
        $this->assertFalse($proxy2->has('privateProperty'));
587
    }
588
589
    /**
590
     * @group 159
591
     * @group 192
592
     *
593
     * Test designed to verify that the cached logic does take into account the fact that
594
     * proxies are different instances
595
     */
596
    public function testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse()
597
    {
598
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
599
        $proxyName = $this->generateProxy($class);
600
601
        /* @var $proxy1 ClassWithMixedPropertiesAndAccessorMethods */
602
        $proxy1    = $proxyName::staticProxyConstructor(
603
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) {
604
                $initializer = null;
605
                $properties["publicProperty"] = false;
606
                $properties["\0*\0" . "protectedProperty"] = false;
607
                $properties["\0" . $class . "\0" . "privateProperty"] = false;
608
            }
609
        );
610
        /* @var $proxy2 ClassWithMixedPropertiesAndAccessorMethods */
611
        $proxy2    = $proxyName::staticProxyConstructor(
612
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) {
613
                $initializer = null;
614
                $properties["publicProperty"] = null;
615
                $properties["\0*\0" . "protectedProperty"] = null;
616
                $properties["\0" . $class . "\0" . "privateProperty"] = null;
617
            }
618
        );
619
620
        $this->assertTrue($proxy1->has('protectedProperty'));
621
        $this->assertTrue($proxy1->has('publicProperty'));
622
        $this->assertTrue($proxy1->has('privateProperty'));
623
624
        $this->assertFalse($proxy2->has('protectedProperty'));
625
        $this->assertFalse($proxy2->has('publicProperty'));
626
        $this->assertFalse($proxy2->has('privateProperty'));
627
    }
628
629
    public function testByRefInitialization()
630
    {
631
        $proxyName = $this->generateProxy(ClassWithMixedProperties::class);
632
        /* @var $proxy ClassWithPrivateProperties */
633
        $proxy     = $proxyName::staticProxyConstructor(
634
            function ($proxy, $method, $params, & $initializer, array $properties) {
635
                $initializer = null;
636
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty0"] = 'private0';
637
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty1"] = 'private1';
638
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty2"] = 'private2';
639
                $properties["\0*\0protectedProperty0"] = 'protected0';
640
                $properties["\0*\0protectedProperty1"] = 'protected1';
641
                $properties["\0*\0protectedProperty2"] = 'protected2';
642
                $properties["publicProperty0"] = 'public0';
643
                $properties["publicProperty1"] = 'public1';
644
                $properties["publicProperty2"] = 'public2';
645
            }
646
        );
647
648
        $reflectionClass = new ReflectionClass(ClassWithMixedProperties::class);
649
650
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
651
            $property->setAccessible(true);
652
653
            $this->assertSame(str_replace('Property', '', $property->getName()), $property->getValue($proxy));
654
        }
655
    }
656
657
    /**
658
     * @group 115
659
     * @group 175
660
     */
661
    public function testWillBehaveLikeObjectWithNormalConstructor()
662
    {
663
        $instance = new ClassWithCounterConstructor(10);
664
665
        $this->assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
666
        $this->assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
667
        $instance->__construct(3);
668
        $this->assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
669
        $this->assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
670
671
        $proxyName = $this->generateProxy(get_class($instance));
672
673
        /* @var $proxy ClassWithCounterConstructor */
674
        $proxy = new $proxyName(15);
675
676
        $this->assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
677
        $this->assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
678
        $proxy->__construct(5);
679
        $this->assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
680
        $this->assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
681
    }
682
683
    public function testInitializeProxyWillReturnTrueOnSuccessfulInitialization()
684
    {
685
        $proxyName = $this->generateProxy(ClassWithMixedProperties::class);
686
687
        /* @var $proxy GhostObjectInterface */
688
        $proxy = $proxyName::staticProxyConstructor($this->createInitializer(
689
            ClassWithMixedProperties::class,
690
            new ClassWithMixedProperties()
691
        ));
692
693
        $this->assertTrue($proxy->initializeProxy());
694
        $this->assertTrue($proxy->isProxyInitialized());
695
        $this->assertFalse($proxy->initializeProxy());
696
    }
697
698
    /**
699
     * Generates a proxy for the given class name, and retrieves its class name
700
     *
701
     * @param string  $parentClassName
702
     * @param mixed[] $proxyOptions
703
     *
704
     * @return string
705
     */
706
    private function generateProxy($parentClassName, array $proxyOptions = [])
707
    {
708
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
709
        $generatedClass     = new ClassGenerator($generatedClassName);
710
711
        (new LazyLoadingGhostGenerator())->generate(
712
            new ReflectionClass($parentClassName),
713
            $generatedClass,
714
            $proxyOptions
715
        );
716
        (new EvaluatingGeneratorStrategy())->generate($generatedClass);
717
718
        return $generatedClassName;
719
    }
720
721
    /**
722
     * @param string $className
723
     * @param object $realInstance
724
     * @param Mock   $initializerMatcher
725
     *
726
     * @return \Closure
727
     */
728
    private function createInitializer($className, $realInstance, Mock $initializerMatcher = null)
729
    {
730
        if (null === $initializerMatcher) {
731
            $initializerMatcher = $this->getMock('stdClass', ['__invoke']);
732
733
            $initializerMatcher
734
                ->expects($this->once())
735
                ->method('__invoke')
736
                ->with(
737
                    $this->logicalAnd(
738
                        $this->isInstanceOf(GhostObjectInterface::class),
739
                        $this->isInstanceOf($className)
740
                    )
741
                );
742
        }
743
744
        /* @var $initializerMatcher callable */
745
        $initializerMatcher = $initializerMatcher ?: $this->getMock('stdClass', ['__invoke']);
746
747
        return function (
748
            GhostObjectInterface $proxy,
749
            $method,
750
            $params,
751
            & $initializer
752
        ) use (
753
            $initializerMatcher,
754
            $realInstance
755
        ) {
756
            $initializer     = null;
757
            $reflectionClass = new ReflectionClass($realInstance);
758
759
            foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
760
                $property->setAccessible(true);
761
                $property->setValue($proxy, $property->getValue($realInstance));
762
            }
763
764
            $initializerMatcher($proxy, $method, $params);
765
766
            return true;
767
        };
768
    }
769
770
    /**
771
     * Generates a list of object | invoked method | parameters | expected result
772
     *
773
     * @return array
774
     */
775
    public function getProxyMethods()
776
    {
777
        $selfHintParam = new ClassWithSelfHint();
778
779
        return [
780
            [
781
                BaseClass::class,
782
                new BaseClass(),
783
                'publicMethod',
784
                [],
785
                'publicMethodDefault'
786
            ],
787
            [
788
                BaseClass::class,
789
                new BaseClass(),
790
                'publicTypeHintedMethod',
791
                [new \stdClass()],
792
                'publicTypeHintedMethodDefault'
793
            ],
794
            [
795
                BaseClass::class,
796
                new BaseClass(),
797
                'publicByReferenceMethod',
798
                [],
799
                'publicByReferenceMethodDefault'
800
            ],
801
            [
802
                ClassWithSelfHint::class,
803
                new ClassWithSelfHint(),
804
                'selfHintMethod',
805
                ['parameter' => $selfHintParam],
806
                $selfHintParam
807
            ],
808
            [
809
                ClassWithAbstractPublicMethod::class,
810
                new EmptyClass(), // EmptyClass just used to not make reflection explode when synchronizing properties
811
                'publicAbstractMethod',
812
                [],
813
                null
814
            ],
815
            [
816
                ClassWithMethodWithByRefVariadicFunction::class,
817
                new ClassWithMethodWithByRefVariadicFunction(),
818
                'tuz',
819
                ['Ocramius', 'Malukenho'],
820
                ['Ocramius', 'changed']
821
            ],
822
        ];
823
    }
824
825
    /**
826
     * Generates a list of object | invoked method | parameters | expected result for methods that cause lazy-loading
827
     * of a ghost object
828
     *
829
     * @return array
830
     */
831
    public function getProxyInitializingMethods()
832
    {
833
        $methods = [
834
            [
835
                BaseClass::class,
836
                new BaseClass(),
837
                'publicPropertyGetter',
838
                [],
839
                'publicPropertyDefault'
840
            ],
841
            [
842
                BaseClass::class,
843
                new BaseClass(),
844
                'protectedPropertyGetter',
845
                [],
846
                'protectedPropertyDefault'
847
            ],
848
            [
849
                BaseClass::class,
850
                new BaseClass(),
851
                'privatePropertyGetter',
852
                [],
853
                'privatePropertyDefault'
854
            ],
855
            [
856
                ClassWithMethodWithVariadicFunction::class,
857
                new ClassWithMethodWithVariadicFunction(),
858
                'foo',
859
                ['Ocramius', 'Malukenho'],
860
                null
861
            ],
862
        ];
863
864
        return $methods;
865
    }
866
867
    /**
868
     * Generates a list of object | invoked method | parameters | expected result for methods DON'T cause lazy-loading
869
     *
870
     * @return array
871
     */
872
    public function getProxyNonInitializingMethods()
873
    {
874
        return $this->getProxyMethods();
875
    }
876
877
    /**
878
     * Generates proxies and instances with a public property to feed to the property accessor methods
879
     *
880
     * @return array
881
     */
882
    public function getPropertyAccessProxies()
883
    {
884
        $instance1 = new BaseClass();
885
        $proxyName1 = $this->generateProxy(get_class($instance1));
886
        $instance2 = new BaseClass();
887
        $proxyName2 = $this->generateProxy(get_class($instance2));
888
889
        return [
890
            [
891
                $instance1,
892
                new $proxyName1($this->createInitializer(BaseClass::class, $instance1)),
893
                'publicProperty',
894
                'publicPropertyDefault',
895
            ],
896
            [
897
                $instance2,
898
                unserialize(
899
                    serialize(new $proxyName2($this->createInitializer(BaseClass::class, $instance2)))
900
                ),
901
                'publicProperty',
902
                'publicPropertyDefault',
903
            ],
904
        ];
905
    }
906
907
    /**
908
     * @dataProvider skipPropertiesFixture
909
     *
910
     * @param string  $className
911
     * @param string  $propertyClass
912
     * @param string  $propertyName
913
     * @param mixed   $expected
914
     * @param mixed[] $proxyOptions
915
     */
916
    public function testInitializationIsSkippedForSkippedProperties(
917
        $className,
918
        $propertyClass,
919
        $propertyName,
920
        array $proxyOptions,
921
        $expected
922
    ) {
923
        $proxy       = $this->generateProxy($className, $proxyOptions);
924
        $ghostObject = $proxy::staticProxyConstructor(function () use ($propertyName) {
925
            $this->fail(sprintf('The Property "%s" was not expected to be lazy-loaded', $propertyName));
926
        });
927
928
        $property = new ReflectionProperty($propertyClass, $propertyName);
929
        $property->setAccessible(true);
930
931
        $this->assertSame($expected, $property->getValue($ghostObject));
932
    }
933
934
    /**
935
     * @dataProvider skipPropertiesFixture
936
     *
937
     * @param string  $className
938
     * @param string  $propertyClass
939
     * @param string  $propertyName
940
     * @param mixed[] $proxyOptions
941
     */
942
    public function testSkippedPropertiesAreNotOverwrittenOnInitialization(
943
        $className,
944
        $propertyClass,
945
        $propertyName,
946
        array $proxyOptions
947
    ) {
948
        $proxyName   = $this->generateProxy($className, $proxyOptions);
949
        /* @var $ghostObject GhostObjectInterface */
950
        $ghostObject = $proxyName::staticProxyConstructor(function ($proxy, $method, $params, & $initializer) {
951
            $initializer = null;
952
953
            return true;
954
        });
955
956
        $property = new ReflectionProperty($propertyClass, $propertyName);
957
958
        $property->setAccessible(true);
959
960
        $value = uniqid('', true);
961
962
        $property->setValue($ghostObject, $value);
963
964
        $this->assertTrue($ghostObject->initializeProxy());
965
966
        $this->assertSame(
967
            $value,
968
            $property->getValue($ghostObject),
969
            'Property should not be changed by proxy initialization'
970
        );
971
    }
972
973
    /**
974
     * @group 265
975
     */
976
    public function testWillForwardVariadicByRefArguments()
977
    {
978
        $proxyName   = $this->generateProxy(ClassWithMethodWithByRefVariadicFunction::class);
979
        /* @var $object ClassWithMethodWithByRefVariadicFunction */
980
        $object = $proxyName::staticProxyConstructor(function ($proxy, $method, $params, & $initializer) {
981
            $initializer = null;
982
983
            return true;
984
        });
985
986
        $parameters = ['a', 'b', 'c'];
987
988
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
989
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
990
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
991
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
992
    }
993
994
    /**
995
     * @group 265
996
     */
997
    public function testWillForwardDynamicArguments()
998
    {
999
        $proxyName   = $this->generateProxy(ClassWithDynamicArgumentsMethod::class);
1000
        /* @var $object ClassWithDynamicArgumentsMethod */
1001
        $object = $proxyName::staticProxyConstructor(function () {
1002
        });
1003
1004
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1005
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
1006
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
1007
    }
1008
1009
    /**
1010
     * @return mixed[] in order:
1011
     *                  - the class to be proxied
1012
     *                  - the class owning the property to be checked
1013
     *                  - the property name
1014
     *                  - the options to be passed to the generator
1015
     *                  - the expected value of the property
1016
     */
1017
    public function skipPropertiesFixture()
1018
    {
1019
        return [
1020
            [
1021
                ClassWithPublicProperties::class,
1022
                ClassWithPublicProperties::class,
1023
                'property9',
1024
                [
1025
                    'skippedProperties' => ["property9"]
1026
                ],
1027
                'property9',
1028
            ],
1029
            [
1030
                ClassWithProtectedProperties::class,
1031
                ClassWithProtectedProperties::class,
1032
                'property9',
1033
                [
1034
                    'skippedProperties' => ["\0*\0property9"]
1035
                ],
1036
                'property9',
1037
            ],
1038
            [
1039
                ClassWithPrivateProperties::class,
1040
                ClassWithPrivateProperties::class,
1041
                'property9',
1042
                [
1043
                    'skippedProperties' => [
1044
                        "\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property9"
1045
                    ]
1046
                ],
1047
                'property9',
1048
            ],
1049
            [
1050
                ClassWithCollidingPrivateInheritedProperties::class,
1051
                ClassWithCollidingPrivateInheritedProperties::class,
1052
                'property0',
1053
                [
1054
                    'skippedProperties' => [
1055
                        "\0ProxyManagerTestAsset\\ClassWithCollidingPrivateInheritedProperties\0property0"
1056
                    ]
1057
                ],
1058
                'childClassProperty0',
1059
            ],
1060
            [
1061
                ClassWithCollidingPrivateInheritedProperties::class,
1062
                ClassWithPrivateProperties::class,
1063
                'property0',
1064
                [
1065
                    'skippedProperties' => [
1066
                        "\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property0"
1067
                    ]
1068
                ],
1069
                'property0',
1070
            ],
1071
        ];
1072
    }
1073
}
1074