Completed
Pull Request — master (#467)
by Marco
22:49
created

LazyLoadingGhostFunctionalTest   D

Complexity

Total Complexity 55

Size/Duplication

Total Lines 1509
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 19
dl 0
loc 1509
rs 4.8
c 0
b 0
f 0

49 Methods

Rating   Name   Duplication   Size   Complexity  
A testMethodCallsThatLazyLoadTheObject() 0 18 1
A testMethodCallsThatDoNotLazyLoadTheObject() 0 26 1
A testMethodCallsAfterUnSerialization() 0 21 1
A testMethodCallsAfterCloning() 0 22 1
A testPropertyReadAccess() 0 9 1
A testPropertyWriteAccess() 0 9 1
A testPropertyExistence() 0 5 1
A testPropertyAbsence() 0 6 1
A testPropertyUnset() 0 8 1
A testCanWriteToArrayKeysInPublicProperty() 0 15 1
A testWillNotModifyRetrievedPublicProperties() 0 15 1
A testWillModifyByRefRetrievedPublicProperties() 0 16 1
A testKeepsInitializerWhenNotOverwitten() 0 15 1
A testKeepsInitializedPublicProperties() 0 26 1
A testPublicPropertyDefaultWillBePreserved() 0 11 1
A testProtectedPropertyDefaultWillBePreserved() 0 15 1
A testPrivatePropertyDefaultWillBePreserved() 0 15 1
A testMultiLevelPrivatePropertiesDefaultsWillBePreserved() 0 18 1
A testMultiLevelPrivatePropertiesByRefInitialization() 0 28 1
A testGetPropertyFromDifferentProxyInstances() 0 48 1
A testSetPrivatePropertyOnDifferentProxyInstances() 0 35 1
A testIssetPrivatePropertyOnDifferentProxyInstances() 0 37 1
A testUnsetPrivatePropertyOnDifferentProxyInstances() 0 38 1
A testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse() 0 46 1
A testByRefInitialization() 0 34 2
A testByRefInitializationOfTypedProperties() 0 34 1
A testWillBehaveLikeObjectWithNormalConstructor() 0 20 1
A testInitializeProxyWillReturnTrueOnSuccessfulInitialization() 0 14 1
A generateProxy() 0 17 1
A makeProxy() 0 10 1
A createInitializer() 0 41 4
B getProxyMethods() 0 57 1
A getProxyInitializingMethods() 0 33 1
A getProxyNonInitializingMethods() 0 4 1
A getPropertyAccessProxies() 0 31 1
A testInitializationIsSkippedForSkippedProperties() 0 22 1
A testSkippedPropertiesAreNotOverwrittenOnInitialization() 0 37 1
A testWillForwardVariadicByRefArguments() 0 23 1
A testWillForwardDynamicArguments() 0 13 1
A skipPropertiesFixture() 0 50 1
A testWillLazyLoadMembersOfOtherProxiesWithTheSamePrivateScope() 0 31 1
A testWillAccessMembersOfOtherDeSerializedProxiesWithTheSamePrivateScope() 0 32 1
A testWillAccessMembersOfOtherClonedProxiesWithTheSamePrivateScope() 0 30 1
B getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() 0 64 1
A testFriendObjectWillNotCauseLazyLoadingOnSkippedProperty() 0 39 1
A testClonedSkippedPropertiesArePreserved() 0 47 1
A testWillExecuteLogicInAVoidMethod() 0 31 1
A isPropertyInitialized() 0 9 3
A assertByRefVariableValueSame() 0 4 1

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 Closure;
9
use PHPUnit\Framework\MockObject\MockObject as Mock;
10
use PHPUnit\Framework\TestCase;
11
use ProxyManager\Factory\LazyLoadingGhostFactory;
12
use ProxyManager\Generator\ClassGenerator;
13
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
14
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
15
use ProxyManager\Proxy\GhostObjectInterface;
16
use ProxyManager\Proxy\LazyLoadingInterface;
17
use ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator;
18
use ProxyManager\ProxyGenerator\Util\Properties;
19
use ProxyManagerTestAsset\BaseClass;
20
use ProxyManagerTestAsset\CallableInterface;
21
use ProxyManagerTestAsset\ClassWithAbstractPublicMethod;
22
use ProxyManagerTestAsset\ClassWithCollidingPrivateInheritedProperties;
23
use ProxyManagerTestAsset\ClassWithCounterConstructor;
24
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
25
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
26
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
27
use ProxyManagerTestAsset\ClassWithMixedProperties;
28
use ProxyManagerTestAsset\ClassWithMixedPropertiesAndAccessorMethods;
29
use ProxyManagerTestAsset\ClassWithMixedTypedProperties;
30
use ProxyManagerTestAsset\ClassWithParentHint;
31
use ProxyManagerTestAsset\ClassWithPrivateProperties;
32
use ProxyManagerTestAsset\ClassWithProtectedProperties;
33
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
34
use ProxyManagerTestAsset\ClassWithPublicProperties;
35
use ProxyManagerTestAsset\ClassWithSelfHint;
36
use ProxyManagerTestAsset\EmptyClass;
37
use ProxyManagerTestAsset\OtherObjectAccessClass;
38
use ProxyManagerTestAsset\VoidCounter;
39
use ReflectionClass;
40
use ReflectionProperty;
41
use stdClass;
42
use function array_key_exists;
43
use function array_values;
44
use function get_class;
45
use function get_parent_class;
46
use function random_int;
47
use function serialize;
48
use function sprintf;
49
use function str_replace;
50
use function uniqid;
51
use function unserialize;
52
53
/**
54
 * Tests for {@see \ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator} produced objects
55
 *
56
 * @group Functional
57
 * @coversNothing
58
 */
59
final class LazyLoadingGhostFunctionalTest extends TestCase
60
{
61
    /**
62
     * @param mixed[] $params
63
     * @param mixed   $expectedValue
64
     *
65
     * @dataProvider getProxyInitializingMethods
66
     *
67
     * @psalm-template OriginalClass
68
     * @psalm-param class-string<OriginalClass> $className
69
     * @psalm-param OriginalClass $instance
70
     */
71
    public function testMethodCallsThatLazyLoadTheObject(
72
        string $className,
73
        object $instance,
74
        string $method,
75
        array $params,
76
        $expectedValue
77
    ) : void {
78
        $proxy = $this->makeProxy($className, $this->createInitializer($className, $instance));
79
80
        self::assertFalse($proxy->isProxyInitialized());
81
82
        $callProxyMethod = [$proxy, $method];
83
        $parameterValues = array_values($params);
84
85
        self::assertIsCallable($callProxyMethod);
86
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
87
        self::assertTrue($proxy->isProxyInitialized());
88
    }
89
90
    /**
91
     * @param mixed[] $params
92
     * @param mixed   $expectedValue
93
     *
94
     * @dataProvider getProxyNonInitializingMethods
95
     *
96
     * @psalm-template OriginalClass
97
     * @psalm-param class-string<OriginalClass> $className
98
     * @psalm-param OriginalClass $instance
99
     */
100
    public function testMethodCallsThatDoNotLazyLoadTheObject(
101
        string $className,
102
        object $instance,
103
        string $method,
104
        array $params,
105
        $expectedValue
106
    ) : void {
107
        $initializeMatcher = $this->createMock(CallableInterface::class);
108
109
        $initializeMatcher->expects(self::never())->method('__invoke'); // should not initialize the proxy
110
111
        /** @var GhostObjectInterface $proxy */
112
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
113
            $className,
114
            $this->createInitializer($className, $instance, $initializeMatcher)
115
        );
116
117
        self::assertFalse($proxy->isProxyInitialized());
118
119
        $callProxyMethod = [$proxy, $method];
120
        $parameterValues = array_values($params);
121
122
        self::assertIsCallable($callProxyMethod);
123
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
124
        self::assertFalse($proxy->isProxyInitialized());
125
    }
126
127
    /**
128
     * @param mixed[] $params
129
     * @param mixed   $expectedValue
130
     *
131
     * @dataProvider getProxyMethods
132
     *
133
     * @psalm-template OriginalClass
134
     * @psalm-param class-string<OriginalClass> $className
135
     * @psalm-param OriginalClass $instance
136
     */
137
    public function testMethodCallsAfterUnSerialization(
138
        string $className,
139
        object $instance,
140
        string $method,
141
        array $params,
142
        $expectedValue
143
    ) : void {
144
        /** @var GhostObjectInterface $proxy */
145
        $proxy = unserialize(serialize((new LazyLoadingGhostFactory())->createProxy(
146
            $className,
147
            $this->createInitializer($className, $instance)
148
        )));
149
150
        self::assertTrue($proxy->isProxyInitialized());
151
152
        $callProxyMethod = [$proxy, $method];
153
        $parameterValues = array_values($params);
154
155
        self::assertIsCallable($callProxyMethod);
156
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
157
    }
158
159
    /**
160
     * @param mixed[] $params
161
     * @param mixed   $expectedValue
162
     *
163
     * @dataProvider getProxyMethods
164
     *
165
     * @psalm-template OriginalClass
166
     * @psalm-param class-string<OriginalClass> $className
167
     * @psalm-param OriginalClass $instance
168
     */
169
    public function testMethodCallsAfterCloning(
170
        string $className,
171
        object $instance,
172
        string $method,
173
        array $params,
174
        $expectedValue
175
    ) : void {
176
        /** @var GhostObjectInterface $proxy */
177
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
178
            $className,
179
            $this->createInitializer($className, $instance)
180
        );
181
        $cloned = clone $proxy;
182
183
        self::assertTrue($cloned->isProxyInitialized());
184
185
        $callProxyMethod = [$proxy, $method];
186
        $parameterValues = array_values($params);
187
188
        self::assertIsCallable($callProxyMethod);
189
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
190
    }
191
192
    /**
193
     * @param mixed $propertyValue
194
     *
195
     * @dataProvider getPropertyAccessProxies
196
     */
197
    public function testPropertyReadAccess(
198
        object $instance,
0 ignored issues
show
Unused Code introduced by
The parameter $instance 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...
199
        GhostObjectInterface $proxy,
200
        string $publicProperty,
201
        $propertyValue
202
    ) : void {
203
        self::assertSame($propertyValue, $proxy->$publicProperty);
204
        self::assertTrue($proxy->isProxyInitialized());
205
    }
206
207
    /**
208
     * @dataProvider getPropertyAccessProxies
209
     */
210
    public function testPropertyWriteAccess(object $instance, GhostObjectInterface $proxy, string $publicProperty
0 ignored issues
show
Unused Code introduced by
The parameter $instance 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...
211
    ) : void
212
    {
213
        $newValue               = uniqid('', true);
214
        $proxy->$publicProperty = $newValue;
215
216
        self::assertTrue($proxy->isProxyInitialized());
217
        self::assertSame($newValue, $proxy->$publicProperty);
218
    }
219
220
    /**
221
     * @dataProvider getPropertyAccessProxies
222
     */
223
    public function testPropertyExistence(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
224
    {
225
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
226
        self::assertTrue($proxy->isProxyInitialized());
227
    }
228
229
    /**
230
     * @dataProvider getPropertyAccessProxies
231
     */
232
    public function testPropertyAbsence(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
0 ignored issues
show
Unused Code introduced by
The parameter $instance 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...
233
    {
234
        $proxy->$publicProperty = null;
235
        self::assertFalse(isset($proxy->$publicProperty));
236
        self::assertTrue($proxy->isProxyInitialized());
237
    }
238
239
    /**
240
     * @dataProvider getPropertyAccessProxies
241
     */
242
    public function testPropertyUnset(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
243
    {
244
        unset($proxy->$publicProperty);
245
246
        self::assertTrue($proxy->isProxyInitialized());
247
        self::assertTrue(isset($instance->$publicProperty));
248
        self::assertFalse(isset($proxy->$publicProperty));
249
    }
250
251
    /**
252
     * Verifies that accessing a public property containing an array behaves like in a normal context
253
     */
254
    public function testCanWriteToArrayKeysInPublicProperty() : void
255
    {
256
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
257
            ClassWithPublicArrayProperty::class,
258
            $this->createInitializer(ClassWithPublicArrayProperty::class, new ClassWithPublicArrayProperty())
0 ignored issues
show
Documentation introduced by
new \ProxyManagerTestAss...thPublicArrayProperty() is of type object<ProxyManagerTestA...ithPublicArrayProperty>, but the function expects a object<ProxyManagerTest\Functional\object>.

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...
259
        );
260
261
        $proxy->arrayProperty['foo'] = 'bar';
262
263
        self::assertByRefVariableValueSame('bar', $proxy->arrayProperty['foo']);
264
265
        $proxy->arrayProperty = ['tab' => 'taz'];
266
267
        self::assertByRefVariableValueSame(['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() : void
274
    {
275
        $proxy    = (new LazyLoadingGhostFactory())->createProxy(
276
            ClassWithPublicProperties::class,
277
            $this->createInitializer(ClassWithPublicProperties::class, new ClassWithPublicProperties())
0 ignored issues
show
Documentation introduced by
new \ProxyManagerTestAss...sWithPublicProperties() is of type object<ProxyManagerTestA...ssWithPublicProperties>, but the function expects a object<ProxyManagerTest\Functional\object>.

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...
278
        );
279
        $variable = $proxy->property0;
280
281
        self::assertByRefVariableValueSame('property0', $variable);
282
283
        $variable = 'foo';
284
285
        self::assertByRefVariableValueSame('property0', $proxy->property0);
286
        self::assertByRefVariableValueSame('foo', $variable);
287
    }
288
289
    /**
290
     * Verifies that public properties references retrieved via `__get` modify in the object state
291
     */
292
    public function testWillModifyByRefRetrievedPublicProperties() : void
293
    {
294
        $instance = new ClassWithPublicProperties();
295
        $proxy    = (new LazyLoadingGhostFactory())->createProxy(
296
            ClassWithPublicProperties::class,
297
            $this->createInitializer(ClassWithPublicProperties::class, $instance)
0 ignored issues
show
Documentation introduced by
$instance is of type object<ProxyManagerTestA...ssWithPublicProperties>, but the function expects a object<ProxyManagerTest\Functional\object>.

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...
298
        );
299
        $variable = &$proxy->property0;
300
301
        self::assertByRefVariableValueSame('property0', $variable);
302
303
        $variable = 'foo';
304
305
        self::assertByRefVariableValueSame('foo', $proxy->property0);
306
        self::assertByRefVariableValueSame('foo', $variable);
307
    }
308
309
    public function testKeepsInitializerWhenNotOverwitten() : void
310
    {
311
        $initializer = static function () : bool {
312
            return true;
313
        };
314
315
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
316
            BaseClass::class,
317
            $initializer
318
        );
319
320
        $proxy->initializeProxy();
321
322
        self::assertSame($initializer, $proxy->getProxyInitializer());
323
    }
324
325
    /**
326
     * Verifies that public properties are not being initialized multiple times
327
     */
328
    public function testKeepsInitializedPublicProperties() : void
329
    {
330
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
331
            BaseClass::class,
332
            static function (
333
                object $proxy,
334
                string $method,
335
                array $parameters,
336
                ?Closure & $initializer
337
            ) : bool {
338
                $initializer           = null;
339
                $proxy->publicProperty = 'newValue';
340
341
                return true;
342
            }
343
        );
344
345
        $proxy->initializeProxy();
346
        self::assertSame('newValue', $proxy->publicProperty);
347
348
        $proxy->publicProperty = 'otherValue';
349
350
        $proxy->initializeProxy();
351
352
        self::assertSame('otherValue', $proxy->publicProperty);
353
    }
354
355
    /**
356
     * Verifies that properties' default values are preserved
357
     */
358
    public function testPublicPropertyDefaultWillBePreserved() : void
359
    {
360
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
361
            ClassWithPublicProperties::class,
362
            static function () : bool {
363
                return true;
364
            }
365
        );
366
367
        self::assertSame('property0', $proxy->property0);
368
    }
369
370
    /**
371
     * Verifies that protected properties' default values are preserved
372
     */
373
    public function testProtectedPropertyDefaultWillBePreserved() : void
374
    {
375
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
376
            ClassWithProtectedProperties::class,
377
            static function () : bool {
378
                return true;
379
            }
380
        );
381
382
        // Check protected property via reflection
383
        $reflectionProperty = new ReflectionProperty(ClassWithProtectedProperties::class, 'property0');
384
        $reflectionProperty->setAccessible(true);
385
386
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
387
    }
388
389
    /**
390
     * Verifies that private properties' default values are preserved
391
     */
392
    public function testPrivatePropertyDefaultWillBePreserved() : void
393
    {
394
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
395
            ClassWithPrivateProperties::class,
396
            static function () : bool {
397
                return true;
398
            }
399
        );
400
401
        // Check protected property via reflection
402
        $reflectionProperty = new ReflectionProperty(ClassWithPrivateProperties::class, 'property0');
403
        $reflectionProperty->setAccessible(true);
404
405
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
406
    }
407
408
    /**
409
     * @group 159
410
     * @group 192
411
     */
412
    public function testMultiLevelPrivatePropertiesDefaultsWillBePreserved() : void
413
    {
414
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
415
            ClassWithCollidingPrivateInheritedProperties::class,
416
            static function () : bool {
417
                return true;
418
            }
419
        );
420
421
        $childProperty  = new ReflectionProperty(ClassWithCollidingPrivateInheritedProperties::class, 'property0');
422
        $parentProperty = new ReflectionProperty(get_parent_class(ClassWithCollidingPrivateInheritedProperties::class), 'property0');
423
424
        $childProperty->setAccessible(true);
425
        $parentProperty->setAccessible(true);
426
427
        self::assertSame('childClassProperty0', $childProperty->getValue($proxy));
428
        self::assertSame('property0', $parentProperty->getValue($proxy));
429
    }
430
431
    /**
432
     * @group 159
433
     * @group 192
434
     */
435
    public function testMultiLevelPrivatePropertiesByRefInitialization() : void
436
    {
437
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
438
            ClassWithCollidingPrivateInheritedProperties::class,
439
            static function (
440
                GhostObjectInterface $proxy,
441
                string $method,
442
                array $params,
443
                ?Closure & $initializer,
444
                array $properties
445
            ) : bool {
446
                $initializer                                                                            = null;
447
                $properties["\0" . ClassWithCollidingPrivateInheritedProperties::class . "\0property0"]                   = 'foo';
448
                $properties["\0" . get_parent_class(ClassWithCollidingPrivateInheritedProperties::class) . "\0property0"] = 'bar';
449
450
                return true;
451
            }
452
        );
453
454
        $childProperty  = new ReflectionProperty(ClassWithCollidingPrivateInheritedProperties::class, 'property0');
455
        $parentProperty = new ReflectionProperty(get_parent_class(ClassWithCollidingPrivateInheritedProperties::class), 'property0');
456
457
        $childProperty->setAccessible(true);
458
        $parentProperty->setAccessible(true);
459
460
        self::assertSame('foo', $childProperty->getValue($proxy));
461
        self::assertSame('bar', $parentProperty->getValue($proxy));
462
    }
463
464
    /**
465
     * @group 159
466
     * @group 192
467
     *
468
     * Test designed to verify that the cached logic does take into account the fact that
469
     * proxies are different instances
470
     */
471
    public function testGetPropertyFromDifferentProxyInstances() : void
472
    {
473
        $factory = new LazyLoadingGhostFactory();
474
        $proxy1  = $factory->createProxy(
475
            ClassWithCollidingPrivateInheritedProperties::class,
476
            static function (
477
                GhostObjectInterface $proxy,
478
                string $method,
479
                array $params,
480
                ?Closure & $initializer,
481
                array $properties
482
            ) : bool {
483
                $initializer                                                                                              = null;
484
                $properties["\0" . ClassWithCollidingPrivateInheritedProperties::class . "\0property0"]                   = 'foo';
485
                $properties["\0" . get_parent_class(ClassWithCollidingPrivateInheritedProperties::class) . "\0property0"] = 'bar';
486
487
                return true;
488
            }
489
        );
490
        $proxy2  = $factory->createProxy(
491
            ClassWithCollidingPrivateInheritedProperties::class,
492
            static function (
493
                GhostObjectInterface $proxy,
494
                string $method,
495
                array $params,
496
                ?Closure & $initializer,
497
                array $properties
498
            ) : bool {
499
                $initializer                                                                                              = null;
500
                $properties["\0" . ClassWithCollidingPrivateInheritedProperties::class . "\0property0"]                   = 'baz';
501
                $properties["\0" . get_parent_class(ClassWithCollidingPrivateInheritedProperties::class) . "\0property0"] = 'tab';
502
503
                return true;
504
            }
505
        );
506
507
        $childProperty  = new ReflectionProperty(ClassWithCollidingPrivateInheritedProperties::class, 'property0');
508
        $parentProperty = new ReflectionProperty(get_parent_class(ClassWithCollidingPrivateInheritedProperties::class), 'property0');
509
510
        $childProperty->setAccessible(true);
511
        $parentProperty->setAccessible(true);
512
513
        self::assertSame('foo', $childProperty->getValue($proxy1));
514
        self::assertSame('bar', $parentProperty->getValue($proxy1));
515
516
        self::assertSame('baz', $childProperty->getValue($proxy2));
517
        self::assertSame('tab', $parentProperty->getValue($proxy2));
518
    }
519
520
    /**
521
     * @group 159
522
     * @group 192
523
     *
524
     * Test designed to verify that the cached logic does take into account the fact that
525
     * proxies are different instances
526
     */
527
    public function testSetPrivatePropertyOnDifferentProxyInstances() : void
528
    {
529
        $factory = new LazyLoadingGhostFactory();
530
        $proxy1  = $factory->createProxy(
531
            ClassWithMixedPropertiesAndAccessorMethods::class,
532
            static function (
533
                GhostObjectInterface $proxy,
534
                string $method,
535
                array $params,
536
                ?Closure & $initializer
537
            ) : bool {
538
                $initializer = null;
539
540
                return true;
541
            }
542
        );
543
        $proxy2  = $factory->createProxy(
544
            ClassWithMixedPropertiesAndAccessorMethods::class,
545
            static function (
546
                GhostObjectInterface $proxy,
547
                string $method,
548
                array $params,
549
                ?Closure & $initializer
550
            ) : bool {
551
                $initializer = null;
552
553
                return true;
554
            }
555
        );
556
557
        $proxy1->set('privateProperty', 'private1');
558
        $proxy2->set('privateProperty', 'private2');
559
        self::assertSame('private1', $proxy1->get('privateProperty'));
560
        self::assertSame('private2', $proxy2->get('privateProperty'));
561
    }
562
563
    /**
564
     * @group 159
565
     * @group 192
566
     *
567
     * Test designed to verify that the cached logic does take into account the fact that
568
     * proxies are different instances
569
     */
570
    public function testIssetPrivatePropertyOnDifferentProxyInstances() : void
571
    {
572
        $factory = new LazyLoadingGhostFactory();
573
        $proxy1  = $factory->createProxy(
574
            ClassWithMixedPropertiesAndAccessorMethods::class,
575
            static function (
576
                GhostObjectInterface $proxy,
577
                string $method,
578
                array $params,
579
                ?Closure & $initializer
580
            ) : bool {
581
                $initializer = null;
582
583
                return true;
584
            }
585
        );
586
        $proxy2  = $factory->createProxy(
587
            ClassWithMixedPropertiesAndAccessorMethods::class,
588
            static function (
589
                GhostObjectInterface $proxy,
590
                string $method,
591
                array $params,
592
                ?Closure & $initializer,
593
                array $properties
594
            ) : bool {
595
                $initializer                                                                                = null;
596
                $properties["\0" . ClassWithMixedPropertiesAndAccessorMethods::class . "\0privateProperty"] = null;
597
598
                return true;
599
            }
600
        );
601
602
        self::assertTrue($proxy1->has('privateProperty'));
603
        self::assertFalse($proxy2->has('privateProperty'));
604
        self::assertTrue($proxy1->has('privateProperty'));
605
        self::assertFalse($proxy2->has('privateProperty'));
606
    }
607
608
    /**
609
     * @group 159
610
     * @group 192
611
     *
612
     * Test designed to verify that the cached logic does take into account the fact that
613
     * proxies are different instances
614
     */
615
    public function testUnsetPrivatePropertyOnDifferentProxyInstances() : void
616
    {
617
        $factory = new LazyLoadingGhostFactory();
618
        $proxy1  = $factory->createProxy(
619
            ClassWithMixedPropertiesAndAccessorMethods::class,
620
            static function (
621
                GhostObjectInterface $proxy,
622
                string $method,
623
                array $params,
624
                ?Closure & $initializer
625
            ) : bool {
626
                $initializer = null;
627
628
                return true;
629
            }
630
        );
631
        $proxy2  = $factory->createProxy(
632
            ClassWithMixedPropertiesAndAccessorMethods::class,
633
            static function (
634
                GhostObjectInterface $proxy,
635
                string $method,
636
                array $params,
637
                ?Closure & $initializer
638
            ) : bool {
639
                $initializer = null;
640
641
                return true;
642
            }
643
        );
644
645
        self::assertTrue($proxy1->has('privateProperty'));
646
        $proxy2->remove('privateProperty');
647
        self::assertFalse($proxy2->has('privateProperty'));
648
        self::assertTrue($proxy1->has('privateProperty'));
649
        $proxy1->remove('privateProperty');
650
        self::assertFalse($proxy1->has('privateProperty'));
651
        self::assertFalse($proxy2->has('privateProperty'));
652
    }
653
654
    /**
655
     * @group 159
656
     * @group 192
657
     *
658
     * Test designed to verify that the cached logic does take into account the fact that
659
     * proxies are different instances
660
     */
661
    public function testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse() : void
662
    {
663
        $factory = new LazyLoadingGhostFactory();
664
        $proxy1  = $factory->createProxy(
665
            ClassWithMixedPropertiesAndAccessorMethods::class,
666
            static function (
667
                GhostObjectInterface $proxy,
668
                string $method,
669
                array $params,
670
                ?Closure & $initializer,
671
                array $properties
672
            ) : bool {
673
                $initializer                                                                                = null;
674
                $properties['publicProperty']                                                               = false;
675
                $properties["\0*\0protectedProperty"]                                                       = false;
676
                $properties["\0" . ClassWithMixedPropertiesAndAccessorMethods::class . "\0privateProperty"] = false;
677
678
                return true;
679
            }
680
        );
681
        $proxy2  = $factory->createProxy(
682
            ClassWithMixedPropertiesAndAccessorMethods::class,
683
            static function (
684
                GhostObjectInterface $proxy,
685
                string $method,
686
                array $params,
687
                ?Closure & $initializer,
688
                array $properties
689
            ) : bool {
690
                $initializer                                                                                = null;
691
                $properties['publicProperty']                                                               = null;
692
                $properties["\0*\0protectedProperty"]                                                       = null;
693
                $properties["\0" . ClassWithMixedPropertiesAndAccessorMethods::class . "\0privateProperty"] = null;
694
695
                return true;
696
            }
697
        );
698
699
        self::assertTrue($proxy1->has('protectedProperty'));
700
        self::assertTrue($proxy1->has('publicProperty'));
701
        self::assertTrue($proxy1->has('privateProperty'));
702
703
        self::assertFalse($proxy2->has('protectedProperty'));
704
        self::assertFalse($proxy2->has('publicProperty'));
705
        self::assertFalse($proxy2->has('privateProperty'));
706
    }
707
708
    public function testByRefInitialization() : void
709
    {
710
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
711
            ClassWithMixedProperties::class,
712
            static function (
713
                GhostObjectInterface $proxy,
714
                string $method,
715
                array $params,
716
                ?Closure & $initializer,
717
                array $properties
718
            ) : bool {
719
                $initializer                                                               = null;
720
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty0"] = 'private0';
721
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty1"] = 'private1';
722
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty2"] = 'private2';
723
                $properties["\0*\0protectedProperty0"]                                     = 'protected0';
724
                $properties["\0*\0protectedProperty1"]                                     = 'protected1';
725
                $properties["\0*\0protectedProperty2"]                                     = 'protected2';
726
                $properties['publicProperty0']                                             = 'public0';
727
                $properties['publicProperty1']                                             = 'public1';
728
                $properties['publicProperty2']                                             = 'public2';
729
730
                return true;
731
            }
732
        );
733
734
        $reflectionClass = new ReflectionClass(ClassWithMixedProperties::class);
735
736
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
737
            $property->setAccessible(true);
738
739
            self::assertSame(str_replace('Property', '', $property->getName()), $property->getValue($proxy));
740
        }
741
    }
742
743
    public function testByRefInitializationOfTypedProperties() : void
744
    {
745
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
746
            ClassWithMixedTypedProperties::class,
747
            static function (
748
                GhostObjectInterface $proxy,
749
                string $method,
750
                array $params,
751
                ?Closure & $initializer,
752
                array $properties
753
            ) : bool {
754
                $initializer                                                                         = null;
755
                $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"] = 'private0';
756
                $properties["\0*\0protectedStringProperty"]                                          = 'protected0';
757
                $properties['publicStringProperty']                                                  = 'public0';
758
759
                return true;
760
            }
761
        );
762
763
        $reflectionClass = new ReflectionClass(ClassWithMixedTypedProperties::class);
764
765
        $properties = Properties::fromReflectionClass($reflectionClass)->getInstanceProperties();
766
767
        $privateProperty   = $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"];
768
        $protectedProperty = $properties["\0*\0protectedStringProperty"];
769
770
        $privateProperty->setAccessible(true);
771
        $protectedProperty->setAccessible(true);
772
773
        self::assertSame('private0', $privateProperty->getValue($proxy));
774
        self::assertSame('protected0', $properties["\0*\0protectedStringProperty"]->getValue($proxy));
775
        self::assertSame('public0', $proxy->publicStringProperty);
776
    }
777
778
    /**
779
     * @group 115
780
     * @group 175
781
     */
782
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
783
    {
784
        $instance = new ClassWithCounterConstructor(10);
785
786
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
787
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
788
        $instance->__construct(3);
789
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
790
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
791
792
        $proxyName = $this->generateProxy(ClassWithCounterConstructor::class);
793
794
        $proxy = new $proxyName(15);
795
796
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
797
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
798
        $proxy->__construct(5);
799
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
800
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
801
    }
802
803
    public function testInitializeProxyWillReturnTrueOnSuccessfulInitialization() : void
804
    {
805
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
806
            ClassWithMixedTypedProperties::class,
807
            $this->createInitializer(
808
                ClassWithMixedTypedProperties::class,
809
                new ClassWithMixedTypedProperties()
810
            )
811
        );
812
813
        self::assertTrue($proxy->initializeProxy());
814
        self::assertTrue($proxy->isProxyInitialized());
815
        self::assertFalse($proxy->initializeProxy());
816
    }
817
818
    /**
819
     * Generates a proxy for the given class name, and retrieves its class name
820
     *
821
     * @param array{skippedProperties?: array<int, string>} $proxyOptions
822
     *
823
     * @psalm-template OriginalClass
824
     * @psalm-param class-string<OriginalClass> $parentClassName
825
     * @psalm-return class-string<OriginalClass>
826
     * @psalm-suppress MoreSpecificReturnType
827
     */
828
    private function generateProxy(string $parentClassName, array $proxyOptions = []) : string
829
    {
830
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
831
        $generatedClass     = new ClassGenerator($generatedClassName);
832
833
        (new LazyLoadingGhostGenerator())->generate(
834
            new ReflectionClass($parentClassName),
835
            $generatedClass,
836
            $proxyOptions
837
        );
838
        (new EvaluatingGeneratorStrategy())->generate($generatedClass);
839
840
        /**
841
         * @psalm-suppress LessSpecificReturnStatement
842
         */
843
        return $generatedClassName;
844
    }
845
846
    /**
847
     * @psalm-template OriginalClass
848
     * @psalm-param class-string<OriginalClass> $originalClassName
849
     * @psalm-param OriginalClass $realInstance
850
     * @psalm-return GhostObjectInterface<OriginalClass>&OriginalClass
851
     *
852
     * @psalm-suppress MixedInferredReturnType
853
     * @psalm-suppress MoreSpecificReturnType
854
     */
855
    private function makeProxy(string $originalClassName, callable $initializer) : GhostObjectInterface
856
    {
857
        $proxyClassName = $this->generateProxy($originalClassName);
858
859
        /**
860
         * @psalm-suppress MixedMethodCall
861
         * @psalm-suppress MixedReturnStatement
862
         */
863
        return $proxyClassName::staticProxyConstructor($initializer);
864
    }
865
866
    /**
867
     * @psalm-param (CallableInterface&Mock)|null $initializerMatcher
868
     *
869
     * @psalm-return Closure(
870
     *   GhostObjectInterface $proxy,
871
     *   string $method,
872
     *   array $params,
873
     *   ?Closure $initializer
874
     * ) : bool
875
     */
876
    private function createInitializer(string $className, object $realInstance, ?Mock $initializerMatcher = null) : Closure
877
    {
878
        if (! $initializerMatcher) {
879
            $initializerMatcher = $this->createMock(CallableInterface::class);
880
881
            $initializerMatcher
882
                ->expects(self::once())
883
                ->method('__invoke')
884
                ->with(self::logicalAnd(
885
                    self::isInstanceOf(GhostObjectInterface::class),
886
                    self::isInstanceOf($className)
887
                ));
888
        }
889
890
        return static function (
891
            GhostObjectInterface $proxy,
892
            string $method,
893
            array $params,
894
            ?Closure & $initializer
895
        ) use (
896
            $initializerMatcher,
897
            $realInstance
898
        ) : bool {
899
            $initializer = null;
900
901
            $reflectionClass = new ReflectionClass($realInstance);
902
903
            foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
904
                if (! self::isPropertyInitialized($realInstance, $property)) {
905
                    continue;
906
                }
907
908
                $property->setAccessible(true);
909
                $property->setValue($proxy, $property->getValue($realInstance));
910
            }
911
912
            $initializerMatcher->__invoke($proxy, $method, $params);
913
914
            return true;
915
        };
916
    }
917
918
    /**
919
     * Generates a list of object | invoked method | parameters | expected result
920
     *
921
     * @return null[][]|string[][]|object[][]|mixed[][][]
922
     */
923
    public function getProxyMethods() : array
924
    {
925
        $selfHintParam = new ClassWithSelfHint();
926
        $empty         = new EmptyClass();
927
928
        return [
929
            [
930
                BaseClass::class,
931
                new BaseClass(),
932
                'publicMethod',
933
                [],
934
                'publicMethodDefault',
935
            ],
936
            [
937
                BaseClass::class,
938
                new BaseClass(),
939
                'publicTypeHintedMethod',
940
                [new stdClass()],
941
                'publicTypeHintedMethodDefault',
942
            ],
943
            [
944
                BaseClass::class,
945
                new BaseClass(),
946
                'publicByReferenceMethod',
947
                [],
948
                'publicByReferenceMethodDefault',
949
            ],
950
            [
951
                ClassWithSelfHint::class,
952
                new ClassWithSelfHint(),
953
                'selfHintMethod',
954
                ['parameter' => $selfHintParam],
955
                $selfHintParam,
956
            ],
957
            [
958
                ClassWithParentHint::class,
959
                new ClassWithParentHint(),
960
                'parentHintMethod',
961
                ['parameter' => $empty],
962
                $empty,
963
            ],
964
            [
965
                ClassWithAbstractPublicMethod::class,
966
                new EmptyClass(), // EmptyClass just used to not make reflection explode when synchronizing properties
967
                'publicAbstractMethod',
968
                [],
969
                null,
970
            ],
971
            [
972
                ClassWithMethodWithByRefVariadicFunction::class,
973
                new ClassWithMethodWithByRefVariadicFunction(),
974
                'tuz',
975
                ['Ocramius', 'Malukenho'],
976
                ['Ocramius', 'changed'],
977
            ],
978
        ];
979
    }
980
981
    /**
982
     * Generates a list of object | invoked method | parameters | expected result for methods that cause lazy-loading
983
     * of a ghost object
984
     *
985
     * @return string[][]|object[][]|mixed[][][]|null[][]
986
     */
987
    public function getProxyInitializingMethods() : array
988
    {
989
        return [
990
            [
991
                BaseClass::class,
992
                new BaseClass(),
993
                'publicPropertyGetter',
994
                [],
995
                'publicPropertyDefault',
996
            ],
997
            [
998
                BaseClass::class,
999
                new BaseClass(),
1000
                'protectedPropertyGetter',
1001
                [],
1002
                'protectedPropertyDefault',
1003
            ],
1004
            [
1005
                BaseClass::class,
1006
                new BaseClass(),
1007
                'privatePropertyGetter',
1008
                [],
1009
                'privatePropertyDefault',
1010
            ],
1011
            [
1012
                ClassWithMethodWithVariadicFunction::class,
1013
                new ClassWithMethodWithVariadicFunction(),
1014
                'foo',
1015
                ['Ocramius', 'Malukenho'],
1016
                null,
1017
            ],
1018
        ];
1019
    }
1020
1021
    /**
1022
     * Generates a list of object | invoked method | parameters | expected result for methods DON'T cause lazy-loading
1023
     *
1024
     * @return null[][]|string[][]|object[][]|mixed[][][]
1025
     */
1026
    public function getProxyNonInitializingMethods() : array
1027
    {
1028
        return $this->getProxyMethods();
1029
    }
1030
1031
    /**
1032
     * Generates proxies and instances with a public property to feed to the property accessor methods
1033
     *
1034
     * @return string[][]|object[][]
1035
     */
1036
    public function getPropertyAccessProxies() : array
1037
    {
1038
        $instance1  = new BaseClass();
1039
        $instance2  = new BaseClass();
1040
1041
        $factory = new LazyLoadingGhostFactory();
1042
1043
        /** @var GhostObjectInterface $serialized */
1044
        $serialized = unserialize(serialize($factory->createProxy(
1045
            BaseClass::class,
1046
            $this->createInitializer(BaseClass::class, $instance2)
0 ignored issues
show
Documentation introduced by
$instance2 is of type object<ProxyManagerTestAsset\BaseClass>, but the function expects a object<ProxyManagerTest\Functional\object>.

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...
1047
        )));
1048
1049
        return [
1050
            [
1051
                $instance1,
1052
                $factory->createProxy(
1053
                    BaseClass::class,
1054
                    $this->createInitializer(BaseClass::class, $instance1)
0 ignored issues
show
Documentation introduced by
$instance1 is of type object<ProxyManagerTestAsset\BaseClass>, but the function expects a object<ProxyManagerTest\Functional\object>.

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...
1055
                ),
1056
                'publicProperty',
1057
                'publicPropertyDefault',
1058
            ],
1059
            [
1060
                $instance2,
1061
                $serialized,
1062
                'publicProperty',
1063
                'publicPropertyDefault',
1064
            ],
1065
        ];
1066
    }
1067
1068
    /**
1069
     * @param mixed   $expected
1070
     * @param mixed[] $proxyOptions
1071
     *
1072
     * @dataProvider skipPropertiesFixture
1073
     *
1074
     * @psalm-param class-string $className
1075
     * @psalm-param array{skippedProperties?: array<int, string>} $proxyOptions
1076
     */
1077
    public function testInitializationIsSkippedForSkippedProperties(
1078
        string $className,
1079
        string $propertyClass,
1080
        string $propertyName,
1081
        array $proxyOptions,
1082
        $expected
1083
    ) : void {
1084
        $ghostObject = (new LazyLoadingGhostFactory())->createProxy(
1085
            $className,
1086
            static function () use ($propertyName) : bool {
1087
                self::fail(sprintf('The Property "%s" was not expected to be lazy-loaded', $propertyName));
1088
1089
                return true;
1090
            },
1091
            $proxyOptions
1092
        );
1093
1094
        $property = new ReflectionProperty($propertyClass, $propertyName);
1095
        $property->setAccessible(true);
1096
1097
        self::assertSame($expected, $property->getValue($ghostObject));
1098
    }
1099
1100
    /**
1101
     * @param array<string, mixed> $proxyOptions
1102
     *
1103
     * @dataProvider skipPropertiesFixture
1104
     *
1105
     * @psalm-param class-string $className
1106
     * @psalm-param array{skippedProperties?: array<int, string>} $proxyOptions
1107
     */
1108
    public function testSkippedPropertiesAreNotOverwrittenOnInitialization(
1109
        string $className,
1110
        string $propertyClass,
1111
        string $propertyName,
1112
        array $proxyOptions
1113
    ) : void {
1114
        $ghostObject = (new LazyLoadingGhostFactory())->createProxy(
1115
            $className,
1116
            static function (
1117
                GhostObjectInterface $proxy,
1118
                string $method,
1119
                array $params,
1120
                ?Closure & $initializer
1121
            ) : bool {
1122
                $initializer = null;
1123
1124
                return true;
1125
            },
1126
            $proxyOptions
1127
        );
1128
1129
        $property = new ReflectionProperty($propertyClass, $propertyName);
1130
1131
        $property->setAccessible(true);
1132
1133
        $value = uniqid('', true);
1134
1135
        $property->setValue($ghostObject, $value);
1136
1137
        self::assertTrue($ghostObject->initializeProxy());
1138
1139
        self::assertSame(
1140
            $value,
1141
            $property->getValue($ghostObject),
1142
            'Property should not be changed by proxy initialization'
1143
        );
1144
    }
1145
1146
    /**
1147
     * @group 265
1148
     */
1149
    public function testWillForwardVariadicByRefArguments() : void
1150
    {
1151
        $object = (new LazyLoadingGhostFactory())->createProxy(
1152
            ClassWithMethodWithByRefVariadicFunction::class,
1153
            static function (
1154
                GhostObjectInterface $proxy,
1155
                string $method,
1156
                array $params,
1157
                ?Closure & $initializer
1158
            ) : bool {
1159
                $initializer = null;
1160
1161
                return true;
1162
            }
1163
        );
1164
1165
        $parameters = ['a', 'b', 'c'];
1166
1167
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1168
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
1169
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
1170
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
1171
    }
1172
1173
    /**
1174
     * @group 265
1175
     */
1176
    public function testWillForwardDynamicArguments() : void
1177
    {
1178
        $object = (new LazyLoadingGhostFactory())->createProxy(
1179
            ClassWithDynamicArgumentsMethod::class,
1180
            static function () : bool {
1181
                return true;
1182
            }
1183
        );
1184
1185
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1186
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
1187
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
1188
    }
1189
1190
    /**
1191
     * @return mixed[] in order:
1192
     *                  - the class to be proxied
1193
     *                  - the class owning the property to be checked
1194
     *                  - the property name
1195
     *                  - the options to be passed to the generator
1196
     *                  - the expected value of the property
1197
     */
1198
    public function skipPropertiesFixture() : array
1199
    {
1200
        return [
1201
            [
1202
                ClassWithPublicProperties::class,
1203
                ClassWithPublicProperties::class,
1204
                'property9',
1205
                [
1206
                    'skippedProperties' => ['property9'],
1207
                ],
1208
                'property9',
1209
            ],
1210
            [
1211
                ClassWithProtectedProperties::class,
1212
                ClassWithProtectedProperties::class,
1213
                'property9',
1214
                [
1215
                    'skippedProperties' => ["\0*\0property9"],
1216
                ],
1217
                'property9',
1218
            ],
1219
            [
1220
                ClassWithPrivateProperties::class,
1221
                ClassWithPrivateProperties::class,
1222
                'property9',
1223
                [
1224
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property9"],
1225
                ],
1226
                'property9',
1227
            ],
1228
            [
1229
                ClassWithCollidingPrivateInheritedProperties::class,
1230
                ClassWithCollidingPrivateInheritedProperties::class,
1231
                'property0',
1232
                [
1233
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithCollidingPrivateInheritedProperties\0property0"],
1234
                ],
1235
                'childClassProperty0',
1236
            ],
1237
            [
1238
                ClassWithCollidingPrivateInheritedProperties::class,
1239
                ClassWithPrivateProperties::class,
1240
                'property0',
1241
                [
1242
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property0"],
1243
                ],
1244
                'property0',
1245
            ],
1246
        ];
1247
    }
1248
1249
    /**
1250
     * @group        276
1251
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1252
     */
1253
    public function testWillLazyLoadMembersOfOtherProxiesWithTheSamePrivateScope(
1254
        object $callerObject,
1255
        string $method,
1256
        string $propertyIndex,
1257
        string $expectedValue
1258
    ) : void {
1259
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
1260
            OtherObjectAccessClass::class,
1261
            static function (
1262
                GhostObjectInterface $proxy,
1263
                string $method,
1264
                array $params,
1265
                ?Closure & $initializer,
1266
                array $properties
1267
            ) use ($propertyIndex, $expectedValue) : bool {
0 ignored issues
show
Coding Style introduced by
Multi-line use declarations must define one parameter per line
Loading history...
1268
                $initializer = null;
1269
1270
                $properties[$propertyIndex] = $expectedValue;
1271
1272
                return true;
1273
            }
1274
        );
1275
1276
        $accessor = [$callerObject, $method];
1277
1278
        self::assertIsCallable($accessor);
1279
1280
        self::assertFalse($proxy->isProxyInitialized());
1281
        self::assertSame($expectedValue, $accessor($proxy));
1282
        self::assertTrue($proxy->isProxyInitialized());
1283
    }
1284
1285
    /**
1286
     * @group        276
1287
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1288
     */
1289
    public function testWillAccessMembersOfOtherDeSerializedProxiesWithTheSamePrivateScope(
1290
        object $callerObject,
1291
        string $method,
1292
        string $propertyIndex,
1293
        string $expectedValue
1294
    ) : void {
1295
        /** @var OtherObjectAccessClass&LazyLoadingInterface $proxy */
0 ignored issues
show
Documentation introduced by
The doc-type OtherObjectAccessClass&LazyLoadingInterface could not be parsed: Unknown type name "OtherObjectAccessClass&LazyLoadingInterface" 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...
1296
        $proxy = unserialize(serialize(
1297
            (new LazyLoadingGhostFactory())->createProxy(
1298
                OtherObjectAccessClass::class,
1299
            static function (
1300
                GhostObjectInterface$proxy,
1301
                string $method,
1302
                array $params,
1303
                ?Closure & $initializer,
1304
                array $properties
1305
            ) use ($propertyIndex, $expectedValue) : bool {
0 ignored issues
show
Coding Style introduced by
Multi-line use declarations must define one parameter per line
Loading history...
1306
                $initializer = null;
1307
1308
                $properties[$propertyIndex] = $expectedValue;
1309
1310
                return true;
1311
            }
1312
        )));
1313
1314
        $accessor = [$callerObject, $method];
1315
1316
        self::assertIsCallable($accessor);
1317
1318
        self::assertTrue($proxy->isProxyInitialized());
1319
        self::assertSame($expectedValue, $accessor($proxy));
1320
    }
1321
1322
    /**
1323
     * @group        276
1324
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1325
     */
1326
    public function testWillAccessMembersOfOtherClonedProxiesWithTheSamePrivateScope(
1327
        object $callerObject,
1328
        string $method,
1329
        string $propertyIndex,
1330
        string $expectedValue
1331
    ) : void {
1332
        $proxy = clone $this->makeProxy(
1333
            OtherObjectAccessClass::class,
1334
            static function (
1335
                GhostObjectInterface $proxy,
1336
                string $method,
1337
                array $params,
1338
                ?Closure & $initializer,
1339
                array $properties
1340
            ) use ($propertyIndex, $expectedValue) : bool {
0 ignored issues
show
Coding Style introduced by
Multi-line use declarations must define one parameter per line
Loading history...
1341
                $initializer = null;
1342
1343
                $properties[$propertyIndex] = $expectedValue;
1344
1345
                return true;
1346
            }
1347
        );
1348
1349
        $accessor = [$callerObject, $method];
1350
1351
        self::assertIsCallable($accessor);
1352
1353
        self::assertTrue($proxy->isProxyInitialized());
1354
        self::assertSame($expectedValue, $accessor($proxy));
1355
    }
1356
1357
    /** @return string[][]|object[][] */
1358
    public function getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() : array
1359
    {
1360
        $factory = new LazyLoadingGhostFactory();
1361
1362
        return [
1363
            OtherObjectAccessClass::class . '#$privateProperty'                => [
1364
                new OtherObjectAccessClass(),
1365
                'getPrivateProperty',
1366
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1367
                uniqid('', true),
1368
            ],
1369
            OtherObjectAccessClass::class . '#$protectedProperty'              => [
1370
                new OtherObjectAccessClass(),
1371
                'getProtectedProperty',
1372
                "\0*\0protectedProperty",
1373
                uniqid('', true),
1374
            ],
1375
            OtherObjectAccessClass::class . '#$publicProperty'                 => [
1376
                new OtherObjectAccessClass(),
1377
                'getPublicProperty',
1378
                'publicProperty',
1379
                uniqid('', true),
1380
            ],
1381
            '(proxy) ' . OtherObjectAccessClass::class . '#$privateProperty'   => [
1382
                $factory->createProxy(
1383
                    OtherObjectAccessClass::class,
1384
                    static function () : bool {
1385
                        self::fail('Should never be initialized, as its values aren\'t accessed');
1386
1387
                        return true;
1388
                    }
1389
                ),
1390
                'getPrivateProperty',
1391
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1392
                uniqid('', true),
1393
            ],
1394
            '(proxy) ' . OtherObjectAccessClass::class . '#$protectedProperty' => [
1395
                $factory->createProxy(
1396
                    OtherObjectAccessClass::class,
1397
                    static function () : bool {
1398
                        self::fail('Should never be initialized, as its values aren\'t accessed');
1399
1400
                        return true;
1401
                    }
1402
                ),
1403
                'getProtectedProperty',
1404
                "\0*\0protectedProperty",
1405
                uniqid('', true),
1406
            ],
1407
            '(proxy) ' . OtherObjectAccessClass::class . '#$publicProperty'    => [
1408
                $factory->createProxy(
1409
                    OtherObjectAccessClass::class,
1410
                    static function () : bool {
1411
                        self::fail('Should never be initialized, as its values aren\'t accessed');
1412
1413
                        return true;
1414
                    }
1415
                ),
1416
                'getPublicProperty',
1417
                'publicProperty',
1418
                uniqid('', true),
1419
            ],
1420
        ];
1421
    }
1422
1423
    /**
1424
     * @group 276
1425
     */
1426
    public function testFriendObjectWillNotCauseLazyLoadingOnSkippedProperty() : void
1427
    {
1428
        $proxy = (new LazyLoadingGhostFactory())
1429
            ->createProxy(
1430
                OtherObjectAccessClass::class,
1431
                static function () : bool {
1432
                    throw new BadMethodCallException('The proxy should never be initialized, as all properties are skipped');
1433
                },
1434
                [
1435
                    'skippedProperties' => [
1436
                        "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1437
                        "\0*\0protectedProperty",
1438
                        'publicProperty',
1439
                    ],
1440
                ]
1441
            );
1442
1443
        $privatePropertyValue   = uniqid('', true);
1444
        $protectedPropertyValue = uniqid('', true);
1445
        $publicPropertyValue    = uniqid('', true);
1446
1447
        $reflectionPrivateProperty = new ReflectionProperty(OtherObjectAccessClass::class, 'privateProperty');
1448
1449
        $reflectionPrivateProperty->setAccessible(true);
1450
        $reflectionPrivateProperty->setValue($proxy, $privatePropertyValue);
1451
1452
        $reflectionProtectedProperty = new ReflectionProperty(OtherObjectAccessClass::class, 'protectedProperty');
1453
1454
        $reflectionProtectedProperty->setAccessible(true);
1455
        $reflectionProtectedProperty->setValue($proxy, $protectedPropertyValue);
1456
1457
        $proxy->publicProperty = $publicPropertyValue;
1458
1459
        $friendObject = new OtherObjectAccessClass();
1460
1461
        self::assertSame($privatePropertyValue, $friendObject->getPrivateProperty($proxy));
1462
        self::assertSame($protectedPropertyValue, $friendObject->getProtectedProperty($proxy));
1463
        self::assertSame($publicPropertyValue, $friendObject->getPublicProperty($proxy));
1464
    }
1465
1466
    public function testClonedSkippedPropertiesArePreserved() : void
1467
    {
1468
        $proxy = (new LazyLoadingGhostFactory())
1469
            ->createProxy(
1470
                BaseClass::class,
1471
                static function (GhostObjectInterface $proxy) : bool {
1472
                    $proxy->setProxyInitializer(null);
1473
1474
                    return true;
1475
                },
1476
                [
1477
                    'skippedProperties' => [
1478
                        "\0" . BaseClass::class . "\0privateProperty",
1479
                        "\0*\0protectedProperty",
1480
                        'publicProperty',
1481
                    ],
1482
                ]
1483
            );
1484
1485
        $reflectionPrivate   = new ReflectionProperty(BaseClass::class, 'privateProperty');
1486
        $reflectionProtected = new ReflectionProperty(BaseClass::class, 'protectedProperty');
1487
1488
        $reflectionPrivate->setAccessible(true);
1489
        $reflectionProtected->setAccessible(true);
1490
1491
        $privateValue   = uniqid('', true);
1492
        $protectedValue = uniqid('', true);
1493
        $publicValue    = uniqid('', true);
1494
1495
        $reflectionPrivate->setValue($proxy, $privateValue);
1496
        $reflectionProtected->setValue($proxy, $protectedValue);
1497
        $proxy->publicProperty = $publicValue;
1498
1499
        self::assertFalse($proxy->isProxyInitialized());
1500
1501
        $clone = clone $proxy;
1502
1503
        self::assertFalse($proxy->isProxyInitialized());
1504
        self::assertTrue($clone->isProxyInitialized());
1505
1506
        self::assertSame($privateValue, $reflectionPrivate->getValue($proxy));
1507
        self::assertSame($privateValue, $reflectionPrivate->getValue($clone));
1508
        self::assertSame($protectedValue, $reflectionProtected->getValue($proxy));
1509
        self::assertSame($protectedValue, $reflectionProtected->getValue($clone));
1510
        self::assertSame($publicValue, $proxy->publicProperty);
1511
        self::assertSame($publicValue, $clone->publicProperty);
1512
    }
1513
1514
    /**
1515
     * @group 327
1516
     */
1517
    public function testWillExecuteLogicInAVoidMethod() : void
1518
    {
1519
        $initialCounter = random_int(10, 1000);
1520
1521
        /** @var VoidCounter|LazyLoadingInterface $proxy */
1522
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
1523
            VoidCounter::class,
1524
            static function (
1525
                LazyLoadingInterface $proxy,
1526
                string $method,
1527
                array $params,
1528
                ?Closure & $initializer,
1529
                array $properties
1530
            ) use ($initialCounter) : bool {
1531
                $initializer = null;
1532
1533
                $properties['counter'] = $initialCounter;
1534
1535
                return true;
1536
            }
1537
        );
1538
1539
        self::assertInstanceOf(VoidCounter::class, $proxy);
1540
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1541
1542
        $increment = random_int(1001, 10000);
1543
1544
        $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...
1545
1546
        self::assertSame($initialCounter + $increment, $proxy->counter);
1547
    }
1548
1549
    private static function isPropertyInitialized(object $object, ReflectionProperty $property) : bool
1550
    {
1551
        return array_key_exists(
1552
            ($property->isProtected() ? "\0*\0" : '')
1553
            . ($property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : '')
1554
            . $property->getName(),
1555
            (array) $object
1556
        );
1557
    }
1558
1559
    /**
1560
     * @param mixed $expected
1561
     * @param mixed $actual
1562
     */
1563
    private static function assertByRefVariableValueSame($expected, & $actual) : void
1564
    {
1565
        self::assertSame($expected, $actual);
1566
    }
1567
}
1568