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

LazyLoadingGhostFunctionalTest::generateProxy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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