Completed
Pull Request — master (#425)
by Marco
01:56
created

skipPropertiesFixture()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1248
    }
1249
1250
    public function testClonedSkippedPropertiesArePreserved() : void
1251
    {
1252
        $proxyName = $this->generateProxy(
1253
            BaseClass::class,
1254
            [
1255
                'skippedProperties' => [
1256
                    "\0" . BaseClass::class . "\0privateProperty",
1257
                    "\0*\0protectedProperty",
1258
                    'publicProperty',
1259
                ],
1260
            ]
1261
        );
1262
1263
        /** @var BaseClass|GhostObjectInterface $proxy */
1264
        $proxy = $proxyName::staticProxyConstructor(function ($proxy) : void {
1265
            $proxy->setProxyInitializer(null);
1266
        });
1267
1268
        self::assertInstanceOf(BaseClass::class, $proxy);
1269
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1270
1271
        $reflectionPrivate   = new \ReflectionProperty(BaseClass::class, 'privateProperty');
1272
        $reflectionProtected = new \ReflectionProperty(BaseClass::class, 'protectedProperty');
1273
1274
        $reflectionPrivate->setAccessible(true);
1275
        $reflectionProtected->setAccessible(true);
1276
1277
        $privateValue   = uniqid('', true);
1278
        $protectedValue = uniqid('', true);
1279
        $publicValue    = uniqid('', true);
1280
1281
        $reflectionPrivate->setValue($proxy, $privateValue);
1282
        $reflectionProtected->setValue($proxy, $protectedValue);
1283
        $proxy->publicProperty = $publicValue;
1284
1285
        self::assertFalse($proxy->isProxyInitialized());
0 ignored issues
show
Bug introduced by
The method isProxyInitialized does only exist in ProxyManager\Proxy\GhostObjectInterface, but not in ProxyManagerTestAsset\BaseClass.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1286
1287
        $clone = clone $proxy;
1288
1289
        self::assertFalse($proxy->isProxyInitialized());
1290
        self::assertTrue($clone->isProxyInitialized());
1291
1292
        self::assertSame($privateValue, $reflectionPrivate->getValue($proxy));
1293
        self::assertSame($privateValue, $reflectionPrivate->getValue($clone));
1294
        self::assertSame($protectedValue, $reflectionProtected->getValue($proxy));
1295
        self::assertSame($protectedValue, $reflectionProtected->getValue($clone));
1296
        self::assertSame($publicValue, $proxy->publicProperty);
1297
        self::assertSame($publicValue, $clone->publicProperty);
1298
    }
1299
1300
    /**
1301
     * @group 327
1302
     */
1303
    public function testWillExecuteLogicInAVoidMethod() : void
1304
    {
1305
        $proxyName = $this->generateProxy(VoidCounter::class);
1306
1307
        $initialCounter = random_int(10, 1000);
1308
1309
        /** @var VoidCounter|LazyLoadingInterface $proxy */
1310
        $proxy = $proxyName::staticProxyConstructor(
1311
            function (VoidCounter $proxy, $method, $params, & $initializer, array $props) use ($initialCounter) : bool {
1312
                $initializer = null;
1313
1314
                $props['counter'] = $initialCounter;
1315
1316
                return true;
1317
            }
1318
        );
1319
1320
        self::assertInstanceOf(VoidCounter::class, $proxy);
1321
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1322
1323
        $increment = random_int(1001, 10000);
1324
1325
        $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...
1326
1327
        self::assertSame($initialCounter + $increment, $proxy->counter);
1328
    }
1329
}
1330