Completed
Pull Request — master (#467)
by Marco
23:32
created

testInitializationIsSkippedForSkippedProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.568
c 0
b 0
f 0
cc 1
nc 1
nop 5
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\Proxy\GhostObjectInterface;
13
use ProxyManager\Proxy\LazyLoadingInterface;
14
use ProxyManager\ProxyGenerator\Util\Properties;
15
use ProxyManagerTestAsset\BaseClass;
16
use ProxyManagerTestAsset\CallableInterface;
17
use ProxyManagerTestAsset\ClassWithAbstractPublicMethod;
18
use ProxyManagerTestAsset\ClassWithCollidingPrivateInheritedProperties;
19
use ProxyManagerTestAsset\ClassWithCounterConstructor;
20
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
21
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
22
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
23
use ProxyManagerTestAsset\ClassWithMixedProperties;
24
use ProxyManagerTestAsset\ClassWithMixedPropertiesAndAccessorMethods;
25
use ProxyManagerTestAsset\ClassWithMixedTypedProperties;
26
use ProxyManagerTestAsset\ClassWithParentHint;
27
use ProxyManagerTestAsset\ClassWithPrivateProperties;
28
use ProxyManagerTestAsset\ClassWithProtectedProperties;
29
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
30
use ProxyManagerTestAsset\ClassWithPublicProperties;
31
use ProxyManagerTestAsset\ClassWithSelfHint;
32
use ProxyManagerTestAsset\EmptyClass;
33
use ProxyManagerTestAsset\OtherObjectAccessClass;
34
use ProxyManagerTestAsset\VoidCounter;
35
use ReflectionClass;
36
use ReflectionProperty;
37
use stdClass;
38
use function array_key_exists;
39
use function array_values;
40
use function get_class;
41
use function get_parent_class;
42
use function random_int;
43
use function serialize;
44
use function sprintf;
45
use function str_replace;
46
use function uniqid;
47
use function unserialize;
48
49
/**
50
 * Tests for {@see \ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator} produced objects
51
 *
52
 * @group Functional
53
 * @coversNothing
54
 */
55
final class LazyLoadingGhostFunctionalTest extends TestCase
56
{
57
    /**
58
     * @param mixed[] $params
59
     * @param mixed   $expectedValue
60
     *
61
     * @dataProvider getProxyInitializingMethods
62
     *
63
     * @psalm-template OriginalClass
64
     * @psalm-param class-string<OriginalClass> $className
65
     * @psalm-param OriginalClass $instance
66
     */
67
    public function testMethodCallsThatLazyLoadTheObject(
68
        string $className,
69
        object $instance,
70
        string $method,
71
        array $params,
72
        $expectedValue
73
    ) : void {
74
        $proxy = (new LazyLoadingGhostFactory())
75
            ->createProxy($className, $this->createInitializer($className, $instance));
76
77
        self::assertFalse($proxy->isProxyInitialized());
78
79
        $callProxyMethod = [$proxy, $method];
80
        $parameterValues = array_values($params);
81
82
        self::assertIsCallable($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
     * @psalm-template OriginalClass
94
     * @psalm-param class-string<OriginalClass> $className
95
     * @psalm-param OriginalClass $instance
96
     */
97
    public function testMethodCallsThatDoNotLazyLoadTheObject(
98
        string $className,
99
        object $instance,
100
        string $method,
101
        array $params,
102
        $expectedValue
103
    ) : void {
104
        $initializeMatcher = $this->createMock(CallableInterface::class);
105
106
        $initializeMatcher->expects(self::never())->method('__invoke'); // should not initialize the proxy
107
108
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
109
            $className,
110
            $this->createInitializer($className, $instance, $initializeMatcher)
111
        );
112
113
        self::assertFalse($proxy->isProxyInitialized());
114
115
        $callProxyMethod = [$proxy, $method];
116
        $parameterValues = array_values($params);
117
118
        self::assertIsCallable($callProxyMethod);
119
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
120
        self::assertFalse($proxy->isProxyInitialized());
121
    }
122
123
    /**
124
     * @param mixed[] $params
125
     * @param mixed   $expectedValue
126
     *
127
     * @dataProvider getProxyMethods
128
     *
129
     * @psalm-template OriginalClass
130
     * @psalm-param class-string<OriginalClass> $className
131
     * @psalm-param OriginalClass $instance
132
     */
133
    public function testMethodCallsAfterUnSerialization(
134
        string $className,
135
        object $instance,
136
        string $method,
137
        array $params,
138
        $expectedValue
139
    ) : void {
140
        /** @var GhostObjectInterface $proxy */
141
        $proxy = unserialize(serialize((new LazyLoadingGhostFactory())->createProxy(
142
            $className,
143
            $this->createInitializer($className, $instance)
144
        )));
145
146
        self::assertTrue($proxy->isProxyInitialized());
147
148
        $callProxyMethod = [$proxy, $method];
149
        $parameterValues = array_values($params);
150
151
        self::assertIsCallable($callProxyMethod);
152
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
153
    }
154
155
    /**
156
     * @param mixed[] $params
157
     * @param mixed   $expectedValue
158
     *
159
     * @dataProvider getProxyMethods
160
     *
161
     * @psalm-template OriginalClass
162
     * @psalm-param class-string<OriginalClass> $className
163
     * @psalm-param OriginalClass $instance
164
     */
165
    public function testMethodCallsAfterCloning(
166
        string $className,
167
        object $instance,
168
        string $method,
169
        array $params,
170
        $expectedValue
171
    ) : void {
172
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
173
            $className,
174
            $this->createInitializer($className, $instance)
175
        );
176
        $cloned = clone $proxy;
177
178
        self::assertTrue($cloned->isProxyInitialized());
179
180
        $callProxyMethod = [$proxy, $method];
181
        $parameterValues = array_values($params);
182
183
        self::assertIsCallable($callProxyMethod);
184
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
185
    }
186
187
    /**
188
     * @param mixed $propertyValue
189
     *
190
     * @dataProvider getPropertyAccessProxies
191
     */
192
    public function testPropertyReadAccess(
193
        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...
194
        GhostObjectInterface $proxy,
195
        string $publicProperty,
196
        $propertyValue
197
    ) : void {
198
        self::assertSame($propertyValue, $proxy->$publicProperty);
199
        self::assertTrue($proxy->isProxyInitialized());
200
    }
201
202
    /**
203
     * @dataProvider getPropertyAccessProxies
204
     */
205
    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...
206
    ) : void
207
    {
208
        $newValue               = uniqid('', true);
209
        $proxy->$publicProperty = $newValue;
210
211
        self::assertTrue($proxy->isProxyInitialized());
212
        self::assertSame($newValue, $proxy->$publicProperty);
213
    }
214
215
    /**
216
     * @dataProvider getPropertyAccessProxies
217
     */
218
    public function testPropertyExistence(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
219
    {
220
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
221
        self::assertTrue($proxy->isProxyInitialized());
222
    }
223
224
    /**
225
     * @dataProvider getPropertyAccessProxies
226
     */
227
    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...
228
    {
229
        $proxy->$publicProperty = null;
230
        self::assertFalse(isset($proxy->$publicProperty));
231
        self::assertTrue($proxy->isProxyInitialized());
232
    }
233
234
    /**
235
     * @dataProvider getPropertyAccessProxies
236
     */
237
    public function testPropertyUnset(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
238
    {
239
        unset($proxy->$publicProperty);
240
241
        self::assertTrue($proxy->isProxyInitialized());
242
        self::assertTrue(isset($instance->$publicProperty));
243
        self::assertFalse(isset($proxy->$publicProperty));
244
    }
245
246
    /**
247
     * Verifies that accessing a public property containing an array behaves like in a normal context
248
     */
249
    public function testCanWriteToArrayKeysInPublicProperty() : void
250
    {
251
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
252
            ClassWithPublicArrayProperty::class,
253
            $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...
254
        );
255
256
        $proxy->arrayProperty['foo'] = 'bar';
257
258
        self::assertByRefVariableValueSame('bar', $proxy->arrayProperty['foo']);
259
260
        $proxy->arrayProperty = ['tab' => 'taz'];
261
262
        self::assertByRefVariableValueSame(['tab' => 'taz'], $proxy->arrayProperty);
263
    }
264
265
    /**
266
     * Verifies that public properties retrieved via `__get` don't get modified in the object itself
267
     */
268
    public function testWillNotModifyRetrievedPublicProperties() : void
269
    {
270
        $proxy    = (new LazyLoadingGhostFactory())->createProxy(
271
            ClassWithPublicProperties::class,
272
            $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...
273
        );
274
        $variable = $proxy->property0;
275
276
        self::assertByRefVariableValueSame('property0', $variable);
277
278
        $variable = 'foo';
279
280
        self::assertByRefVariableValueSame('property0', $proxy->property0);
281
        self::assertByRefVariableValueSame('foo', $variable);
282
    }
283
284
    /**
285
     * Verifies that public properties references retrieved via `__get` modify in the object state
286
     */
287
    public function testWillModifyByRefRetrievedPublicProperties() : void
288
    {
289
        $instance = new ClassWithPublicProperties();
290
        $proxy    = (new LazyLoadingGhostFactory())->createProxy(
291
            ClassWithPublicProperties::class,
292
            $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...
293
        );
294
        $variable = &$proxy->property0;
295
296
        self::assertByRefVariableValueSame('property0', $variable);
297
298
        $variable = 'foo';
299
300
        self::assertByRefVariableValueSame('foo', $proxy->property0);
301
        self::assertByRefVariableValueSame('foo', $variable);
302
    }
303
304
    public function testKeepsInitializerWhenNotOverwitten() : void
305
    {
306
        $initializer = static function () : bool {
307
            return true;
308
        };
309
310
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
311
            BaseClass::class,
312
            $initializer
313
        );
314
315
        $proxy->initializeProxy();
316
317
        self::assertSame($initializer, $proxy->getProxyInitializer());
318
    }
319
320
    /**
321
     * Verifies that public properties are not being initialized multiple times
322
     */
323
    public function testKeepsInitializedPublicProperties() : void
324
    {
325
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
326
            BaseClass::class,
327
            static function (
328
                object $proxy,
329
                string $method,
330
                array $parameters,
331
                ?Closure & $initializer
332
            ) : bool {
333
                $initializer           = null;
334
                $proxy->publicProperty = 'newValue';
335
336
                return true;
337
            }
338
        );
339
340
        $proxy->initializeProxy();
341
        self::assertSame('newValue', $proxy->publicProperty);
342
343
        $proxy->publicProperty = 'otherValue';
344
345
        $proxy->initializeProxy();
346
347
        self::assertSame('otherValue', $proxy->publicProperty);
348
    }
349
350
    /**
351
     * Verifies that properties' default values are preserved
352
     */
353
    public function testPublicPropertyDefaultWillBePreserved() : void
354
    {
355
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
356
            ClassWithPublicProperties::class,
357
            static function () : bool {
358
                return true;
359
            }
360
        );
361
362
        self::assertSame('property0', $proxy->property0);
363
    }
364
365
    /**
366
     * Verifies that protected properties' default values are preserved
367
     */
368
    public function testProtectedPropertyDefaultWillBePreserved() : void
369
    {
370
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
371
            ClassWithProtectedProperties::class,
372
            static function () : bool {
373
                return true;
374
            }
375
        );
376
377
        // Check protected property via reflection
378
        $reflectionProperty = new ReflectionProperty(ClassWithProtectedProperties::class, 'property0');
379
        $reflectionProperty->setAccessible(true);
380
381
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
382
    }
383
384
    /**
385
     * Verifies that private properties' default values are preserved
386
     */
387
    public function testPrivatePropertyDefaultWillBePreserved() : void
388
    {
389
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
390
            ClassWithPrivateProperties::class,
391
            static function () : bool {
392
                return true;
393
            }
394
        );
395
396
        // Check protected property via reflection
397
        $reflectionProperty = new ReflectionProperty(ClassWithPrivateProperties::class, 'property0');
398
        $reflectionProperty->setAccessible(true);
399
400
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
401
    }
402
403
    /**
404
     * @group 159
405
     * @group 192
406
     */
407
    public function testMultiLevelPrivatePropertiesDefaultsWillBePreserved() : void
408
    {
409
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
410
            ClassWithCollidingPrivateInheritedProperties::class,
411
            static function () : bool {
412
                return true;
413
            }
414
        );
415
416
        $childProperty  = new ReflectionProperty(ClassWithCollidingPrivateInheritedProperties::class, 'property0');
417
        $parentProperty = new ReflectionProperty(get_parent_class(ClassWithCollidingPrivateInheritedProperties::class), 'property0');
418
419
        $childProperty->setAccessible(true);
420
        $parentProperty->setAccessible(true);
421
422
        self::assertSame('childClassProperty0', $childProperty->getValue($proxy));
423
        self::assertSame('property0', $parentProperty->getValue($proxy));
424
    }
425
426
    /**
427
     * @group 159
428
     * @group 192
429
     */
430
    public function testMultiLevelPrivatePropertiesByRefInitialization() : void
431
    {
432
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
433
            ClassWithCollidingPrivateInheritedProperties::class,
434
            static function (
435
                GhostObjectInterface $proxy,
436
                string $method,
437
                array $params,
438
                ?Closure & $initializer,
439
                array $properties
440
            ) : bool {
441
                $initializer                                                                            = null;
442
                $properties["\0" . ClassWithCollidingPrivateInheritedProperties::class . "\0property0"]                   = 'foo';
443
                $properties["\0" . get_parent_class(ClassWithCollidingPrivateInheritedProperties::class) . "\0property0"] = 'bar';
444
445
                return true;
446
            }
447
        );
448
449
        $childProperty  = new ReflectionProperty(ClassWithCollidingPrivateInheritedProperties::class, 'property0');
450
        $parentProperty = new ReflectionProperty(get_parent_class(ClassWithCollidingPrivateInheritedProperties::class), 'property0');
451
452
        $childProperty->setAccessible(true);
453
        $parentProperty->setAccessible(true);
454
455
        self::assertSame('foo', $childProperty->getValue($proxy));
456
        self::assertSame('bar', $parentProperty->getValue($proxy));
457
    }
458
459
    /**
460
     * @group 159
461
     * @group 192
462
     *
463
     * Test designed to verify that the cached logic does take into account the fact that
464
     * proxies are different instances
465
     */
466
    public function testGetPropertyFromDifferentProxyInstances() : void
467
    {
468
        $factory = new LazyLoadingGhostFactory();
469
        $proxy1  = $factory->createProxy(
470
            ClassWithCollidingPrivateInheritedProperties::class,
471
            static function (
472
                GhostObjectInterface $proxy,
473
                string $method,
474
                array $params,
475
                ?Closure & $initializer,
476
                array $properties
477
            ) : bool {
478
                $initializer                                                                                              = null;
479
                $properties["\0" . ClassWithCollidingPrivateInheritedProperties::class . "\0property0"]                   = 'foo';
480
                $properties["\0" . get_parent_class(ClassWithCollidingPrivateInheritedProperties::class) . "\0property0"] = 'bar';
481
482
                return true;
483
            }
484
        );
485
        $proxy2  = $factory->createProxy(
486
            ClassWithCollidingPrivateInheritedProperties::class,
487
            static function (
488
                GhostObjectInterface $proxy,
489
                string $method,
490
                array $params,
491
                ?Closure & $initializer,
492
                array $properties
493
            ) : bool {
494
                $initializer                                                                                              = null;
495
                $properties["\0" . ClassWithCollidingPrivateInheritedProperties::class . "\0property0"]                   = 'baz';
496
                $properties["\0" . get_parent_class(ClassWithCollidingPrivateInheritedProperties::class) . "\0property0"] = 'tab';
497
498
                return true;
499
            }
500
        );
501
502
        $childProperty  = new ReflectionProperty(ClassWithCollidingPrivateInheritedProperties::class, 'property0');
503
        $parentProperty = new ReflectionProperty(get_parent_class(ClassWithCollidingPrivateInheritedProperties::class), 'property0');
504
505
        $childProperty->setAccessible(true);
506
        $parentProperty->setAccessible(true);
507
508
        self::assertSame('foo', $childProperty->getValue($proxy1));
509
        self::assertSame('bar', $parentProperty->getValue($proxy1));
510
511
        self::assertSame('baz', $childProperty->getValue($proxy2));
512
        self::assertSame('tab', $parentProperty->getValue($proxy2));
513
    }
514
515
    /**
516
     * @group 159
517
     * @group 192
518
     *
519
     * Test designed to verify that the cached logic does take into account the fact that
520
     * proxies are different instances
521
     */
522
    public function testSetPrivatePropertyOnDifferentProxyInstances() : void
523
    {
524
        $factory = new LazyLoadingGhostFactory();
525
        $proxy1  = $factory->createProxy(
526
            ClassWithMixedPropertiesAndAccessorMethods::class,
527
            static function (
528
                GhostObjectInterface $proxy,
529
                string $method,
530
                array $params,
531
                ?Closure & $initializer
532
            ) : bool {
533
                $initializer = null;
534
535
                return true;
536
            }
537
        );
538
        $proxy2  = $factory->createProxy(
539
            ClassWithMixedPropertiesAndAccessorMethods::class,
540
            static function (
541
                GhostObjectInterface $proxy,
542
                string $method,
543
                array $params,
544
                ?Closure & $initializer
545
            ) : bool {
546
                $initializer = null;
547
548
                return true;
549
            }
550
        );
551
552
        $proxy1->set('privateProperty', 'private1');
553
        $proxy2->set('privateProperty', 'private2');
554
        self::assertSame('private1', $proxy1->get('privateProperty'));
555
        self::assertSame('private2', $proxy2->get('privateProperty'));
556
    }
557
558
    /**
559
     * @group 159
560
     * @group 192
561
     *
562
     * Test designed to verify that the cached logic does take into account the fact that
563
     * proxies are different instances
564
     */
565
    public function testIssetPrivatePropertyOnDifferentProxyInstances() : void
566
    {
567
        $factory = new LazyLoadingGhostFactory();
568
        $proxy1  = $factory->createProxy(
569
            ClassWithMixedPropertiesAndAccessorMethods::class,
570
            static function (
571
                GhostObjectInterface $proxy,
572
                string $method,
573
                array $params,
574
                ?Closure & $initializer
575
            ) : bool {
576
                $initializer = null;
577
578
                return true;
579
            }
580
        );
581
        $proxy2  = $factory->createProxy(
582
            ClassWithMixedPropertiesAndAccessorMethods::class,
583
            static function (
584
                GhostObjectInterface $proxy,
585
                string $method,
586
                array $params,
587
                ?Closure & $initializer,
588
                array $properties
589
            ) : bool {
590
                $initializer                                                                                = null;
591
                $properties["\0" . ClassWithMixedPropertiesAndAccessorMethods::class . "\0privateProperty"] = null;
592
593
                return true;
594
            }
595
        );
596
597
        self::assertTrue($proxy1->has('privateProperty'));
598
        self::assertFalse($proxy2->has('privateProperty'));
599
        self::assertTrue($proxy1->has('privateProperty'));
600
        self::assertFalse($proxy2->has('privateProperty'));
601
    }
602
603
    /**
604
     * @group 159
605
     * @group 192
606
     *
607
     * Test designed to verify that the cached logic does take into account the fact that
608
     * proxies are different instances
609
     */
610
    public function testUnsetPrivatePropertyOnDifferentProxyInstances() : void
611
    {
612
        $factory = new LazyLoadingGhostFactory();
613
        $proxy1  = $factory->createProxy(
614
            ClassWithMixedPropertiesAndAccessorMethods::class,
615
            static function (
616
                GhostObjectInterface $proxy,
617
                string $method,
618
                array $params,
619
                ?Closure & $initializer
620
            ) : bool {
621
                $initializer = null;
622
623
                return true;
624
            }
625
        );
626
        $proxy2  = $factory->createProxy(
627
            ClassWithMixedPropertiesAndAccessorMethods::class,
628
            static function (
629
                GhostObjectInterface $proxy,
630
                string $method,
631
                array $params,
632
                ?Closure & $initializer
633
            ) : bool {
634
                $initializer = null;
635
636
                return true;
637
            }
638
        );
639
640
        self::assertTrue($proxy1->has('privateProperty'));
641
        $proxy2->remove('privateProperty');
642
        self::assertFalse($proxy2->has('privateProperty'));
643
        self::assertTrue($proxy1->has('privateProperty'));
644
        $proxy1->remove('privateProperty');
645
        self::assertFalse($proxy1->has('privateProperty'));
646
        self::assertFalse($proxy2->has('privateProperty'));
647
    }
648
649
    /**
650
     * @group 159
651
     * @group 192
652
     *
653
     * Test designed to verify that the cached logic does take into account the fact that
654
     * proxies are different instances
655
     */
656
    public function testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse() : void
657
    {
658
        $factory = new LazyLoadingGhostFactory();
659
        $proxy1  = $factory->createProxy(
660
            ClassWithMixedPropertiesAndAccessorMethods::class,
661
            static function (
662
                GhostObjectInterface $proxy,
663
                string $method,
664
                array $params,
665
                ?Closure & $initializer,
666
                array $properties
667
            ) : bool {
668
                $initializer                                                                                = null;
669
                $properties['publicProperty']                                                               = false;
670
                $properties["\0*\0protectedProperty"]                                                       = false;
671
                $properties["\0" . ClassWithMixedPropertiesAndAccessorMethods::class . "\0privateProperty"] = false;
672
673
                return true;
674
            }
675
        );
676
        $proxy2  = $factory->createProxy(
677
            ClassWithMixedPropertiesAndAccessorMethods::class,
678
            static function (
679
                GhostObjectInterface $proxy,
680
                string $method,
681
                array $params,
682
                ?Closure & $initializer,
683
                array $properties
684
            ) : bool {
685
                $initializer                                                                                = null;
686
                $properties['publicProperty']                                                               = null;
687
                $properties["\0*\0protectedProperty"]                                                       = null;
688
                $properties["\0" . ClassWithMixedPropertiesAndAccessorMethods::class . "\0privateProperty"] = null;
689
690
                return true;
691
            }
692
        );
693
694
        self::assertTrue($proxy1->has('protectedProperty'));
695
        self::assertTrue($proxy1->has('publicProperty'));
696
        self::assertTrue($proxy1->has('privateProperty'));
697
698
        self::assertFalse($proxy2->has('protectedProperty'));
699
        self::assertFalse($proxy2->has('publicProperty'));
700
        self::assertFalse($proxy2->has('privateProperty'));
701
    }
702
703
    public function testByRefInitialization() : void
704
    {
705
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
706
            ClassWithMixedProperties::class,
707
            static function (
708
                GhostObjectInterface $proxy,
709
                string $method,
710
                array $params,
711
                ?Closure & $initializer,
712
                array $properties
713
            ) : bool {
714
                $initializer                                                               = null;
715
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty0"] = 'private0';
716
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty1"] = 'private1';
717
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty2"] = 'private2';
718
                $properties["\0*\0protectedProperty0"]                                     = 'protected0';
719
                $properties["\0*\0protectedProperty1"]                                     = 'protected1';
720
                $properties["\0*\0protectedProperty2"]                                     = 'protected2';
721
                $properties['publicProperty0']                                             = 'public0';
722
                $properties['publicProperty1']                                             = 'public1';
723
                $properties['publicProperty2']                                             = 'public2';
724
725
                return true;
726
            }
727
        );
728
729
        $reflectionClass = new ReflectionClass(ClassWithMixedProperties::class);
730
731
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
732
            $property->setAccessible(true);
733
734
            self::assertSame(str_replace('Property', '', $property->getName()), $property->getValue($proxy));
735
        }
736
    }
737
738
    public function testByRefInitializationOfTypedProperties() : void
739
    {
740
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
741
            ClassWithMixedTypedProperties::class,
742
            static function (
743
                GhostObjectInterface $proxy,
744
                string $method,
745
                array $params,
746
                ?Closure & $initializer,
747
                array $properties
748
            ) : bool {
749
                $initializer                                                                         = null;
750
                $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"] = 'private0';
751
                $properties["\0*\0protectedStringProperty"]                                          = 'protected0';
752
                $properties['publicStringProperty']                                                  = 'public0';
753
754
                return true;
755
            }
756
        );
757
758
        $reflectionClass = new ReflectionClass(ClassWithMixedTypedProperties::class);
759
760
        $properties = Properties::fromReflectionClass($reflectionClass)->getInstanceProperties();
761
762
        $privateProperty   = $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"];
763
        $protectedProperty = $properties["\0*\0protectedStringProperty"];
764
765
        $privateProperty->setAccessible(true);
766
        $protectedProperty->setAccessible(true);
767
768
        self::assertSame('private0', $privateProperty->getValue($proxy));
769
        self::assertSame('protected0', $properties["\0*\0protectedStringProperty"]->getValue($proxy));
770
        self::assertSame('public0', $proxy->publicStringProperty);
771
    }
772
773
    /**
774
     * @group 115
775
     * @group 175
776
     */
777
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
778
    {
779
        $instance = new ClassWithCounterConstructor(10);
780
781
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
782
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
783
        $instance->__construct(3);
784
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
785
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
786
787
        $proxyName = get_class(
788
            (new LazyLoadingGhostFactory())
789
                ->createProxy(
790
                    ClassWithCounterConstructor::class,
791
                    static function () : bool {
792
                        return true;
793
                    }
794
                )
795
        );
796
797
        $proxy = new $proxyName(15);
798
799
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
800
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
801
        $proxy->__construct(5);
802
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
803
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
804
    }
805
806
    public function testInitializeProxyWillReturnTrueOnSuccessfulInitialization() : void
807
    {
808
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
809
            ClassWithMixedTypedProperties::class,
810
            $this->createInitializer(
811
                ClassWithMixedTypedProperties::class,
812
                new ClassWithMixedTypedProperties()
813
            )
814
        );
815
816
        self::assertTrue($proxy->initializeProxy());
817
        self::assertTrue($proxy->isProxyInitialized());
818
        self::assertFalse($proxy->initializeProxy());
819
    }
820
821
    /**
822
     * @psalm-param (CallableInterface&Mock)|null $initializerMatcher
823
     *
824
     * @psalm-return Closure(
825
     *   GhostObjectInterface $proxy,
826
     *   string $method,
827
     *   array $params,
828
     *   ?Closure $initializer
829
     * ) : bool
830
     */
831
    private function createInitializer(string $className, object $realInstance, ?Mock $initializerMatcher = null) : Closure
832
    {
833
        if (! $initializerMatcher) {
834
            $initializerMatcher = $this->createMock(CallableInterface::class);
835
836
            $initializerMatcher
837
                ->expects(self::once())
838
                ->method('__invoke')
839
                ->with(self::logicalAnd(
840
                    self::isInstanceOf(GhostObjectInterface::class),
841
                    self::isInstanceOf($className)
842
                ));
843
        }
844
845
        return static function (
846
            GhostObjectInterface $proxy,
847
            string $method,
848
            array $params,
849
            ?Closure & $initializer
850
        ) use (
851
            $initializerMatcher,
852
            $realInstance
853
        ) : bool {
854
            $initializer = null;
855
856
            $reflectionClass = new ReflectionClass($realInstance);
857
858
            foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
859
                if (! self::isPropertyInitialized($realInstance, $property)) {
860
                    continue;
861
                }
862
863
                $property->setAccessible(true);
864
                $property->setValue($proxy, $property->getValue($realInstance));
865
            }
866
867
            $initializerMatcher->__invoke($proxy, $method, $params);
868
869
            return true;
870
        };
871
    }
872
873
    /**
874
     * Generates a list of object | invoked method | parameters | expected result
875
     *
876
     * @return null[][]|string[][]|object[][]|mixed[][][]
877
     */
878
    public function getProxyMethods() : array
879
    {
880
        $selfHintParam = new ClassWithSelfHint();
881
        $empty         = new EmptyClass();
882
883
        return [
884
            [
885
                BaseClass::class,
886
                new BaseClass(),
887
                'publicMethod',
888
                [],
889
                'publicMethodDefault',
890
            ],
891
            [
892
                BaseClass::class,
893
                new BaseClass(),
894
                'publicTypeHintedMethod',
895
                [new stdClass()],
896
                'publicTypeHintedMethodDefault',
897
            ],
898
            [
899
                BaseClass::class,
900
                new BaseClass(),
901
                'publicByReferenceMethod',
902
                [],
903
                'publicByReferenceMethodDefault',
904
            ],
905
            [
906
                ClassWithSelfHint::class,
907
                new ClassWithSelfHint(),
908
                'selfHintMethod',
909
                ['parameter' => $selfHintParam],
910
                $selfHintParam,
911
            ],
912
            [
913
                ClassWithParentHint::class,
914
                new ClassWithParentHint(),
915
                'parentHintMethod',
916
                ['parameter' => $empty],
917
                $empty,
918
            ],
919
            [
920
                ClassWithAbstractPublicMethod::class,
921
                new EmptyClass(), // EmptyClass just used to not make reflection explode when synchronizing properties
922
                'publicAbstractMethod',
923
                [],
924
                null,
925
            ],
926
            [
927
                ClassWithMethodWithByRefVariadicFunction::class,
928
                new ClassWithMethodWithByRefVariadicFunction(),
929
                'tuz',
930
                ['Ocramius', 'Malukenho'],
931
                ['Ocramius', 'changed'],
932
            ],
933
        ];
934
    }
935
936
    /**
937
     * Generates a list of object | invoked method | parameters | expected result for methods that cause lazy-loading
938
     * of a ghost object
939
     *
940
     * @return string[][]|object[][]|mixed[][][]|null[][]
941
     */
942
    public function getProxyInitializingMethods() : array
943
    {
944
        return [
945
            [
946
                BaseClass::class,
947
                new BaseClass(),
948
                'publicPropertyGetter',
949
                [],
950
                'publicPropertyDefault',
951
            ],
952
            [
953
                BaseClass::class,
954
                new BaseClass(),
955
                'protectedPropertyGetter',
956
                [],
957
                'protectedPropertyDefault',
958
            ],
959
            [
960
                BaseClass::class,
961
                new BaseClass(),
962
                'privatePropertyGetter',
963
                [],
964
                'privatePropertyDefault',
965
            ],
966
            [
967
                ClassWithMethodWithVariadicFunction::class,
968
                new ClassWithMethodWithVariadicFunction(),
969
                'foo',
970
                ['Ocramius', 'Malukenho'],
971
                null,
972
            ],
973
        ];
974
    }
975
976
    /**
977
     * Generates a list of object | invoked method | parameters | expected result for methods DON'T cause lazy-loading
978
     *
979
     * @return null[][]|string[][]|object[][]|mixed[][][]
980
     */
981
    public function getProxyNonInitializingMethods() : array
982
    {
983
        return $this->getProxyMethods();
984
    }
985
986
    /**
987
     * Generates proxies and instances with a public property to feed to the property accessor methods
988
     *
989
     * @return string[][]|object[][]
990
     */
991
    public function getPropertyAccessProxies() : array
992
    {
993
        $instance1  = new BaseClass();
994
        $instance2  = new BaseClass();
995
996
        $factory = new LazyLoadingGhostFactory();
997
998
        /** @var GhostObjectInterface $serialized */
999
        $serialized = unserialize(serialize($factory->createProxy(
1000
            BaseClass::class,
1001
            $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...
1002
        )));
1003
1004
        return [
1005
            [
1006
                $instance1,
1007
                $factory->createProxy(
1008
                    BaseClass::class,
1009
                    $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...
1010
                ),
1011
                'publicProperty',
1012
                'publicPropertyDefault',
1013
            ],
1014
            [
1015
                $instance2,
1016
                $serialized,
1017
                'publicProperty',
1018
                'publicPropertyDefault',
1019
            ],
1020
        ];
1021
    }
1022
1023
    /**
1024
     * @param mixed   $expected
1025
     * @param mixed[] $proxyOptions
1026
     *
1027
     * @dataProvider skipPropertiesFixture
1028
     *
1029
     * @psalm-param class-string $className
1030
     * @psalm-param array{skippedProperties?: array<int, string>} $proxyOptions
1031
     */
1032
    public function testInitializationIsSkippedForSkippedProperties(
1033
        string $className,
1034
        string $propertyClass,
1035
        string $propertyName,
1036
        array $proxyOptions,
1037
        $expected
1038
    ) : void {
1039
        $ghostObject = (new LazyLoadingGhostFactory())->createProxy(
1040
            $className,
1041
            static function () use ($propertyName) : bool {
1042
                self::fail(sprintf('The Property "%s" was not expected to be lazy-loaded', $propertyName));
1043
1044
                return true;
1045
            },
1046
            $proxyOptions
1047
        );
1048
1049
        $property = new ReflectionProperty($propertyClass, $propertyName);
1050
        $property->setAccessible(true);
1051
1052
        self::assertSame($expected, $property->getValue($ghostObject));
1053
    }
1054
1055
    /**
1056
     * @param array<string, mixed> $proxyOptions
1057
     *
1058
     * @dataProvider skipPropertiesFixture
1059
     *
1060
     * @psalm-param class-string $className
1061
     * @psalm-param array{skippedProperties?: array<int, string>} $proxyOptions
1062
     */
1063
    public function testSkippedPropertiesAreNotOverwrittenOnInitialization(
1064
        string $className,
1065
        string $propertyClass,
1066
        string $propertyName,
1067
        array $proxyOptions
1068
    ) : void {
1069
        $ghostObject = (new LazyLoadingGhostFactory())->createProxy(
1070
            $className,
1071
            static function (
1072
                GhostObjectInterface $proxy,
1073
                string $method,
1074
                array $params,
1075
                ?Closure & $initializer
1076
            ) : bool {
1077
                $initializer = null;
1078
1079
                return true;
1080
            },
1081
            $proxyOptions
1082
        );
1083
1084
        $property = new ReflectionProperty($propertyClass, $propertyName);
1085
1086
        $property->setAccessible(true);
1087
1088
        $value = uniqid('', true);
1089
1090
        $property->setValue($ghostObject, $value);
1091
1092
        self::assertTrue($ghostObject->initializeProxy());
1093
1094
        self::assertSame(
1095
            $value,
1096
            $property->getValue($ghostObject),
1097
            'Property should not be changed by proxy initialization'
1098
        );
1099
    }
1100
1101
    /**
1102
     * @group 265
1103
     */
1104
    public function testWillForwardVariadicByRefArguments() : void
1105
    {
1106
        $object = (new LazyLoadingGhostFactory())->createProxy(
1107
            ClassWithMethodWithByRefVariadicFunction::class,
1108
            static function (
1109
                GhostObjectInterface $proxy,
1110
                string $method,
1111
                array $params,
1112
                ?Closure & $initializer
1113
            ) : bool {
1114
                $initializer = null;
1115
1116
                return true;
1117
            }
1118
        );
1119
1120
        $parameters = ['a', 'b', 'c'];
1121
1122
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1123
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
1124
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
1125
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
1126
    }
1127
1128
    /**
1129
     * @group 265
1130
     */
1131
    public function testWillForwardDynamicArguments() : void
1132
    {
1133
        $object = (new LazyLoadingGhostFactory())->createProxy(
1134
            ClassWithDynamicArgumentsMethod::class,
1135
            static function () : bool {
1136
                return true;
1137
            }
1138
        );
1139
1140
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1141
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
1142
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
1143
    }
1144
1145
    /**
1146
     * @return mixed[] in order:
1147
     *                  - the class to be proxied
1148
     *                  - the class owning the property to be checked
1149
     *                  - the property name
1150
     *                  - the options to be passed to the generator
1151
     *                  - the expected value of the property
1152
     */
1153
    public function skipPropertiesFixture() : array
1154
    {
1155
        return [
1156
            [
1157
                ClassWithPublicProperties::class,
1158
                ClassWithPublicProperties::class,
1159
                'property9',
1160
                [
1161
                    'skippedProperties' => ['property9'],
1162
                ],
1163
                'property9',
1164
            ],
1165
            [
1166
                ClassWithProtectedProperties::class,
1167
                ClassWithProtectedProperties::class,
1168
                'property9',
1169
                [
1170
                    'skippedProperties' => ["\0*\0property9"],
1171
                ],
1172
                'property9',
1173
            ],
1174
            [
1175
                ClassWithPrivateProperties::class,
1176
                ClassWithPrivateProperties::class,
1177
                'property9',
1178
                [
1179
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property9"],
1180
                ],
1181
                'property9',
1182
            ],
1183
            [
1184
                ClassWithCollidingPrivateInheritedProperties::class,
1185
                ClassWithCollidingPrivateInheritedProperties::class,
1186
                'property0',
1187
                [
1188
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithCollidingPrivateInheritedProperties\0property0"],
1189
                ],
1190
                'childClassProperty0',
1191
            ],
1192
            [
1193
                ClassWithCollidingPrivateInheritedProperties::class,
1194
                ClassWithPrivateProperties::class,
1195
                'property0',
1196
                [
1197
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property0"],
1198
                ],
1199
                'property0',
1200
            ],
1201
        ];
1202
    }
1203
1204
    /**
1205
     * @group        276
1206
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1207
     */
1208
    public function testWillLazyLoadMembersOfOtherProxiesWithTheSamePrivateScope(
1209
        object $callerObject,
1210
        string $method,
1211
        string $propertyIndex,
1212
        string $expectedValue
1213
    ) : void {
1214
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
1215
            OtherObjectAccessClass::class,
1216
            static function (
1217
                GhostObjectInterface $proxy,
1218
                string $method,
1219
                array $params,
1220
                ?Closure & $initializer,
1221
                array $properties
1222
            ) use ($propertyIndex, $expectedValue) : bool {
0 ignored issues
show
Coding Style introduced by
Multi-line use declarations must define one parameter per line
Loading history...
1223
                $initializer = null;
1224
1225
                $properties[$propertyIndex] = $expectedValue;
1226
1227
                return true;
1228
            }
1229
        );
1230
1231
        $accessor = [$callerObject, $method];
1232
1233
        self::assertIsCallable($accessor);
1234
1235
        self::assertFalse($proxy->isProxyInitialized());
1236
        self::assertSame($expectedValue, $accessor($proxy));
1237
        self::assertTrue($proxy->isProxyInitialized());
1238
    }
1239
1240
    /**
1241
     * @group        276
1242
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1243
     */
1244
    public function testWillAccessMembersOfOtherDeSerializedProxiesWithTheSamePrivateScope(
1245
        object $callerObject,
1246
        string $method,
1247
        string $propertyIndex,
1248
        string $expectedValue
1249
    ) : void {
1250
        /** @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...
1251
        $proxy = unserialize(serialize(
1252
            (new LazyLoadingGhostFactory())->createProxy(
1253
                OtherObjectAccessClass::class,
1254
            static function (
1255
                GhostObjectInterface$proxy,
1256
                string $method,
1257
                array $params,
1258
                ?Closure & $initializer,
1259
                array $properties
1260
            ) use ($propertyIndex, $expectedValue) : bool {
0 ignored issues
show
Coding Style introduced by
Multi-line use declarations must define one parameter per line
Loading history...
1261
                $initializer = null;
1262
1263
                $properties[$propertyIndex] = $expectedValue;
1264
1265
                return true;
1266
            }
1267
        )));
1268
1269
        $accessor = [$callerObject, $method];
1270
1271
        self::assertIsCallable($accessor);
1272
1273
        self::assertTrue($proxy->isProxyInitialized());
1274
        self::assertSame($expectedValue, $accessor($proxy));
1275
    }
1276
1277
    /**
1278
     * @group        276
1279
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1280
     */
1281
    public function testWillAccessMembersOfOtherClonedProxiesWithTheSamePrivateScope(
1282
        object $callerObject,
1283
        string $method,
1284
        string $propertyIndex,
1285
        string $expectedValue
1286
    ) : void {
1287
        $proxy = clone (new LazyLoadingGhostFactory())->createProxy(
1288
            OtherObjectAccessClass::class,
1289
            static function (
1290
                GhostObjectInterface $proxy,
1291
                string $method,
1292
                array $params,
1293
                ?Closure & $initializer,
1294
                array $properties
1295
            ) use ($propertyIndex, $expectedValue) : bool {
0 ignored issues
show
Coding Style introduced by
Multi-line use declarations must define one parameter per line
Loading history...
1296
                $initializer = null;
1297
1298
                $properties[$propertyIndex] = $expectedValue;
1299
1300
                return true;
1301
            }
1302
        );
1303
1304
        $accessor = [$callerObject, $method];
1305
1306
        self::assertIsCallable($accessor);
1307
1308
        self::assertTrue($proxy->isProxyInitialized());
1309
        self::assertSame($expectedValue, $accessor($proxy));
1310
    }
1311
1312
    /** @return string[][]|object[][] */
1313
    public function getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() : array
1314
    {
1315
        $factory = new LazyLoadingGhostFactory();
1316
1317
        return [
1318
            OtherObjectAccessClass::class . '#$privateProperty'                => [
1319
                new OtherObjectAccessClass(),
1320
                'getPrivateProperty',
1321
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1322
                uniqid('', true),
1323
            ],
1324
            OtherObjectAccessClass::class . '#$protectedProperty'              => [
1325
                new OtherObjectAccessClass(),
1326
                'getProtectedProperty',
1327
                "\0*\0protectedProperty",
1328
                uniqid('', true),
1329
            ],
1330
            OtherObjectAccessClass::class . '#$publicProperty'                 => [
1331
                new OtherObjectAccessClass(),
1332
                'getPublicProperty',
1333
                'publicProperty',
1334
                uniqid('', true),
1335
            ],
1336
            '(proxy) ' . OtherObjectAccessClass::class . '#$privateProperty'   => [
1337
                $factory->createProxy(
1338
                    OtherObjectAccessClass::class,
1339
                    static function () : bool {
1340
                        self::fail('Should never be initialized, as its values aren\'t accessed');
1341
1342
                        return true;
1343
                    }
1344
                ),
1345
                'getPrivateProperty',
1346
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1347
                uniqid('', true),
1348
            ],
1349
            '(proxy) ' . OtherObjectAccessClass::class . '#$protectedProperty' => [
1350
                $factory->createProxy(
1351
                    OtherObjectAccessClass::class,
1352
                    static function () : bool {
1353
                        self::fail('Should never be initialized, as its values aren\'t accessed');
1354
1355
                        return true;
1356
                    }
1357
                ),
1358
                'getProtectedProperty',
1359
                "\0*\0protectedProperty",
1360
                uniqid('', true),
1361
            ],
1362
            '(proxy) ' . OtherObjectAccessClass::class . '#$publicProperty'    => [
1363
                $factory->createProxy(
1364
                    OtherObjectAccessClass::class,
1365
                    static function () : bool {
1366
                        self::fail('Should never be initialized, as its values aren\'t accessed');
1367
1368
                        return true;
1369
                    }
1370
                ),
1371
                'getPublicProperty',
1372
                'publicProperty',
1373
                uniqid('', true),
1374
            ],
1375
        ];
1376
    }
1377
1378
    /**
1379
     * @group 276
1380
     */
1381
    public function testFriendObjectWillNotCauseLazyLoadingOnSkippedProperty() : void
1382
    {
1383
        $proxy = (new LazyLoadingGhostFactory())
1384
            ->createProxy(
1385
                OtherObjectAccessClass::class,
1386
                static function () : bool {
1387
                    throw new BadMethodCallException('The proxy should never be initialized, as all properties are skipped');
1388
                },
1389
                [
1390
                    'skippedProperties' => [
1391
                        "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1392
                        "\0*\0protectedProperty",
1393
                        'publicProperty',
1394
                    ],
1395
                ]
1396
            );
1397
1398
        $privatePropertyValue   = uniqid('', true);
1399
        $protectedPropertyValue = uniqid('', true);
1400
        $publicPropertyValue    = uniqid('', true);
1401
1402
        $reflectionPrivateProperty = new ReflectionProperty(OtherObjectAccessClass::class, 'privateProperty');
1403
1404
        $reflectionPrivateProperty->setAccessible(true);
1405
        $reflectionPrivateProperty->setValue($proxy, $privatePropertyValue);
1406
1407
        $reflectionProtectedProperty = new ReflectionProperty(OtherObjectAccessClass::class, 'protectedProperty');
1408
1409
        $reflectionProtectedProperty->setAccessible(true);
1410
        $reflectionProtectedProperty->setValue($proxy, $protectedPropertyValue);
1411
1412
        $proxy->publicProperty = $publicPropertyValue;
1413
1414
        $friendObject = new OtherObjectAccessClass();
1415
1416
        self::assertSame($privatePropertyValue, $friendObject->getPrivateProperty($proxy));
1417
        self::assertSame($protectedPropertyValue, $friendObject->getProtectedProperty($proxy));
1418
        self::assertSame($publicPropertyValue, $friendObject->getPublicProperty($proxy));
1419
    }
1420
1421
    public function testClonedSkippedPropertiesArePreserved() : void
1422
    {
1423
        $proxy = (new LazyLoadingGhostFactory())
1424
            ->createProxy(
1425
                BaseClass::class,
1426
                static function (GhostObjectInterface $proxy) : bool {
1427
                    $proxy->setProxyInitializer(null);
1428
1429
                    return true;
1430
                },
1431
                [
1432
                    'skippedProperties' => [
1433
                        "\0" . BaseClass::class . "\0privateProperty",
1434
                        "\0*\0protectedProperty",
1435
                        'publicProperty',
1436
                    ],
1437
                ]
1438
            );
1439
1440
        $reflectionPrivate   = new ReflectionProperty(BaseClass::class, 'privateProperty');
1441
        $reflectionProtected = new ReflectionProperty(BaseClass::class, 'protectedProperty');
1442
1443
        $reflectionPrivate->setAccessible(true);
1444
        $reflectionProtected->setAccessible(true);
1445
1446
        $privateValue   = uniqid('', true);
1447
        $protectedValue = uniqid('', true);
1448
        $publicValue    = uniqid('', true);
1449
1450
        $reflectionPrivate->setValue($proxy, $privateValue);
1451
        $reflectionProtected->setValue($proxy, $protectedValue);
1452
        $proxy->publicProperty = $publicValue;
1453
1454
        self::assertFalse($proxy->isProxyInitialized());
1455
1456
        $clone = clone $proxy;
1457
1458
        self::assertFalse($proxy->isProxyInitialized());
1459
        self::assertTrue($clone->isProxyInitialized());
1460
1461
        self::assertSame($privateValue, $reflectionPrivate->getValue($proxy));
1462
        self::assertSame($privateValue, $reflectionPrivate->getValue($clone));
1463
        self::assertSame($protectedValue, $reflectionProtected->getValue($proxy));
1464
        self::assertSame($protectedValue, $reflectionProtected->getValue($clone));
1465
        self::assertSame($publicValue, $proxy->publicProperty);
1466
        self::assertSame($publicValue, $clone->publicProperty);
1467
    }
1468
1469
    /**
1470
     * @group 327
1471
     */
1472
    public function testWillExecuteLogicInAVoidMethod() : void
1473
    {
1474
        $initialCounter = random_int(10, 1000);
1475
1476
        $proxy = (new LazyLoadingGhostFactory())->createProxy(
1477
            VoidCounter::class,
1478
            static function (
1479
                LazyLoadingInterface $proxy,
1480
                string $method,
1481
                array $params,
1482
                ?Closure & $initializer,
1483
                array $properties
1484
            ) use ($initialCounter) : bool {
1485
                $initializer = null;
1486
1487
                $properties['counter'] = $initialCounter;
1488
1489
                return true;
1490
            }
1491
        );
1492
1493
        $increment = random_int(1001, 10000);
1494
1495
        $proxy->increment($increment);
1496
1497
        self::assertSame($initialCounter + $increment, $proxy->counter);
1498
    }
1499
1500
    private static function isPropertyInitialized(object $object, ReflectionProperty $property) : bool
1501
    {
1502
        return array_key_exists(
1503
            ($property->isProtected() ? "\0*\0" : '')
1504
            . ($property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : '')
1505
            . $property->getName(),
1506
            (array) $object
1507
        );
1508
    }
1509
1510
    /**
1511
     * @param mixed $expected
1512
     * @param mixed $actual
1513
     */
1514
    private static function assertByRefVariableValueSame($expected, & $actual) : void
1515
    {
1516
        self::assertSame($expected, $actual);
1517
    }
1518
}
1519