Completed
Push — master ( 2f25cf...59cc7f )
by Marco
43:54 queued 18:55
created

LazyLoadingGhostFunctionalTest   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 1312
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
dl 0
loc 1312
c 0
b 0
f 0
wmc 54
lcom 1
cbo 21
rs 5.2798

47 Methods

Rating   Name   Duplication   Size   Complexity  
A testMethodCallsAfterUnSerialization() 0 23 1
A testPropertyUnset() 0 8 1
A testCanWriteToArrayKeysInPublicProperty() 0 17 1
A testWillNotModifyRetrievedPublicProperties() 0 17 1
A testWillModifyByRefRetrievedPublicProperties() 0 17 1
A testKeepsInitializerWhenNotOverwitten() 0 13 1
A testKeepsInitializedPublicProperties() 0 20 1
A testPublicPropertyDefaultWillBePreserved() 0 10 1
A testProtectedPropertyDefaultWillBePreserved() 0 14 1
A testPrivatePropertyDefaultWillBePreserved() 0 14 1
A testMultiLevelPrivatePropertiesDefaultsWillBePreserved() 0 17 1
A testMultiLevelPrivatePropertiesByRefInitialization() 0 22 1
A testGetPropertyFromDifferentProxyInstances() 0 34 1
A testSetPrivatePropertyOnDifferentProxyInstances() 0 23 1
A testIssetPrivatePropertyOnDifferentProxyInstances() 0 24 1
A testUnsetPrivatePropertyOnDifferentProxyInstances() 0 26 1
A testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse() 0 32 1
A testByRefInitialization() 0 27 2
A testPropertyAbsence() 0 6 1
A testPropertyExistence() 0 5 1
A testMethodCallsThatLazyLoadTheObject() 0 22 1
A testMethodCallsThatDoNotLazyLoadTheObject() 0 27 1
A testMethodCallsAfterCloning() 0 22 1
A testPropertyReadAccess() 0 9 1
A testPropertyWriteAccess() 0 9 1
A testByRefInitializationOfTypedProperties() 0 27 1
A testWillBehaveLikeObjectWithNormalConstructor() 0 21 1
A testInitializeProxyWillReturnTrueOnSuccessfulInitialization() 0 14 1
A generateProxy() 0 14 1
A createInitializer() 0 44 4
B getProxyMethods() 0 57 1
A getProxyInitializingMethods() 0 33 1
A getProxyNonInitializingMethods() 0 4 1
A getPropertyAccessProxies() 0 24 1
A testInitializationIsSkippedForSkippedProperties() 0 17 1
A testSkippedPropertiesAreNotOverwrittenOnInitialization() 0 32 1
A testWillForwardVariadicByRefArguments() 0 17 1
A testWillForwardDynamicArguments() 0 11 1
A skipPropertiesFixture() 0 50 1
A testWillLazyLoadMembersOfOtherProxiesWithTheSamePrivateScope() 0 27 1
A testWillAccessMembersOfOtherDeSerializedProxiesWithTheSamePrivateScope() 0 26 1
A testWillAccessMembersOfOtherClonedProxiesWithTheSamePrivateScope() 0 26 1
A getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() 0 49 1
A testFriendObjectWillNotCauseLazyLoadingOnSkippedProperty() 0 49 2
A testClonedSkippedPropertiesArePreserved() 0 49 1
A testWillExecuteLogicInAVoidMethod() 0 26 1
A isPropertyInitialized() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like LazyLoadingGhostFunctionalTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LazyLoadingGhostFunctionalTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace ProxyManagerTest\Functional;
6
7
use BadMethodCallException;
8
use PHPUnit\Framework\TestCase;
9
use PHPUnit_Framework_MockObject_MockObject as Mock;
10
use ProxyManager\Generator\ClassGenerator;
11
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
12
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
13
use ProxyManager\Proxy\GhostObjectInterface;
14
use ProxyManager\Proxy\LazyLoadingInterface;
15
use ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator;
16
use ProxyManager\ProxyGenerator\Util\Properties;
17
use ProxyManagerTestAsset\BaseClass;
18
use ProxyManagerTestAsset\ClassWithAbstractPublicMethod;
19
use ProxyManagerTestAsset\ClassWithCollidingPrivateInheritedProperties;
20
use ProxyManagerTestAsset\ClassWithCounterConstructor;
21
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
22
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
23
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
24
use ProxyManagerTestAsset\ClassWithMixedProperties;
25
use ProxyManagerTestAsset\ClassWithMixedPropertiesAndAccessorMethods;
26
use ProxyManagerTestAsset\ClassWithMixedTypedProperties;
27
use ProxyManagerTestAsset\ClassWithParentHint;
28
use ProxyManagerTestAsset\ClassWithPrivateProperties;
29
use ProxyManagerTestAsset\ClassWithProtectedProperties;
30
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
31
use ProxyManagerTestAsset\ClassWithPublicProperties;
32
use ProxyManagerTestAsset\ClassWithSelfHint;
33
use ProxyManagerTestAsset\EmptyClass;
34
use ProxyManagerTestAsset\OtherObjectAccessClass;
35
use ProxyManagerTestAsset\VoidCounter;
36
use ReflectionClass;
37
use ReflectionProperty;
38
use stdClass;
39
use function array_key_exists;
40
use function array_values;
41
use function get_class;
42
use function get_parent_class;
43
use function random_int;
44
use function serialize;
45
use function sprintf;
46
use function str_replace;
47
use function uniqid;
48
use function unserialize;
49
50
/**
51
 * Tests for {@see \ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator} produced objects
52
 *
53
 * @group Functional
54
 * @coversNothing
55
 */
56
class LazyLoadingGhostFunctionalTest extends TestCase
57
{
58
    /**
59
     * @param mixed[] $params
60
     * @param mixed   $expectedValue
61
     *
62
     * @dataProvider getProxyInitializingMethods
63
     */
64
    public function testMethodCallsThatLazyLoadTheObject(
65
        string $className,
66
        object $instance,
67
        string $method,
68
        array $params,
69
        $expectedValue
70
    ) : void {
71
        $proxyName = $this->generateProxy($className);
72
73
        /** @var GhostObjectInterface $proxy */
74
        $proxy = $proxyName::staticProxyConstructor($this->createInitializer($className, $instance));
75
76
        self::assertFalse($proxy->isProxyInitialized());
77
78
        /** @var callable $callProxyMethod */
79
        $callProxyMethod = [$proxy, $method];
80
        $parameterValues = array_values($params);
81
82
        self::assertInternalType('callable', $callProxyMethod);
83
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
84
        self::assertTrue($proxy->isProxyInitialized());
85
    }
86
87
    /**
88
     * @param mixed[] $params
89
     * @param mixed   $expectedValue
90
     *
91
     * @dataProvider getProxyNonInitializingMethods
92
     */
93
    public function testMethodCallsThatDoNotLazyLoadTheObject(
94
        string $className,
95
        object $instance,
96
        string $method,
97
        array $params,
98
        $expectedValue
99
    ) : void {
100
        $proxyName         = $this->generateProxy($className);
101
        $initializeMatcher = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
102
103
        $initializeMatcher->expects(self::never())->method('__invoke'); // should not initialize the proxy
104
105
        /** @var GhostObjectInterface $proxy */
106
        $proxy = $proxyName::staticProxyConstructor(
107
            $this->createInitializer($className, $instance, $initializeMatcher)
108
        );
109
110
        self::assertFalse($proxy->isProxyInitialized());
111
112
        /** @var callable $callProxyMethod */
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
     * @param mixed[] $params
123
     * @param mixed   $expectedValue
124
     *
125
     * @dataProvider getProxyMethods
126
     */
127
    public function testMethodCallsAfterUnSerialization(
128
        string $className,
129
        object $instance,
130
        string $method,
131
        array $params,
132
        $expectedValue
133
    ) : void {
134
        $proxyName = $this->generateProxy($className);
135
136
        /** @var GhostObjectInterface $proxy */
137
        $proxy = unserialize(serialize($proxyName::staticProxyConstructor(
138
            $this->createInitializer($className, $instance)
139
        )));
140
141
        self::assertTrue($proxy->isProxyInitialized());
142
143
        /** @var callable $callProxyMethod */
144
        $callProxyMethod = [$proxy, $method];
145
        $parameterValues = array_values($params);
146
147
        self::assertInternalType('callable', $callProxyMethod);
148
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
149
    }
150
151
    /**
152
     * @param mixed[] $params
153
     * @param mixed   $expectedValue
154
     *
155
     * @dataProvider getProxyMethods
156
     */
157
    public function testMethodCallsAfterCloning(
158
        string $className,
159
        object $instance,
160
        string $method,
161
        array $params,
162
        $expectedValue
163
    ) : void {
164
        $proxyName = $this->generateProxy($className);
165
166
        /** @var GhostObjectInterface $proxy */
167
        $proxy  = $proxyName::staticProxyConstructor($this->createInitializer($className, $instance));
168
        $cloned = clone $proxy;
169
170
        self::assertTrue($cloned->isProxyInitialized());
171
172
        /** @var callable $callProxyMethod */
173
        $callProxyMethod = [$proxy, $method];
174
        $parameterValues = array_values($params);
175
176
        self::assertInternalType('callable', $callProxyMethod);
177
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
178
    }
179
180
    /**
181
     * @param mixed $propertyValue
182
     *
183
     * @dataProvider getPropertyAccessProxies
184
     */
185
    public function testPropertyReadAccess(
186
        object $instance,
187
        GhostObjectInterface $proxy,
188
        string $publicProperty,
189
        $propertyValue
190
    ) : void {
191
        self::assertSame($propertyValue, $proxy->$publicProperty);
192
        self::assertTrue($proxy->isProxyInitialized());
193
    }
194
195
    /**
196
     * @dataProvider getPropertyAccessProxies
197
     */
198
    public function testPropertyWriteAccess(object $instance, GhostObjectInterface $proxy, string $publicProperty
199
    ) : void
200
    {
201
        $newValue               = uniqid('', true);
202
        $proxy->$publicProperty = $newValue;
203
204
        self::assertTrue($proxy->isProxyInitialized());
205
        self::assertSame($newValue, $proxy->$publicProperty);
206
    }
207
208
    /**
209
     * @dataProvider getPropertyAccessProxies
210
     */
211
    public function testPropertyExistence(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
212
    {
213
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
214
        self::assertTrue($proxy->isProxyInitialized());
215
    }
216
217
    /**
218
     * @dataProvider getPropertyAccessProxies
219
     */
220
    public function testPropertyAbsence(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
221
    {
222
        $proxy->$publicProperty = null;
223
        self::assertFalse(isset($proxy->$publicProperty));
224
        self::assertTrue($proxy->isProxyInitialized());
225
    }
226
227
    /**
228
     * @dataProvider getPropertyAccessProxies
229
     */
230
    public function testPropertyUnset(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
231
    {
232
        unset($proxy->$publicProperty);
233
234
        self::assertTrue($proxy->isProxyInitialized());
235
        self::assertTrue(isset($instance->$publicProperty));
236
        self::assertFalse(isset($proxy->$publicProperty));
237
    }
238
239
    /**
240
     * Verifies that accessing a public property containing an array behaves like in a normal context
241
     */
242
    public function testCanWriteToArrayKeysInPublicProperty() : void
243
    {
244
        $instance    = new ClassWithPublicArrayProperty();
245
        $className   = get_class($instance);
246
        $initializer = $this->createInitializer($className, $instance);
247
        $proxyName   = $this->generateProxy($className);
248
        /** @var ClassWithPublicArrayProperty $proxy */
249
        $proxy = $proxyName::staticProxyConstructor($initializer);
250
251
        $proxy->arrayProperty['foo'] = 'bar';
252
253
        self::assertSame('bar', $proxy->arrayProperty['foo']);
254
255
        $proxy->arrayProperty = ['tab' => 'taz'];
256
257
        self::assertSame(['tab' => 'taz'], $proxy->arrayProperty);
258
    }
259
260
    /**
261
     * Verifies that public properties retrieved via `__get` don't get modified in the object itself
262
     */
263
    public function testWillNotModifyRetrievedPublicProperties() : void
264
    {
265
        $instance    = new ClassWithPublicProperties();
266
        $className   = get_class($instance);
267
        $initializer = $this->createInitializer($className, $instance);
268
        $proxyName   = $this->generateProxy($className);
269
        /** @var ClassWithPublicProperties $proxy */
270
        $proxy    = $proxyName::staticProxyConstructor($initializer);
271
        $variable = $proxy->property0;
272
273
        self::assertSame('property0', $variable);
274
275
        $variable = 'foo';
276
277
        self::assertSame('property0', $proxy->property0);
278
        self::assertSame('foo', $variable);
279
    }
280
281
    /**
282
     * Verifies that public properties references retrieved via `__get` modify in the object state
283
     */
284
    public function testWillModifyByRefRetrievedPublicProperties() : void
285
    {
286
        $instance    = new ClassWithPublicProperties();
287
        $className   = get_class($instance);
288
        $initializer = $this->createInitializer($className, $instance);
289
        $proxyName   = $this->generateProxy($className);
290
        /** @var ClassWithPublicProperties $proxy */
291
        $proxy    = $proxyName::staticProxyConstructor($initializer);
292
        $variable = &$proxy->property0;
293
294
        self::assertSame('property0', $variable);
295
296
        $variable = 'foo';
297
298
        self::assertSame('foo', $proxy->property0);
299
        self::assertSame('foo', $variable);
300
    }
301
302
    public function testKeepsInitializerWhenNotOverwitten() : void
303
    {
304
        $instance    = new BaseClass();
305
        $proxyName   = $this->generateProxy(get_class($instance));
306
        $initializer = static function () : void {
307
        };
308
        /** @var GhostObjectInterface $proxy */
309
        $proxy = $proxyName::staticProxyConstructor($initializer);
310
311
        $proxy->initializeProxy();
312
313
        self::assertSame($initializer, $proxy->getProxyInitializer());
314
    }
315
316
    /**
317
     * Verifies that public properties are not being initialized multiple times
318
     */
319
    public function testKeepsInitializedPublicProperties() : void
320
    {
321
        $instance    = new BaseClass();
322
        $proxyName   = $this->generateProxy(get_class($instance));
323
        $initializer = static function (BaseClass $proxy, string $method, $parameters, & $initializer) : void {
324
            $initializer           = null;
325
            $proxy->publicProperty = 'newValue';
326
        };
327
        /** @var GhostObjectInterface&BaseClass $proxy */
0 ignored issues
show
Documentation introduced by
The doc-type GhostObjectInterface&BaseClass could not be parsed: Unknown type name "GhostObjectInterface&BaseClass" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
328
        $proxy = $proxyName::staticProxyConstructor($initializer);
329
330
        $proxy->initializeProxy();
331
        self::assertSame('newValue', $proxy->publicProperty);
332
333
        $proxy->publicProperty = 'otherValue';
334
335
        $proxy->initializeProxy();
336
337
        self::assertSame('otherValue', $proxy->publicProperty);
338
    }
339
340
    /**
341
     * Verifies that properties' default values are preserved
342
     */
343
    public function testPublicPropertyDefaultWillBePreserved() : void
344
    {
345
        $instance  = new ClassWithPublicProperties();
346
        $proxyName = $this->generateProxy(get_class($instance));
347
        /** @var ClassWithPublicProperties $proxy */
348
        $proxy = $proxyName::staticProxyConstructor(static function () : void {
349
        });
350
351
        self::assertSame('property0', $proxy->property0);
352
    }
353
354
    /**
355
     * Verifies that protected properties' default values are preserved
356
     */
357
    public function testProtectedPropertyDefaultWillBePreserved() : void
358
    {
359
        $instance  = new ClassWithProtectedProperties();
360
        $proxyName = $this->generateProxy(get_class($instance));
361
        /** @var ClassWithProtectedProperties $proxy */
362
        $proxy = $proxyName::staticProxyConstructor(static function () : void {
363
        });
364
365
        // Check protected property via reflection
366
        $reflectionProperty = new ReflectionProperty($instance, 'property0');
367
        $reflectionProperty->setAccessible(true);
368
369
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
370
    }
371
372
    /**
373
     * Verifies that private properties' default values are preserved
374
     */
375
    public function testPrivatePropertyDefaultWillBePreserved() : void
376
    {
377
        $instance  = new ClassWithPrivateProperties();
378
        $proxyName = $this->generateProxy(get_class($instance));
379
        /** @var ClassWithPrivateProperties $proxy */
380
        $proxy = $proxyName::staticProxyConstructor(static function () : void {
381
        });
382
383
        // Check protected property via reflection
384
        $reflectionProperty = new ReflectionProperty($instance, 'property0');
385
        $reflectionProperty->setAccessible(true);
386
387
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
388
    }
389
390
    /**
391
     * @group 159
392
     * @group 192
393
     */
394
    public function testMultiLevelPrivatePropertiesDefaultsWillBePreserved() : void
395
    {
396
        $instance  = new ClassWithCollidingPrivateInheritedProperties();
397
        $proxyName = $this->generateProxy(ClassWithCollidingPrivateInheritedProperties::class);
398
        /** @var ClassWithPrivateProperties $proxy */
399
        $proxy = $proxyName::staticProxyConstructor(static function () : void {
400
        });
401
402
        $childProperty  = new ReflectionProperty($instance, 'property0');
403
        $parentProperty = new ReflectionProperty(get_parent_class($instance), 'property0');
404
405
        $childProperty->setAccessible(true);
406
        $parentProperty->setAccessible(true);
407
408
        self::assertSame('childClassProperty0', $childProperty->getValue($proxy));
409
        self::assertSame('property0', $parentProperty->getValue($proxy));
410
    }
411
412
    /**
413
     * @group 159
414
     * @group 192
415
     */
416
    public function testMultiLevelPrivatePropertiesByRefInitialization() : void
417
    {
418
        $class     = ClassWithCollidingPrivateInheritedProperties::class;
419
        $proxyName = $this->generateProxy($class);
420
        /** @var ClassWithPrivateProperties $proxy */
421
        $proxy = $proxyName::staticProxyConstructor(
422
            static function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
423
                $initializer                                                 = null;
424
                $properties["\0" . $class . "\0property0"]                   = 'foo';
425
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'bar';
426
            }
427
        );
428
429
        $childProperty  = new ReflectionProperty($class, 'property0');
430
        $parentProperty = new ReflectionProperty(get_parent_class($class), 'property0');
431
432
        $childProperty->setAccessible(true);
433
        $parentProperty->setAccessible(true);
434
435
        self::assertSame('foo', $childProperty->getValue($proxy));
436
        self::assertSame('bar', $parentProperty->getValue($proxy));
437
    }
438
439
    /**
440
     * @group 159
441
     * @group 192
442
     *
443
     * Test designed to verify that the cached logic does take into account the fact that
444
     * proxies are different instances
445
     */
446
    public function testGetPropertyFromDifferentProxyInstances() : void
447
    {
448
        $class     = ClassWithCollidingPrivateInheritedProperties::class;
449
        $proxyName = $this->generateProxy($class);
450
451
        /** @var ClassWithPrivateProperties $proxy1 */
452
        $proxy1 = $proxyName::staticProxyConstructor(
453
            static function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
454
                $initializer                                                 = null;
455
                $properties["\0" . $class . "\0property0"]                   = 'foo';
456
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'bar';
457
            }
458
        );
459
        /** @var ClassWithPrivateProperties $proxy2 */
460
        $proxy2 = $proxyName::staticProxyConstructor(
461
            static function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
462
                $initializer                                                 = null;
463
                $properties["\0" . $class . "\0property0"]                   = 'baz';
464
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'tab';
465
            }
466
        );
467
468
        $childProperty  = new ReflectionProperty($class, 'property0');
469
        $parentProperty = new ReflectionProperty(get_parent_class($class), 'property0');
470
471
        $childProperty->setAccessible(true);
472
        $parentProperty->setAccessible(true);
473
474
        self::assertSame('foo', $childProperty->getValue($proxy1));
475
        self::assertSame('bar', $parentProperty->getValue($proxy1));
476
477
        self::assertSame('baz', $childProperty->getValue($proxy2));
478
        self::assertSame('tab', $parentProperty->getValue($proxy2));
479
    }
480
481
    /**
482
     * @group 159
483
     * @group 192
484
     *
485
     * Test designed to verify that the cached logic does take into account the fact that
486
     * proxies are different instances
487
     */
488
    public function testSetPrivatePropertyOnDifferentProxyInstances() : void
489
    {
490
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
491
        $proxyName = $this->generateProxy($class);
492
493
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
494
        $proxy1 = $proxyName::staticProxyConstructor(
495
            static function ($proxy, $method, $params, & $initializer) : void {
496
                $initializer = null;
497
            }
498
        );
499
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
500
        $proxy2 = $proxyName::staticProxyConstructor(
501
            static function ($proxy, $method, $params, & $initializer) : void {
502
                $initializer = null;
503
            }
504
        );
505
506
        $proxy1->set('privateProperty', 'private1');
507
        $proxy2->set('privateProperty', 'private2');
508
        self::assertSame('private1', $proxy1->get('privateProperty'));
509
        self::assertSame('private2', $proxy2->get('privateProperty'));
510
    }
511
512
    /**
513
     * @group 159
514
     * @group 192
515
     *
516
     * Test designed to verify that the cached logic does take into account the fact that
517
     * proxies are different instances
518
     */
519
    public function testIssetPrivatePropertyOnDifferentProxyInstances() : void
520
    {
521
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
522
        $proxyName = $this->generateProxy($class);
523
524
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
525
        $proxy1 = $proxyName::staticProxyConstructor(
526
            static function ($proxy, $method, $params, & $initializer) : void {
527
                $initializer = null;
528
            }
529
        );
530
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
531
        $proxy2 = $proxyName::staticProxyConstructor(
532
            static function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
533
                $initializer                                     = null;
534
                $properties["\0" . $class . "\0privateProperty"] = null;
535
            }
536
        );
537
538
        self::assertTrue($proxy1->has('privateProperty'));
539
        self::assertFalse($proxy2->has('privateProperty'));
540
        self::assertTrue($proxy1->has('privateProperty'));
541
        self::assertFalse($proxy2->has('privateProperty'));
542
    }
543
544
    /**
545
     * @group 159
546
     * @group 192
547
     *
548
     * Test designed to verify that the cached logic does take into account the fact that
549
     * proxies are different instances
550
     */
551
    public function testUnsetPrivatePropertyOnDifferentProxyInstances() : void
552
    {
553
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
554
        $proxyName = $this->generateProxy($class);
555
556
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
557
        $proxy1 = $proxyName::staticProxyConstructor(
558
            static function ($proxy, $method, $params, & $initializer) : void {
559
                $initializer = null;
560
            }
561
        );
562
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
563
        $proxy2 = $proxyName::staticProxyConstructor(
564
            static function ($proxy, $method, $params, & $initializer) : void {
565
                $initializer = null;
566
            }
567
        );
568
569
        self::assertTrue($proxy1->has('privateProperty'));
570
        $proxy2->remove('privateProperty');
571
        self::assertFalse($proxy2->has('privateProperty'));
572
        self::assertTrue($proxy1->has('privateProperty'));
573
        $proxy1->remove('privateProperty');
574
        self::assertFalse($proxy1->has('privateProperty'));
575
        self::assertFalse($proxy2->has('privateProperty'));
576
    }
577
578
    /**
579
     * @group 159
580
     * @group 192
581
     *
582
     * Test designed to verify that the cached logic does take into account the fact that
583
     * proxies are different instances
584
     */
585
    public function testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse() : void
586
    {
587
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
588
        $proxyName = $this->generateProxy($class);
589
590
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
591
        $proxy1 = $proxyName::staticProxyConstructor(
592
            static function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
593
                $initializer                                     = null;
594
                $properties['publicProperty']                    = false;
595
                $properties["\0*\0protectedProperty"]            = false;
596
                $properties["\0" . $class . "\0privateProperty"] = false;
597
            }
598
        );
599
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
600
        $proxy2 = $proxyName::staticProxyConstructor(
601
            static function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
602
                $initializer                                     = null;
603
                $properties['publicProperty']                    = null;
604
                $properties["\0*\0protectedProperty"]            = null;
605
                $properties["\0" . $class . "\0privateProperty"] = null;
606
            }
607
        );
608
609
        self::assertTrue($proxy1->has('protectedProperty'));
610
        self::assertTrue($proxy1->has('publicProperty'));
611
        self::assertTrue($proxy1->has('privateProperty'));
612
613
        self::assertFalse($proxy2->has('protectedProperty'));
614
        self::assertFalse($proxy2->has('publicProperty'));
615
        self::assertFalse($proxy2->has('privateProperty'));
616
    }
617
618
    public function testByRefInitialization() : void
619
    {
620
        $proxyName = $this->generateProxy(ClassWithMixedProperties::class);
621
        /** @var ClassWithPrivateProperties $proxy */
622
        $proxy = $proxyName::staticProxyConstructor(
623
            static function ($proxy, $method, $params, & $initializer, array $properties) : void {
624
                $initializer                                                               = null;
625
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty0"] = 'private0';
626
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty1"] = 'private1';
627
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty2"] = 'private2';
628
                $properties["\0*\0protectedProperty0"]                                     = 'protected0';
629
                $properties["\0*\0protectedProperty1"]                                     = 'protected1';
630
                $properties["\0*\0protectedProperty2"]                                     = 'protected2';
631
                $properties['publicProperty0']                                             = 'public0';
632
                $properties['publicProperty1']                                             = 'public1';
633
                $properties['publicProperty2']                                             = 'public2';
634
            }
635
        );
636
637
        $reflectionClass = new ReflectionClass(ClassWithMixedProperties::class);
638
639
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
640
            $property->setAccessible(true);
641
642
            self::assertSame(str_replace('Property', '', $property->getName()), $property->getValue($proxy));
643
        }
644
    }
645
646
    public function testByRefInitializationOfTypedProperties() : void
647
    {
648
        $proxyName = $this->generateProxy(ClassWithMixedTypedProperties::class);
649
        /** @var ClassWithMixedTypedProperties $proxy */
650
        $proxy = $proxyName::staticProxyConstructor(
651
            static function ($proxy, $method, $params, & $initializer, array $properties) : void {
652
                $initializer                                                                         = null;
653
                $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"] = 'private0';
654
                $properties["\0*\0protectedStringProperty"]                                          = 'protected0';
655
                $properties['publicStringProperty']                                                  = 'public0';
656
            }
657
        );
658
659
        $reflectionClass = new ReflectionClass(ClassWithMixedTypedProperties::class);
660
661
        $properties = Properties::fromReflectionClass($reflectionClass)->getInstanceProperties();
662
663
        $privateProperty   = $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"];
664
        $protectedProperty = $properties["\0*\0protectedStringProperty"];
665
666
        $privateProperty->setAccessible(true);
667
        $protectedProperty->setAccessible(true);
668
669
        self::assertSame('private0', $privateProperty->getValue($proxy));
670
        self::assertSame('protected0', $properties["\0*\0protectedStringProperty"]->getValue($proxy));
671
        self::assertSame('public0', $proxy->publicStringProperty);
672
    }
673
674
    /**
675
     * @group 115
676
     * @group 175
677
     */
678
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
679
    {
680
        $instance = new ClassWithCounterConstructor(10);
681
682
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
683
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
684
        $instance->__construct(3);
685
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
686
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
687
688
        $proxyName = $this->generateProxy(get_class($instance));
689
690
        /** @var ClassWithCounterConstructor $proxy */
691
        $proxy = new $proxyName(15);
692
693
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
694
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
695
        $proxy->__construct(5);
696
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
697
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
698
    }
699
700
    public function testInitializeProxyWillReturnTrueOnSuccessfulInitialization() : void
701
    {
702
        $proxyName = $this->generateProxy(ClassWithMixedTypedProperties::class);
703
704
        /** @var GhostObjectInterface $proxy */
705
        $proxy = $proxyName::staticProxyConstructor($this->createInitializer(
706
            ClassWithMixedTypedProperties::class,
707
            new ClassWithMixedTypedProperties()
708
        ));
709
710
        self::assertTrue($proxy->initializeProxy());
711
        self::assertTrue($proxy->isProxyInitialized());
712
        self::assertFalse($proxy->initializeProxy());
713
    }
714
715
    /**
716
     * Generates a proxy for the given class name, and retrieves its class name
717
     *
718
     * @param mixed[] $proxyOptions
719
     */
720
    private function generateProxy(string $parentClassName, array $proxyOptions = []) : string
721
    {
722
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
723
        $generatedClass     = new ClassGenerator($generatedClassName);
724
725
        (new LazyLoadingGhostGenerator())->generate(
726
            new ReflectionClass($parentClassName),
727
            $generatedClass,
728
            $proxyOptions
729
        );
730
        (new EvaluatingGeneratorStrategy())->generate($generatedClass);
731
732
        return $generatedClassName;
733
    }
734
735
    private function createInitializer(string $className, object $realInstance, ?Mock $initializerMatcher = null
736
    ) : callable
737
    {
738
        /** @var callable|Mock $initializerMatcher */
739
        if (! $initializerMatcher) {
740
            $initializerMatcher = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
741
742
            $initializerMatcher
743
                ->expects(self::once())
744
                ->method('__invoke')
745
                ->with(self::logicalAnd(
746
                    self::isInstanceOf(GhostObjectInterface::class),
747
                    self::isInstanceOf($className)
748
                ));
749
        }
750
751
        self::assertInternalType('callable', $initializerMatcher);
752
753
        return static function (
754
            GhostObjectInterface $proxy,
755
            $method,
756
            $params,
757
            & $initializer
758
        ) use (
759
            $initializerMatcher,
760
            $realInstance
761
        ) : bool {
762
            $initializer     = null;
763
            $reflectionClass = new ReflectionClass($realInstance);
764
765
            foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
766
                if (! self::isPropertyInitialized($realInstance, $property)) {
767
                    continue;
768
                }
769
770
                $property->setAccessible(true);
771
                $property->setValue($proxy, $property->getValue($realInstance));
772
            }
773
774
            $initializerMatcher($proxy, $method, $params);
775
776
            return true;
777
        };
778
    }
779
780
    /**
781
     * Generates a list of object | invoked method | parameters | expected result
782
     *
783
     * @return null[][]|string[][]|object[][]|mixed[][][]
784
     */
785
    public function getProxyMethods() : array
786
    {
787
        $selfHintParam = new ClassWithSelfHint();
788
        $empty         = new EmptyClass();
789
790
        return [
791
            [
792
                BaseClass::class,
793
                new BaseClass(),
794
                'publicMethod',
795
                [],
796
                'publicMethodDefault',
797
            ],
798
            [
799
                BaseClass::class,
800
                new BaseClass(),
801
                'publicTypeHintedMethod',
802
                [new stdClass()],
803
                'publicTypeHintedMethodDefault',
804
            ],
805
            [
806
                BaseClass::class,
807
                new BaseClass(),
808
                'publicByReferenceMethod',
809
                [],
810
                'publicByReferenceMethodDefault',
811
            ],
812
            [
813
                ClassWithSelfHint::class,
814
                new ClassWithSelfHint(),
815
                'selfHintMethod',
816
                ['parameter' => $selfHintParam],
817
                $selfHintParam,
818
            ],
819
            [
820
                ClassWithParentHint::class,
821
                new ClassWithParentHint(),
822
                'parentHintMethod',
823
                ['parameter' => $empty],
824
                $empty,
825
            ],
826
            [
827
                ClassWithAbstractPublicMethod::class,
828
                new EmptyClass(), // EmptyClass just used to not make reflection explode when synchronizing properties
829
                'publicAbstractMethod',
830
                [],
831
                null,
832
            ],
833
            [
834
                ClassWithMethodWithByRefVariadicFunction::class,
835
                new ClassWithMethodWithByRefVariadicFunction(),
836
                'tuz',
837
                ['Ocramius', 'Malukenho'],
838
                ['Ocramius', 'changed'],
839
            ],
840
        ];
841
    }
842
843
    /**
844
     * Generates a list of object | invoked method | parameters | expected result for methods that cause lazy-loading
845
     * of a ghost object
846
     *
847
     * @return string[][]|object[][]|mixed[][][]|null[][]
848
     */
849
    public function getProxyInitializingMethods() : array
850
    {
851
        return [
852
            [
853
                BaseClass::class,
854
                new BaseClass(),
855
                'publicPropertyGetter',
856
                [],
857
                'publicPropertyDefault',
858
            ],
859
            [
860
                BaseClass::class,
861
                new BaseClass(),
862
                'protectedPropertyGetter',
863
                [],
864
                'protectedPropertyDefault',
865
            ],
866
            [
867
                BaseClass::class,
868
                new BaseClass(),
869
                'privatePropertyGetter',
870
                [],
871
                'privatePropertyDefault',
872
            ],
873
            [
874
                ClassWithMethodWithVariadicFunction::class,
875
                new ClassWithMethodWithVariadicFunction(),
876
                'foo',
877
                ['Ocramius', 'Malukenho'],
878
                null,
879
            ],
880
        ];
881
    }
882
883
    /**
884
     * Generates a list of object | invoked method | parameters | expected result for methods DON'T cause lazy-loading
885
     *
886
     * @return null[][]|string[][]|object[][]|mixed[][][]
887
     */
888
    public function getProxyNonInitializingMethods() : array
889
    {
890
        return $this->getProxyMethods();
891
    }
892
893
    /**
894
     * Generates proxies and instances with a public property to feed to the property accessor methods
895
     *
896
     * @return string[][]|object[][]
897
     */
898
    public function getPropertyAccessProxies() : array
899
    {
900
        $instance1  = new BaseClass();
901
        $proxyName1 = $this->generateProxy(get_class($instance1));
902
        $instance2  = new BaseClass();
903
        $proxyName2 = $this->generateProxy(get_class($instance2));
904
905
        return [
906
            [
907
                $instance1,
908
                new $proxyName1($this->createInitializer(BaseClass::class, $instance1)),
909
                'publicProperty',
910
                'publicPropertyDefault',
911
            ],
912
            [
913
                $instance2,
914
                unserialize(
915
                    serialize(new $proxyName2($this->createInitializer(BaseClass::class, $instance2)))
916
                ),
917
                'publicProperty',
918
                'publicPropertyDefault',
919
            ],
920
        ];
921
    }
922
923
    /**
924
     * @param mixed   $expected
925
     * @param mixed[] $proxyOptions
926
     *
927
     * @dataProvider skipPropertiesFixture
928
     */
929
    public function testInitializationIsSkippedForSkippedProperties(
930
        string $className,
931
        string $propertyClass,
932
        string $propertyName,
933
        array $proxyOptions,
934
        $expected
935
    ) : void {
936
        $proxy       = $this->generateProxy($className, $proxyOptions);
937
        $ghostObject = $proxy::staticProxyConstructor(static function () use ($propertyName) : void {
938
            self::fail(sprintf('The Property "%s" was not expected to be lazy-loaded', $propertyName));
939
        });
940
941
        $property = new ReflectionProperty($propertyClass, $propertyName);
942
        $property->setAccessible(true);
943
944
        self::assertSame($expected, $property->getValue($ghostObject));
945
    }
946
947
    /**
948
     * @param mixed[] $proxyOptions
949
     *
950
     * @dataProvider skipPropertiesFixture
951
     */
952
    public function testSkippedPropertiesAreNotOverwrittenOnInitialization(
953
        string $className,
954
        string $propertyClass,
955
        string $propertyName,
956
        array $proxyOptions
957
    ) : void {
958
        $proxyName = $this->generateProxy($className, $proxyOptions);
959
        /** @var GhostObjectInterface $ghostObject */
960
        $ghostObject = $proxyName::staticProxyConstructor(
961
            static function ($proxy, string $method, $params, & $initializer) : bool {
962
                $initializer = null;
963
964
                return true;
965
            }
966
        );
967
968
        $property = new ReflectionProperty($propertyClass, $propertyName);
969
970
        $property->setAccessible(true);
971
972
        $value = uniqid('', true);
973
974
        $property->setValue($ghostObject, $value);
975
976
        self::assertTrue($ghostObject->initializeProxy());
977
978
        self::assertSame(
979
            $value,
980
            $property->getValue($ghostObject),
981
            'Property should not be changed by proxy initialization'
982
        );
983
    }
984
985
    /**
986
     * @group 265
987
     */
988
    public function testWillForwardVariadicByRefArguments() : void
989
    {
990
        $proxyName = $this->generateProxy(ClassWithMethodWithByRefVariadicFunction::class);
991
        /** @var ClassWithMethodWithByRefVariadicFunction $object */
992
        $object = $proxyName::staticProxyConstructor(static function ($proxy, string $method, $params, & $initializer) : bool {
993
            $initializer = null;
994
995
            return true;
996
        });
997
998
        $parameters = ['a', 'b', 'c'];
999
1000
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1001
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
1002
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
1003
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
1004
    }
1005
1006
    /**
1007
     * @group 265
1008
     */
1009
    public function testWillForwardDynamicArguments() : void
1010
    {
1011
        $proxyName = $this->generateProxy(ClassWithDynamicArgumentsMethod::class);
1012
        /** @var ClassWithDynamicArgumentsMethod $object */
1013
        $object = $proxyName::staticProxyConstructor(static function () : void {
1014
        });
1015
1016
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1017
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
1018
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
1019
    }
1020
1021
    /**
1022
     * @return mixed[] in order:
1023
     *                  - the class to be proxied
1024
     *                  - the class owning the property to be checked
1025
     *                  - the property name
1026
     *                  - the options to be passed to the generator
1027
     *                  - the expected value of the property
1028
     */
1029
    public function skipPropertiesFixture() : array
1030
    {
1031
        return [
1032
            [
1033
                ClassWithPublicProperties::class,
1034
                ClassWithPublicProperties::class,
1035
                'property9',
1036
                [
1037
                    'skippedProperties' => ['property9'],
1038
                ],
1039
                'property9',
1040
            ],
1041
            [
1042
                ClassWithProtectedProperties::class,
1043
                ClassWithProtectedProperties::class,
1044
                'property9',
1045
                [
1046
                    'skippedProperties' => ["\0*\0property9"],
1047
                ],
1048
                'property9',
1049
            ],
1050
            [
1051
                ClassWithPrivateProperties::class,
1052
                ClassWithPrivateProperties::class,
1053
                'property9',
1054
                [
1055
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property9"],
1056
                ],
1057
                'property9',
1058
            ],
1059
            [
1060
                ClassWithCollidingPrivateInheritedProperties::class,
1061
                ClassWithCollidingPrivateInheritedProperties::class,
1062
                'property0',
1063
                [
1064
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithCollidingPrivateInheritedProperties\0property0"],
1065
                ],
1066
                'childClassProperty0',
1067
            ],
1068
            [
1069
                ClassWithCollidingPrivateInheritedProperties::class,
1070
                ClassWithPrivateProperties::class,
1071
                'property0',
1072
                [
1073
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property0"],
1074
                ],
1075
                'property0',
1076
            ],
1077
        ];
1078
    }
1079
1080
    /**
1081
     * @group        276
1082
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1083
     */
1084
    public function testWillLazyLoadMembersOfOtherProxiesWithTheSamePrivateScope(
1085
        object $callerObject,
1086
        string $method,
1087
        string $propertyIndex,
1088
        string $expectedValue
1089
    ) : void {
1090
        $proxyName = $this->generateProxy(get_class($callerObject));
1091
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1092
        $proxy = $proxyName::staticProxyConstructor(
1093
            static function ($proxy, $method, $params, & $initializer, array $props) use ($propertyIndex, $expectedValue
1094
            ) : void {
1095
                $initializer = null;
1096
1097
                $props[$propertyIndex] = $expectedValue;
1098
            }
1099
        );
1100
1101
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1102
1103
        $accessor = [$callerObject, $method];
1104
1105
        self::assertInternalType('callable', $accessor);
1106
1107
        self::assertFalse($proxy->isProxyInitialized());
1108
        self::assertSame($expectedValue, $accessor($proxy));
1109
        self::assertTrue($proxy->isProxyInitialized());
1110
    }
1111
1112
    /**
1113
     * @group        276
1114
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1115
     */
1116
    public function testWillAccessMembersOfOtherDeSerializedProxiesWithTheSamePrivateScope(
1117
        object $callerObject,
1118
        string $method,
1119
        string $propertyIndex,
1120
        string $expectedValue
1121
    ) : void {
1122
        $proxyName = $this->generateProxy(get_class($callerObject));
1123
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1124
        $proxy = unserialize(serialize($proxyName::staticProxyConstructor(
1125
            static function ($proxy, $method, $params, & $initializer, array $props) use ($propertyIndex, $expectedValue
1126
            ) : void {
1127
                $initializer = null;
1128
1129
                $props[$propertyIndex] = $expectedValue;
1130
            }
1131
        )));
1132
1133
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1134
1135
        $accessor = [$callerObject, $method];
1136
1137
        self::assertInternalType('callable', $accessor);
1138
1139
        self::assertTrue($proxy->isProxyInitialized());
1140
        self::assertSame($expectedValue, $accessor($proxy));
1141
    }
1142
1143
    /**
1144
     * @group        276
1145
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1146
     */
1147
    public function testWillAccessMembersOfOtherClonedProxiesWithTheSamePrivateScope(
1148
        object $callerObject,
1149
        string $method,
1150
        string $propertyIndex,
1151
        string $expectedValue
1152
    ) : void {
1153
        $proxyName = $this->generateProxy(get_class($callerObject));
1154
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1155
        $proxy = clone $proxyName::staticProxyConstructor(
1156
            static function ($proxy, $method, $params, & $initializer, array $props) use ($propertyIndex, $expectedValue
1157
            ) : void {
1158
                $initializer = null;
1159
1160
                $props[$propertyIndex] = $expectedValue;
1161
            }
1162
        );
1163
1164
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1165
1166
        $accessor = [$callerObject, $method];
1167
1168
        self::assertInternalType('callable', $accessor);
1169
1170
        self::assertTrue($proxy->isProxyInitialized());
1171
        self::assertSame($expectedValue, $accessor($proxy));
1172
    }
1173
1174
    /** @return string[][]|object[][] */
1175
    public function getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() : array
1176
    {
1177
        $proxyClass = $this->generateProxy(OtherObjectAccessClass::class);
1178
1179
        return [
1180
            OtherObjectAccessClass::class . '#$privateProperty'                => [
1181
                new OtherObjectAccessClass(),
1182
                'getPrivateProperty',
1183
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1184
                uniqid('', true),
1185
            ],
1186
            OtherObjectAccessClass::class . '#$protectedProperty'              => [
1187
                new OtherObjectAccessClass(),
1188
                'getProtectedProperty',
1189
                "\0*\0protectedProperty",
1190
                uniqid('', true),
1191
            ],
1192
            OtherObjectAccessClass::class . '#$publicProperty'                 => [
1193
                new OtherObjectAccessClass(),
1194
                'getPublicProperty',
1195
                'publicProperty',
1196
                uniqid('', true),
1197
            ],
1198
            '(proxy) ' . OtherObjectAccessClass::class . '#$privateProperty'   => [
1199
                $proxyClass::staticProxyConstructor(static function () : void {
1200
                    self::fail('Should never be initialized, as its values aren\'t accessed');
1201
                }),
1202
                'getPrivateProperty',
1203
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1204
                uniqid('', true),
1205
            ],
1206
            '(proxy) ' . OtherObjectAccessClass::class . '#$protectedProperty' => [
1207
                $proxyClass::staticProxyConstructor(static function () : void {
1208
                    self::fail('Should never be initialized, as its values aren\'t accessed');
1209
                }),
1210
                'getProtectedProperty',
1211
                "\0*\0protectedProperty",
1212
                uniqid('', true),
1213
            ],
1214
            '(proxy) ' . OtherObjectAccessClass::class . '#$publicProperty'    => [
1215
                $proxyClass::staticProxyConstructor(static function () : void {
1216
                    self::fail('Should never be initialized, as its values aren\'t accessed');
1217
                }),
1218
                'getPublicProperty',
1219
                'publicProperty',
1220
                uniqid('', true),
1221
            ],
1222
        ];
1223
    }
1224
1225
    /**
1226
     * @group 276
1227
     */
1228
    public function testFriendObjectWillNotCauseLazyLoadingOnSkippedProperty() : void
1229
    {
1230
        $proxyName = $this->generateProxy(
1231
            OtherObjectAccessClass::class,
1232
            [
1233
                'skippedProperties' => [
1234
                    "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1235
                    "\0*\0protectedProperty",
1236
                    'publicProperty',
1237
                ],
1238
            ]
1239
        );
1240
1241
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1242
        $proxy = $proxyName::staticProxyConstructor(static function () : void {
1243
            throw new BadMethodCallException('The proxy should never be initialized, as all properties are skipped');
1244
        });
1245
1246
        self::assertInstanceOf(OtherObjectAccessClass::class, $proxy);
1247
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1248
1249
        $privatePropertyValue   = uniqid('', true);
1250
        $protectedPropertyValue = uniqid('', true);
1251
        $publicPropertyValue    = uniqid('', true);
1252
1253
        $reflectionPrivateProperty = new ReflectionProperty(OtherObjectAccessClass::class, 'privateProperty');
1254
1255
        $reflectionPrivateProperty->setAccessible(true);
1256
        $reflectionPrivateProperty->setValue($proxy, $privatePropertyValue);
1257
1258
        $reflectionProtectedProperty = new ReflectionProperty(OtherObjectAccessClass::class, 'protectedProperty');
1259
1260
        $reflectionProtectedProperty->setAccessible(true);
1261
        $reflectionProtectedProperty->setValue($proxy, $protectedPropertyValue);
1262
1263
        $proxy->publicProperty = $publicPropertyValue;
1264
1265
        $friendObject = new OtherObjectAccessClass();
1266
1267
        self::assertInstanceOf(OtherObjectAccessClass::class, $proxy);
1268
1269
        if (! ($proxy instanceof OtherObjectAccessClass)) {
1270
            return;
1271
        }
1272
1273
        self::assertSame($privatePropertyValue, $friendObject->getPrivateProperty($proxy));
0 ignored issues
show
Documentation introduced by
$proxy is of type object<ProxyManagerTestA...OtherObjectAccessClass>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1274
        self::assertSame($protectedPropertyValue, $friendObject->getProtectedProperty($proxy));
0 ignored issues
show
Documentation introduced by
$proxy is of type object<ProxyManagerTestA...OtherObjectAccessClass>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1275
        self::assertSame($publicPropertyValue, $friendObject->getPublicProperty($proxy));
0 ignored issues
show
Documentation introduced by
$proxy is of type object<ProxyManagerTestA...OtherObjectAccessClass>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1276
    }
1277
1278
    public function testClonedSkippedPropertiesArePreserved() : void
1279
    {
1280
        $proxyName = $this->generateProxy(
1281
            BaseClass::class,
1282
            [
1283
                'skippedProperties' => [
1284
                    "\0" . BaseClass::class . "\0privateProperty",
1285
                    "\0*\0protectedProperty",
1286
                    'publicProperty',
1287
                ],
1288
            ]
1289
        );
1290
1291
        /** @var BaseClass|GhostObjectInterface $proxy */
1292
        $proxy = $proxyName::staticProxyConstructor(static function ($proxy) : void {
1293
            $proxy->setProxyInitializer(null);
1294
        });
1295
1296
        self::assertInstanceOf(BaseClass::class, $proxy);
1297
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1298
1299
        $reflectionPrivate   = new ReflectionProperty(BaseClass::class, 'privateProperty');
1300
        $reflectionProtected = new ReflectionProperty(BaseClass::class, 'protectedProperty');
1301
1302
        $reflectionPrivate->setAccessible(true);
1303
        $reflectionProtected->setAccessible(true);
1304
1305
        $privateValue   = uniqid('', true);
1306
        $protectedValue = uniqid('', true);
1307
        $publicValue    = uniqid('', true);
1308
1309
        $reflectionPrivate->setValue($proxy, $privateValue);
1310
        $reflectionProtected->setValue($proxy, $protectedValue);
1311
        $proxy->publicProperty = $publicValue;
1312
1313
        self::assertFalse($proxy->isProxyInitialized());
0 ignored issues
show
Bug introduced by
The method isProxyInitialized does only exist in ProxyManager\Proxy\GhostObjectInterface, but not in ProxyManagerTestAsset\BaseClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1314
1315
        $clone = clone $proxy;
1316
1317
        self::assertFalse($proxy->isProxyInitialized());
1318
        self::assertTrue($clone->isProxyInitialized());
1319
1320
        self::assertSame($privateValue, $reflectionPrivate->getValue($proxy));
1321
        self::assertSame($privateValue, $reflectionPrivate->getValue($clone));
1322
        self::assertSame($protectedValue, $reflectionProtected->getValue($proxy));
1323
        self::assertSame($protectedValue, $reflectionProtected->getValue($clone));
1324
        self::assertSame($publicValue, $proxy->publicProperty);
1325
        self::assertSame($publicValue, $clone->publicProperty);
1326
    }
1327
1328
    /**
1329
     * @group 327
1330
     */
1331
    public function testWillExecuteLogicInAVoidMethod() : void
1332
    {
1333
        $proxyName = $this->generateProxy(VoidCounter::class);
1334
1335
        $initialCounter = random_int(10, 1000);
1336
1337
        /** @var VoidCounter|LazyLoadingInterface $proxy */
1338
        $proxy = $proxyName::staticProxyConstructor(
1339
            static function (VoidCounter $proxy, $method, $params, & $initializer, array $props) use ($initialCounter) : bool {
1340
                $initializer = null;
1341
1342
                $props['counter'] = $initialCounter;
1343
1344
                return true;
1345
            }
1346
        );
1347
1348
        self::assertInstanceOf(VoidCounter::class, $proxy);
1349
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1350
1351
        $increment = random_int(1001, 10000);
1352
1353
        $proxy->increment($increment);
0 ignored issues
show
Bug introduced by
The method increment does only exist in ProxyManagerTestAsset\VoidCounter, but not in ProxyManager\Proxy\LazyLoadingInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1354
1355
        self::assertSame($initialCounter + $increment, $proxy->counter);
1356
    }
1357
1358
    private static function isPropertyInitialized(object $object, ReflectionProperty $property) : bool
1359
    {
1360
        return array_key_exists(
1361
            ($property->isProtected() ? "\0*\0" : '')
1362
            . ($property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : '')
1363
            . $property->getName(),
1364
            (array) $object
1365
        );
1366
    }
1367
}
1368