Completed
Pull Request — master (#423)
by Marco
24:53
created

LazyLoadingGhostFunctionalTest::generateProxy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 2
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\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_values;
39
use function get_class;
40
use function get_parent_class;
41
use function random_int;
42
use function serialize;
43
use function sprintf;
44
use function str_replace;
45
use function uniqid;
46
use function unserialize;
47
48
/**
49
 * Tests for {@see \ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator} produced objects
50
 *
51
 * @group Functional
52
 * @coversNothing
53
 */
54
class LazyLoadingGhostFunctionalTest extends TestCase
55
{
56
    /**
57
     * @dataProvider getProxyInitializingMethods
58
     *
59
     * @param mixed[] $params
60
     * @param mixed   $expectedValue
61
     */
62
    public function testMethodCallsThatLazyLoadTheObject(
63
        string $className,
64
        object $instance,
65
        string $method,
66
        array $params,
67
        $expectedValue
68
    ) : void {
69
        $proxyName = $this->generateProxy($className);
70
71
        /** @var GhostObjectInterface $proxy */
72
        $proxy = $proxyName::staticProxyConstructor($this->createInitializer($className, $instance));
73
74
        self::assertFalse($proxy->isProxyInitialized());
75
76
        /** @var callable $callProxyMethod */
77
        $callProxyMethod = [$proxy, $method];
78
        $parameterValues = array_values($params);
79
80
        self::assertInternalType('callable', $callProxyMethod);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
81
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
82
        self::assertTrue($proxy->isProxyInitialized());
83
    }
84
85
    /**
86
     * @dataProvider getProxyNonInitializingMethods
87
     *
88
     * @param mixed[] $params
89
     * @param mixed   $expectedValue
90
     */
91
    public function testMethodCallsThatDoNotLazyLoadTheObject(
92
        string $className,
93
        object $instance,
94
        string $method,
95
        array $params,
96
        $expectedValue
97
    ) : void {
98
        $proxyName         = $this->generateProxy($className);
99
        $initializeMatcher = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
100
101
        $initializeMatcher->expects(self::never())->method('__invoke'); // should not initialize the proxy
102
103
        /** @var GhostObjectInterface $proxy */
104
        $proxy = $proxyName::staticProxyConstructor(
105
            $this->createInitializer($className, $instance, $initializeMatcher)
106
        );
107
108
        self::assertFalse($proxy->isProxyInitialized());
109
110
        /** @var callable $callProxyMethod */
111
        $callProxyMethod = [$proxy, $method];
112
        $parameterValues = array_values($params);
113
114
        self::assertInternalType('callable', $callProxyMethod);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
115
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
116
        self::assertFalse($proxy->isProxyInitialized());
117
    }
118
119
    /**
120
     * @dataProvider getProxyMethods
121
     *
122
     * @param mixed[] $params
123
     * @param mixed   $expectedValue
124
     */
125
    public function testMethodCallsAfterUnSerialization(
126
        string $className,
127
        object $instance,
128
        string $method,
129
        array $params,
130
        $expectedValue
131
    ) : void {
132
        $proxyName = $this->generateProxy($className);
133
134
        /** @var GhostObjectInterface $proxy */
135
        $proxy = unserialize(serialize($proxyName::staticProxyConstructor(
136
            $this->createInitializer($className, $instance)
137
        )));
138
139
        self::assertTrue($proxy->isProxyInitialized());
140
141
        /** @var callable $callProxyMethod */
142
        $callProxyMethod = [$proxy, $method];
143
        $parameterValues = array_values($params);
144
145
        self::assertInternalType('callable', $callProxyMethod);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
146
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
147
    }
148
149
    /**
150
     * @dataProvider getProxyMethods
151
     *
152
     * @param mixed[] $params
153
     * @param mixed   $expectedValue
154
     */
155
    public function testMethodCallsAfterCloning(
156
        string $className,
157
        object $instance,
158
        string $method,
159
        array $params,
160
        $expectedValue
161
    ) : void {
162
        $proxyName = $this->generateProxy($className);
163
164
        /** @var GhostObjectInterface $proxy */
165
        $proxy  = $proxyName::staticProxyConstructor($this->createInitializer($className, $instance));
166
        $cloned = clone $proxy;
167
168
        self::assertTrue($cloned->isProxyInitialized());
169
170
        /** @var callable $callProxyMethod */
171
        $callProxyMethod = [$proxy, $method];
172
        $parameterValues = array_values($params);
173
174
        self::assertInternalType('callable', $callProxyMethod);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
175
        self::assertSame($expectedValue, $callProxyMethod(...$parameterValues));
176
    }
177
178
    /**
179
     * @dataProvider getPropertyAccessProxies
180
     *
181
     * @param mixed $propertyValue
182
     */
183
    public function testPropertyReadAccess(
184
        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...
185
        GhostObjectInterface $proxy,
186
        string $publicProperty,
187
        $propertyValue
188
    ) : void {
189
        self::assertSame($propertyValue, $proxy->$publicProperty);
190
        self::assertTrue($proxy->isProxyInitialized());
191
    }
192
193
    /**
194
     * @dataProvider getPropertyAccessProxies
195
     *
196
     */
197
    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...
198
    ) : void
199
    {
200
        $newValue               = uniqid('', true);
201
        $proxy->$publicProperty = $newValue;
202
203
        self::assertTrue($proxy->isProxyInitialized());
204
        self::assertSame($newValue, $proxy->$publicProperty);
205
    }
206
207
    /**
208
     * @dataProvider getPropertyAccessProxies
209
     *
210
     */
211
    public function testPropertyExistence(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
212
    {
213
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
214
        self::assertTrue($proxy->isProxyInitialized());
215
    }
216
217
    /**
218
     * @dataProvider getPropertyAccessProxies
219
     *
220
     */
221
    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...
222
    {
223
        $proxy->$publicProperty = null;
224
        self::assertFalse(isset($proxy->$publicProperty));
225
        self::assertTrue($proxy->isProxyInitialized());
226
    }
227
228
    /**
229
     * @dataProvider getPropertyAccessProxies
230
     *
231
     */
232
    public function testPropertyUnset(object $instance, GhostObjectInterface $proxy, string $publicProperty) : void
233
    {
234
        unset($proxy->$publicProperty);
235
236
        self::assertTrue($proxy->isProxyInitialized());
237
        self::assertTrue(isset($instance->$publicProperty));
238
        self::assertFalse(isset($proxy->$publicProperty));
239
    }
240
241
    /**
242
     * Verifies that accessing a public property containing an array behaves like in a normal context
243
     */
244
    public function testCanWriteToArrayKeysInPublicProperty() : void
245
    {
246
        $instance    = new ClassWithPublicArrayProperty();
247
        $className   = get_class($instance);
248
        $initializer = $this->createInitializer($className, $instance);
0 ignored issues
show
Documentation introduced by
$instance 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...
249
        $proxyName   = $this->generateProxy($className);
250
        /** @var ClassWithPublicArrayProperty $proxy */
251
        $proxy = $proxyName::staticProxyConstructor($initializer);
252
253
        $proxy->arrayProperty['foo'] = 'bar';
254
255
        self::assertSame('bar', $proxy->arrayProperty['foo']);
256
257
        $proxy->arrayProperty = ['tab' => 'taz'];
258
259
        self::assertSame(['tab' => 'taz'], $proxy->arrayProperty);
260
    }
261
262
    /**
263
     * Verifies that public properties retrieved via `__get` don't get modified in the object itself
264
     */
265
    public function testWillNotModifyRetrievedPublicProperties() : void
266
    {
267
        $instance    = new ClassWithPublicProperties();
268
        $className   = get_class($instance);
269
        $initializer = $this->createInitializer($className, $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...
270
        $proxyName   = $this->generateProxy($className);
271
        /** @var ClassWithPublicProperties $proxy */
272
        $proxy    = $proxyName::staticProxyConstructor($initializer);
273
        $variable = $proxy->property0;
274
275
        self::assertSame('property0', $variable);
276
277
        $variable = 'foo';
278
279
        self::assertSame('property0', $proxy->property0);
280
        self::assertSame('foo', $variable);
281
    }
282
283
    /**
284
     * Verifies that public properties references retrieved via `__get` modify in the object state
285
     */
286
    public function testWillModifyByRefRetrievedPublicProperties() : void
287
    {
288
        $instance    = new ClassWithPublicProperties();
289
        $className   = get_class($instance);
290
        $initializer = $this->createInitializer($className, $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...
291
        $proxyName   = $this->generateProxy($className);
292
        /** @var ClassWithPublicProperties $proxy */
293
        $proxy    = $proxyName::staticProxyConstructor($initializer);
294
        $variable = &$proxy->property0;
295
296
        self::assertSame('property0', $variable);
297
298
        $variable = 'foo';
299
300
        self::assertSame('foo', $proxy->property0);
301
        self::assertSame('foo', $variable);
302
    }
303
304
    public function testKeepsInitializerWhenNotOverwitten() : void
305
    {
306
        $instance    = new BaseClass();
307
        $proxyName   = $this->generateProxy(get_class($instance));
308
        $initializer = function () : void {
309
        };
310
        /** @var GhostObjectInterface $proxy */
311
        $proxy = $proxyName::staticProxyConstructor($initializer);
312
313
        $proxy->initializeProxy();
314
315
        self::assertSame($initializer, $proxy->getProxyInitializer());
316
    }
317
318
    /**
319
     * Verifies that public properties are not being initialized multiple times
320
     */
321
    public function testKeepsInitializedPublicProperties() : void
322
    {
323
        $instance    = new BaseClass();
324
        $proxyName   = $this->generateProxy(get_class($instance));
325
        $initializer = function (BaseClass $proxy, string $method, $parameters, & $initializer) : void {
326
            $initializer           = null;
327
            $proxy->publicProperty = 'newValue';
328
        };
329
        /** @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...
330
        $proxy = $proxyName::staticProxyConstructor($initializer);
331
332
        $proxy->initializeProxy();
333
        self::assertSame('newValue', $proxy->publicProperty);
334
335
        $proxy->publicProperty = 'otherValue';
336
337
        $proxy->initializeProxy();
338
339
        self::assertSame('otherValue', $proxy->publicProperty);
340
    }
341
342
    /**
343
     * Verifies that properties' default values are preserved
344
     */
345
    public function testPublicPropertyDefaultWillBePreserved() : void
346
    {
347
        $instance  = new ClassWithPublicProperties();
348
        $proxyName = $this->generateProxy(get_class($instance));
349
        /** @var ClassWithPublicProperties $proxy */
350
        $proxy = $proxyName::staticProxyConstructor(function () : void {
351
        });
352
353
        self::assertSame('property0', $proxy->property0);
354
    }
355
356
    /**
357
     * Verifies that protected properties' default values are preserved
358
     */
359
    public function testProtectedPropertyDefaultWillBePreserved() : void
360
    {
361
        $instance  = new ClassWithProtectedProperties();
362
        $proxyName = $this->generateProxy(get_class($instance));
363
        /** @var ClassWithProtectedProperties $proxy */
364
        $proxy = $proxyName::staticProxyConstructor(function () : void {
365
        });
366
367
        // Check protected property via reflection
368
        $reflectionProperty = new ReflectionProperty($instance, 'property0');
369
        $reflectionProperty->setAccessible(true);
370
371
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
372
    }
373
374
    /**
375
     * Verifies that private properties' default values are preserved
376
     */
377
    public function testPrivatePropertyDefaultWillBePreserved() : void
378
    {
379
        $instance  = new ClassWithPrivateProperties();
380
        $proxyName = $this->generateProxy(get_class($instance));
381
        /** @var ClassWithPrivateProperties $proxy */
382
        $proxy = $proxyName::staticProxyConstructor(function () : void {
383
        });
384
385
        // Check protected property via reflection
386
        $reflectionProperty = new ReflectionProperty($instance, 'property0');
387
        $reflectionProperty->setAccessible(true);
388
389
        self::assertSame('property0', $reflectionProperty->getValue($proxy));
390
    }
391
392
    /**
393
     * @group 159
394
     * @group 192
395
     */
396
    public function testMultiLevelPrivatePropertiesDefaultsWillBePreserved() : void
397
    {
398
        $instance  = new ClassWithCollidingPrivateInheritedProperties();
399
        $proxyName = $this->generateProxy(ClassWithCollidingPrivateInheritedProperties::class);
400
        /** @var ClassWithPrivateProperties $proxy */
401
        $proxy = $proxyName::staticProxyConstructor(function () : void {
402
        });
403
404
        $childProperty  = new ReflectionProperty($instance, 'property0');
405
        $parentProperty = new ReflectionProperty(get_parent_class($instance), 'property0');
406
407
        $childProperty->setAccessible(true);
408
        $parentProperty->setAccessible(true);
409
410
        self::assertSame('childClassProperty0', $childProperty->getValue($proxy));
411
        self::assertSame('property0', $parentProperty->getValue($proxy));
412
    }
413
414
    /**
415
     * @group 159
416
     * @group 192
417
     */
418
    public function testMultiLevelPrivatePropertiesByRefInitialization() : void
419
    {
420
        $class     = ClassWithCollidingPrivateInheritedProperties::class;
421
        $proxyName = $this->generateProxy($class);
422
        /** @var ClassWithPrivateProperties $proxy */
423
        $proxy = $proxyName::staticProxyConstructor(
424
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
425
                $initializer                                                 = null;
426
                $properties["\0" . $class . "\0property0"]                   = 'foo';
427
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'bar';
428
            }
429
        );
430
431
        $childProperty  = new ReflectionProperty($class, 'property0');
432
        $parentProperty = new ReflectionProperty(get_parent_class($class), 'property0');
433
434
        $childProperty->setAccessible(true);
435
        $parentProperty->setAccessible(true);
436
437
        self::assertSame('foo', $childProperty->getValue($proxy));
438
        self::assertSame('bar', $parentProperty->getValue($proxy));
439
    }
440
441
    /**
442
     * @group 159
443
     * @group 192
444
     *
445
     * Test designed to verify that the cached logic does take into account the fact that
446
     * proxies are different instances
447
     */
448
    public function testGetPropertyFromDifferentProxyInstances() : void
449
    {
450
        $class     = ClassWithCollidingPrivateInheritedProperties::class;
451
        $proxyName = $this->generateProxy($class);
452
453
        /** @var ClassWithPrivateProperties $proxy1 */
454
        $proxy1 = $proxyName::staticProxyConstructor(
455
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
456
                $initializer                                                 = null;
457
                $properties["\0" . $class . "\0property0"]                   = 'foo';
458
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'bar';
459
            }
460
        );
461
        /** @var ClassWithPrivateProperties $proxy2 */
462
        $proxy2 = $proxyName::staticProxyConstructor(
463
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
464
                $initializer                                                 = null;
465
                $properties["\0" . $class . "\0property0"]                   = 'baz';
466
                $properties["\0" . get_parent_class($class) . "\0property0"] = 'tab';
467
            }
468
        );
469
470
        $childProperty  = new ReflectionProperty($class, 'property0');
471
        $parentProperty = new ReflectionProperty(get_parent_class($class), 'property0');
472
473
        $childProperty->setAccessible(true);
474
        $parentProperty->setAccessible(true);
475
476
        self::assertSame('foo', $childProperty->getValue($proxy1));
477
        self::assertSame('bar', $parentProperty->getValue($proxy1));
478
479
        self::assertSame('baz', $childProperty->getValue($proxy2));
480
        self::assertSame('tab', $parentProperty->getValue($proxy2));
481
    }
482
483
    /**
484
     * @group 159
485
     * @group 192
486
     *
487
     * Test designed to verify that the cached logic does take into account the fact that
488
     * proxies are different instances
489
     */
490
    public function testSetPrivatePropertyOnDifferentProxyInstances() : void
491
    {
492
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
493
        $proxyName = $this->generateProxy($class);
494
495
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
496
        $proxy1 = $proxyName::staticProxyConstructor(
497
            function ($proxy, $method, $params, & $initializer) : void {
498
                $initializer = null;
499
            }
500
        );
501
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
502
        $proxy2 = $proxyName::staticProxyConstructor(
503
            function ($proxy, $method, $params, & $initializer) : void {
504
                $initializer = null;
505
            }
506
        );
507
508
        $proxy1->set('privateProperty', 'private1');
509
        $proxy2->set('privateProperty', 'private2');
510
        self::assertSame('private1', $proxy1->get('privateProperty'));
511
        self::assertSame('private2', $proxy2->get('privateProperty'));
512
    }
513
514
    /**
515
     * @group 159
516
     * @group 192
517
     *
518
     * Test designed to verify that the cached logic does take into account the fact that
519
     * proxies are different instances
520
     */
521
    public function testIssetPrivatePropertyOnDifferentProxyInstances() : void
522
    {
523
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
524
        $proxyName = $this->generateProxy($class);
525
526
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
527
        $proxy1 = $proxyName::staticProxyConstructor(
528
            function ($proxy, $method, $params, & $initializer) : void {
529
                $initializer = null;
530
            }
531
        );
532
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
533
        $proxy2 = $proxyName::staticProxyConstructor(
534
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
535
                $initializer                                     = null;
536
                $properties["\0" . $class . "\0privateProperty"] = null;
537
            }
538
        );
539
540
        self::assertTrue($proxy1->has('privateProperty'));
541
        self::assertFalse($proxy2->has('privateProperty'));
542
        self::assertTrue($proxy1->has('privateProperty'));
543
        self::assertFalse($proxy2->has('privateProperty'));
544
    }
545
546
    /**
547
     * @group 159
548
     * @group 192
549
     *
550
     * Test designed to verify that the cached logic does take into account the fact that
551
     * proxies are different instances
552
     */
553
    public function testUnsetPrivatePropertyOnDifferentProxyInstances() : void
554
    {
555
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
556
        $proxyName = $this->generateProxy($class);
557
558
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
559
        $proxy1 = $proxyName::staticProxyConstructor(
560
            function ($proxy, $method, $params, & $initializer) : void {
561
                $initializer = null;
562
            }
563
        );
564
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
565
        $proxy2 = $proxyName::staticProxyConstructor(
566
            function ($proxy, $method, $params, & $initializer) : void {
567
                $initializer = null;
568
            }
569
        );
570
571
        self::assertTrue($proxy1->has('privateProperty'));
572
        $proxy2->remove('privateProperty');
573
        self::assertFalse($proxy2->has('privateProperty'));
574
        self::assertTrue($proxy1->has('privateProperty'));
575
        $proxy1->remove('privateProperty');
576
        self::assertFalse($proxy1->has('privateProperty'));
577
        self::assertFalse($proxy2->has('privateProperty'));
578
    }
579
580
    /**
581
     * @group 159
582
     * @group 192
583
     *
584
     * Test designed to verify that the cached logic does take into account the fact that
585
     * proxies are different instances
586
     */
587
    public function testIssetPrivateAndProtectedPropertiesDoesCheckAgainstBooleanFalse() : void
588
    {
589
        $class     = ClassWithMixedPropertiesAndAccessorMethods::class;
590
        $proxyName = $this->generateProxy($class);
591
592
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy1 */
593
        $proxy1 = $proxyName::staticProxyConstructor(
594
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
595
                $initializer                                     = null;
596
                $properties['publicProperty']                    = false;
597
                $properties["\0*\0protectedProperty"]            = false;
598
                $properties["\0" . $class . "\0privateProperty"] = false;
599
            }
600
        );
601
        /** @var ClassWithMixedPropertiesAndAccessorMethods $proxy2 */
602
        $proxy2 = $proxyName::staticProxyConstructor(
603
            function ($proxy, $method, $params, & $initializer, array $properties) use ($class) : void {
604
                $initializer                                     = null;
605
                $properties['publicProperty']                    = null;
606
                $properties["\0*\0protectedProperty"]            = null;
607
                $properties["\0" . $class . "\0privateProperty"] = null;
608
            }
609
        );
610
611
        self::assertTrue($proxy1->has('protectedProperty'));
612
        self::assertTrue($proxy1->has('publicProperty'));
613
        self::assertTrue($proxy1->has('privateProperty'));
614
615
        self::assertFalse($proxy2->has('protectedProperty'));
616
        self::assertFalse($proxy2->has('publicProperty'));
617
        self::assertFalse($proxy2->has('privateProperty'));
618
    }
619
620
    public function testByRefInitialization() : void
621
    {
622
        $proxyName = $this->generateProxy(ClassWithMixedProperties::class);
623
        /** @var ClassWithPrivateProperties $proxy */
624
        $proxy = $proxyName::staticProxyConstructor(
625
            function ($proxy, $method, $params, & $initializer, array $properties) : void {
626
                $initializer                                                               = null;
627
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty0"] = 'private0';
628
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty1"] = 'private1';
629
                $properties["\0" . ClassWithMixedProperties::class . "\0privateProperty2"] = 'private2';
630
                $properties["\0*\0protectedProperty0"]                                     = 'protected0';
631
                $properties["\0*\0protectedProperty1"]                                     = 'protected1';
632
                $properties["\0*\0protectedProperty2"]                                     = 'protected2';
633
                $properties['publicProperty0']                                             = 'public0';
634
                $properties['publicProperty1']                                             = 'public1';
635
                $properties['publicProperty2']                                             = 'public2';
636
            }
637
        );
638
639
        $reflectionClass = new ReflectionClass(ClassWithMixedProperties::class);
640
641
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
642
            $property->setAccessible(true);
643
644
            self::assertSame(str_replace('Property', '', $property->getName()), $property->getValue($proxy));
645
        }
646
    }
647
648
    public function testByRefInitializationOfTypedProperties() : void
649
    {
650
        $proxyName = $this->generateProxy(ClassWithMixedTypedProperties::class);
651
        /* @var $proxy ClassWithMixedTypedProperties */
652
        $proxy     = $proxyName::staticProxyConstructor(
653
            function ($proxy, $method, $params, & $initializer, array $properties) {
654
                $initializer = null;
655
                $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"] = 'private0';
656
                $properties["\0*\0protectedStringProperty"] = 'protected0';
657
                $properties['publicStringProperty'] = 'public0';
658
            }
659
        );
660
661
        $reflectionClass = new ReflectionClass(ClassWithMixedTypedProperties::class);
662
663
        $properties = Properties::fromReflectionClass($reflectionClass)->getInstanceProperties();
664
665
        $privateProperty   = $properties["\0" . ClassWithMixedTypedProperties::class . "\0privateStringProperty"];
666
        $protectedProperty = $properties["\0*\0protectedStringProperty"];
667
668
        $privateProperty->setAccessible(true);
669
        $protectedProperty->setAccessible(true);
670
671
        self::assertSame('private0', $privateProperty->getValue($proxy));
672
        self::assertSame('protected0', $properties["\0*\0protectedStringProperty"]->getValue($proxy));
673
        self::assertSame('public0', $proxy->publicStringProperty);
674
    }
675
676
    /**
677
     * @group 115
678
     * @group 175
679
     */
680
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
681
    {
682
        $instance = new ClassWithCounterConstructor(10);
683
684
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
685
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
686
        $instance->__construct(3);
687
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
688
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
689
690
        $proxyName = $this->generateProxy(get_class($instance));
691
692
        /** @var ClassWithCounterConstructor $proxy */
693
        $proxy = new $proxyName(15);
694
695
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
696
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
697
        $proxy->__construct(5);
698
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
699
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
700
    }
701
702
    public function testInitializeProxyWillReturnTrueOnSuccessfulInitialization() : void
703
    {
704
        $proxyName = $this->generateProxy(ClassWithMixedTypedProperties::class);
705
706
        /** @var GhostObjectInterface $proxy */
707
        $proxy = $proxyName::staticProxyConstructor($this->createInitializer(
708
            ClassWithMixedTypedProperties::class,
709
            new ClassWithMixedTypedProperties()
710
        ));
711
712
        self::assertTrue($proxy->initializeProxy());
713
        self::assertTrue($proxy->isProxyInitialized());
714
        self::assertFalse($proxy->initializeProxy());
715
    }
716
717
    /**
718
     * Generates a proxy for the given class name, and retrieves its class name
719
     *
720
     * @param mixed[] $proxyOptions
721
     */
722
    private function generateProxy(string $parentClassName, array $proxyOptions = []) : string
723
    {
724
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
725
        $generatedClass     = new ClassGenerator($generatedClassName);
726
727
        (new LazyLoadingGhostGenerator())->generate(
728
            new ReflectionClass($parentClassName),
729
            $generatedClass,
730
            $proxyOptions
731
        );
732
        (new EvaluatingGeneratorStrategy())->generate($generatedClass);
733
734
        return $generatedClassName;
735
    }
736
737
    private function createInitializer(string $className, object $realInstance, ?Mock $initializerMatcher = null
738
    ) : callable
739
    {
740
        /** @var callable|Mock $initializerMatcher */
741
        if (! $initializerMatcher) {
742
            $initializerMatcher = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();
743
744
            $initializerMatcher
745
                ->expects(self::once())
746
                ->method('__invoke')
747
                ->with(self::logicalAnd(
748
                    self::isInstanceOf(GhostObjectInterface::class),
749
                    self::isInstanceOf($className)
750
                ));
751
            ;
752
        }
753
754
        self::assertInternalType('callable', $initializerMatcher);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
755
756
        return function (
757
            GhostObjectInterface $proxy,
758
            $method,
759
            $params,
760
            & $initializer
761
        ) use (
762
            $initializerMatcher,
763
            $realInstance
764
        ) : bool {
765
            $initializer     = null;
766
            $reflectionClass = new ReflectionClass($realInstance);
767
768
            foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
769
                if (! self::isPropertyInitialized($realInstance, $property)) {
770
                    continue;
771
                }
772
773
                $property->setAccessible(true);
774
                $property->setValue($proxy, $property->getValue($realInstance));
775
            }
776
777
            $initializerMatcher($proxy, $method, $params);
778
779
            return true;
780
        };
781
    }
782
783
    /**
784
     * Generates a list of object | invoked method | parameters | expected result
785
     *
786
     * @return null[][]|string[][]|object[][]|mixed[][][]
787
     */
788
    public function getProxyMethods() : array
789
    {
790
        $selfHintParam = new ClassWithSelfHint();
791
        $empty         = new EmptyClass();
792
793
        return [
794
            [
795
                BaseClass::class,
796
                new BaseClass(),
797
                'publicMethod',
798
                [],
799
                'publicMethodDefault',
800
            ],
801
            [
802
                BaseClass::class,
803
                new BaseClass(),
804
                'publicTypeHintedMethod',
805
                [new stdClass()],
806
                'publicTypeHintedMethodDefault',
807
            ],
808
            [
809
                BaseClass::class,
810
                new BaseClass(),
811
                'publicByReferenceMethod',
812
                [],
813
                'publicByReferenceMethodDefault',
814
            ],
815
            [
816
                ClassWithSelfHint::class,
817
                new ClassWithSelfHint(),
818
                'selfHintMethod',
819
                ['parameter' => $selfHintParam],
820
                $selfHintParam,
821
            ],
822
            [
823
                ClassWithParentHint::class,
824
                new ClassWithParentHint(),
825
                'parentHintMethod',
826
                ['parameter' => $empty],
827
                $empty,
828
            ],
829
            [
830
                ClassWithAbstractPublicMethod::class,
831
                new EmptyClass(), // EmptyClass just used to not make reflection explode when synchronizing properties
832
                'publicAbstractMethod',
833
                [],
834
                null,
835
            ],
836
            [
837
                ClassWithMethodWithByRefVariadicFunction::class,
838
                new ClassWithMethodWithByRefVariadicFunction(),
839
                'tuz',
840
                ['Ocramius', 'Malukenho'],
841
                ['Ocramius', 'changed'],
842
            ],
843
        ];
844
    }
845
846
    /**
847
     * Generates a list of object | invoked method | parameters | expected result for methods that cause lazy-loading
848
     * of a ghost object
849
     *
850
     * @return string[][]|object[][]|mixed[][][]|null[][]
851
     */
852
    public function getProxyInitializingMethods() : array
853
    {
854
        return [
855
            [
856
                BaseClass::class,
857
                new BaseClass(),
858
                'publicPropertyGetter',
859
                [],
860
                'publicPropertyDefault',
861
            ],
862
            [
863
                BaseClass::class,
864
                new BaseClass(),
865
                'protectedPropertyGetter',
866
                [],
867
                'protectedPropertyDefault',
868
            ],
869
            [
870
                BaseClass::class,
871
                new BaseClass(),
872
                'privatePropertyGetter',
873
                [],
874
                'privatePropertyDefault',
875
            ],
876
            [
877
                ClassWithMethodWithVariadicFunction::class,
878
                new ClassWithMethodWithVariadicFunction(),
879
                'foo',
880
                ['Ocramius', 'Malukenho'],
881
                null,
882
            ],
883
        ];
884
    }
885
886
    /**
887
     * Generates a list of object | invoked method | parameters | expected result for methods DON'T cause lazy-loading
888
     *
889
     * @return null[][]|string[][]|object[][]|mixed[][][]
890
     */
891
    public function getProxyNonInitializingMethods() : array
892
    {
893
        return $this->getProxyMethods();
894
    }
895
896
    /**
897
     * Generates proxies and instances with a public property to feed to the property accessor methods
898
     *
899
     * @return string[][]|object[][]
900
     */
901
    public function getPropertyAccessProxies() : array
902
    {
903
        $instance1  = new BaseClass();
904
        $proxyName1 = $this->generateProxy(get_class($instance1));
905
        $instance2  = new BaseClass();
906
        $proxyName2 = $this->generateProxy(get_class($instance2));
907
908
        return [
909
            [
910
                $instance1,
911
                new $proxyName1($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...
912
                'publicProperty',
913
                'publicPropertyDefault',
914
            ],
915
            [
916
                $instance2,
917
                unserialize(
918
                    serialize(new $proxyName2($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...
919
                ),
920
                'publicProperty',
921
                'publicPropertyDefault',
922
            ],
923
        ];
924
    }
925
926
    /**
927
     * @dataProvider skipPropertiesFixture
928
     *
929
     * @param mixed   $expected
930
     * @param mixed[] $proxyOptions
931
     */
932
    public function testInitializationIsSkippedForSkippedProperties(
933
        string $className,
934
        string $propertyClass,
935
        string $propertyName,
936
        array $proxyOptions,
937
        $expected
938
    ) : void {
939
        $proxy       = $this->generateProxy($className, $proxyOptions);
940
        $ghostObject = $proxy::staticProxyConstructor(function () use ($propertyName) : void {
941
            self::fail(sprintf('The Property "%s" was not expected to be lazy-loaded', $propertyName));
942
        });
943
944
        $property = new ReflectionProperty($propertyClass, $propertyName);
945
        $property->setAccessible(true);
946
947
        self::assertSame($expected, $property->getValue($ghostObject));
948
    }
949
950
    /**
951
     * @dataProvider skipPropertiesFixture
952
     *
953
     * @param mixed[] $proxyOptions
954
     */
955
    public function testSkippedPropertiesAreNotOverwrittenOnInitialization(
956
        string $className,
957
        string $propertyClass,
958
        string $propertyName,
959
        array $proxyOptions
960
    ) : void {
961
        $proxyName = $this->generateProxy($className, $proxyOptions);
962
        /** @var GhostObjectInterface $ghostObject */
963
        $ghostObject = $proxyName::staticProxyConstructor(
964
            function ($proxy, string $method, $params, & $initializer) : bool {
965
                $initializer = null;
966
967
                return true;
968
            }
969
        );
970
971
        $property = new ReflectionProperty($propertyClass, $propertyName);
972
973
        $property->setAccessible(true);
974
975
        $value = uniqid('', true);
976
977
        $property->setValue($ghostObject, $value);
978
979
        self::assertTrue($ghostObject->initializeProxy());
980
981
        self::assertSame(
982
            $value,
983
            $property->getValue($ghostObject),
984
            'Property should not be changed by proxy initialization'
985
        );
986
    }
987
988
    /**
989
     * @group 265
990
     */
991
    public function testWillForwardVariadicByRefArguments() : void
992
    {
993
        $proxyName = $this->generateProxy(ClassWithMethodWithByRefVariadicFunction::class);
994
        /** @var ClassWithMethodWithByRefVariadicFunction $object */
995
        $object = $proxyName::staticProxyConstructor(function ($proxy, string $method, $params, & $initializer) : bool {
996
            $initializer = null;
997
998
            return true;
999
        });
1000
1001
        $parameters = ['a', 'b', 'c'];
1002
1003
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1004
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
1005
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
1006
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
1007
    }
1008
1009
    /**
1010
     * @group 265
1011
     */
1012
    public function testWillForwardDynamicArguments() : void
1013
    {
1014
        $proxyName = $this->generateProxy(ClassWithDynamicArgumentsMethod::class);
1015
        /** @var ClassWithDynamicArgumentsMethod $object */
1016
        $object = $proxyName::staticProxyConstructor(function () : void {
1017
        });
1018
1019
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
1020
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
1021
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
1022
    }
1023
1024
    /**
1025
     * @return mixed[] in order:
1026
     *                  - the class to be proxied
1027
     *                  - the class owning the property to be checked
1028
     *                  - the property name
1029
     *                  - the options to be passed to the generator
1030
     *                  - the expected value of the property
1031
     */
1032
    public function skipPropertiesFixture() : array
1033
    {
1034
        return [
1035
            [
1036
                ClassWithPublicProperties::class,
1037
                ClassWithPublicProperties::class,
1038
                'property9',
1039
                [
1040
                    'skippedProperties' => ['property9'],
1041
                ],
1042
                'property9',
1043
            ],
1044
            [
1045
                ClassWithProtectedProperties::class,
1046
                ClassWithProtectedProperties::class,
1047
                'property9',
1048
                [
1049
                    'skippedProperties' => ["\0*\0property9"],
1050
                ],
1051
                'property9',
1052
            ],
1053
            [
1054
                ClassWithPrivateProperties::class,
1055
                ClassWithPrivateProperties::class,
1056
                'property9',
1057
                [
1058
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property9"],
1059
                ],
1060
                'property9',
1061
            ],
1062
            [
1063
                ClassWithCollidingPrivateInheritedProperties::class,
1064
                ClassWithCollidingPrivateInheritedProperties::class,
1065
                'property0',
1066
                [
1067
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithCollidingPrivateInheritedProperties\0property0"],
1068
                ],
1069
                'childClassProperty0',
1070
            ],
1071
            [
1072
                ClassWithCollidingPrivateInheritedProperties::class,
1073
                ClassWithPrivateProperties::class,
1074
                'property0',
1075
                [
1076
                    'skippedProperties' => ["\0ProxyManagerTestAsset\\ClassWithPrivateProperties\0property0"],
1077
                ],
1078
                'property0',
1079
            ],
1080
        ];
1081
    }
1082
1083
    /**
1084
     * @group        276
1085
     *
1086
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1087
     *
1088
     */
1089
    public function testWillLazyLoadMembersOfOtherProxiesWithTheSamePrivateScope(
1090
        object $callerObject,
1091
        string $method,
1092
        string $propertyIndex,
1093
        string $expectedValue
1094
    ) : void {
1095
        $proxyName = $this->generateProxy(get_class($callerObject));
1096
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1097
        $proxy = $proxyName::staticProxyConstructor(
1098
            function ($proxy, $method, $params, & $initializer, array $props) use ($propertyIndex, $expectedValue
1099
            ) : void {
1100
                $initializer = null;
1101
1102
                $props[$propertyIndex] = $expectedValue;
1103
            }
1104
        );
1105
1106
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1107
1108
        $accessor = [$callerObject, $method];
1109
1110
        self::assertInternalType('callable', $accessor);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1111
1112
        self::assertFalse($proxy->isProxyInitialized());
1113
        self::assertSame($expectedValue, $accessor($proxy));
1114
        self::assertTrue($proxy->isProxyInitialized());
1115
    }
1116
1117
    /**
1118
     * @group        276
1119
     *
1120
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1121
     *
1122
     */
1123
    public function testWillAccessMembersOfOtherDeSerializedProxiesWithTheSamePrivateScope(
1124
        object $callerObject,
1125
        string $method,
1126
        string $propertyIndex,
1127
        string $expectedValue
1128
    ) : void {
1129
        $proxyName = $this->generateProxy(get_class($callerObject));
1130
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1131
        $proxy = unserialize(serialize($proxyName::staticProxyConstructor(
1132
            function ($proxy, $method, $params, & $initializer, array $props) use ($propertyIndex, $expectedValue
1133
            ) : void {
1134
                $initializer = null;
1135
1136
                $props[$propertyIndex] = $expectedValue;
1137
            }
1138
        )));
1139
1140
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1141
1142
        $accessor = [$callerObject, $method];
1143
1144
        self::assertInternalType('callable', $accessor);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1145
1146
        self::assertTrue($proxy->isProxyInitialized());
1147
        self::assertSame($expectedValue, $accessor($proxy));
1148
    }
1149
1150
    /**
1151
     * @group        276
1152
     *
1153
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
1154
     *
1155
     */
1156
    public function testWillAccessMembersOfOtherClonedProxiesWithTheSamePrivateScope(
1157
        object $callerObject,
1158
        string $method,
1159
        string $propertyIndex,
1160
        string $expectedValue
1161
    ) : void {
1162
        $proxyName = $this->generateProxy(get_class($callerObject));
1163
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1164
        $proxy = clone $proxyName::staticProxyConstructor(
1165
            function ($proxy, $method, $params, & $initializer, array $props) use ($propertyIndex, $expectedValue
1166
            ) : void {
1167
                $initializer = null;
1168
1169
                $props[$propertyIndex] = $expectedValue;
1170
            }
1171
        );
1172
1173
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1174
1175
        $accessor = [$callerObject, $method];
1176
1177
        self::assertInternalType('callable', $accessor);
0 ignored issues
show
Deprecated Code introduced by
The method PHPUnit\Framework\Assert::assertInternalType() has been deprecated with message: https://github.com/sebastianbergmann/phpunit/issues/3369

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1178
1179
        self::assertTrue($proxy->isProxyInitialized());
1180
        self::assertSame($expectedValue, $accessor($proxy));
1181
    }
1182
1183
    /** @return string[][]|object[][] */
1184
    public function getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() : array
1185
    {
1186
        $proxyClass = $this->generateProxy(OtherObjectAccessClass::class);
1187
1188
        return [
1189
            OtherObjectAccessClass::class . '#$privateProperty'                => [
1190
                new OtherObjectAccessClass(),
1191
                'getPrivateProperty',
1192
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1193
                uniqid('', true),
1194
            ],
1195
            OtherObjectAccessClass::class . '#$protectedProperty'              => [
1196
                new OtherObjectAccessClass(),
1197
                'getProtectedProperty',
1198
                "\0*\0protectedProperty",
1199
                uniqid('', true),
1200
            ],
1201
            OtherObjectAccessClass::class . '#$publicProperty'                 => [
1202
                new OtherObjectAccessClass(),
1203
                'getPublicProperty',
1204
                'publicProperty',
1205
                uniqid('', true),
1206
            ],
1207
            '(proxy) ' . OtherObjectAccessClass::class . '#$privateProperty'   => [
1208
                $proxyClass::staticProxyConstructor(function () : void {
1209
                    self::fail('Should never be initialized, as its values aren\'t accessed');
1210
                }),
1211
                'getPrivateProperty',
1212
                "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1213
                uniqid('', true),
1214
            ],
1215
            '(proxy) ' . OtherObjectAccessClass::class . '#$protectedProperty' => [
1216
                $proxyClass::staticProxyConstructor(function () : void {
1217
                    self::fail('Should never be initialized, as its values aren\'t accessed');
1218
                }),
1219
                'getProtectedProperty',
1220
                "\0*\0protectedProperty",
1221
                uniqid('', true),
1222
            ],
1223
            '(proxy) ' . OtherObjectAccessClass::class . '#$publicProperty'    => [
1224
                $proxyClass::staticProxyConstructor(function () : void {
1225
                    self::fail('Should never be initialized, as its values aren\'t accessed');
1226
                }),
1227
                'getPublicProperty',
1228
                'publicProperty',
1229
                uniqid('', true),
1230
            ],
1231
        ];
1232
    }
1233
1234
    /**
1235
     * @group 276
1236
     */
1237
    public function testFriendObjectWillNotCauseLazyLoadingOnSkippedProperty() : void
1238
    {
1239
        $proxyName = $this->generateProxy(
1240
            OtherObjectAccessClass::class,
1241
            [
1242
                'skippedProperties' => [
1243
                    "\0" . OtherObjectAccessClass::class . "\0privateProperty",
1244
                    "\0*\0protectedProperty",
1245
                    'publicProperty',
1246
                ],
1247
            ]
1248
        );
1249
1250
        /** @var OtherObjectAccessClass|LazyLoadingInterface $proxy */
1251
        $proxy = $proxyName::staticProxyConstructor(function () : void {
1252
            throw new \BadMethodCallException('The proxy should never be initialized, as all properties are skipped');
1253
        });
1254
1255
        self::assertInstanceOf(OtherObjectAccessClass::class, $proxy);
1256
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1257
1258
        $privatePropertyValue   = uniqid('', true);
1259
        $protectedPropertyValue = uniqid('', true);
1260
        $publicPropertyValue    = uniqid('', true);
1261
1262
        $reflectionPrivateProperty = new \ReflectionProperty(OtherObjectAccessClass::class, 'privateProperty');
1263
1264
        $reflectionPrivateProperty->setAccessible(true);
1265
        $reflectionPrivateProperty->setValue($proxy, $privatePropertyValue);
1266
1267
        $reflectionProtectedProperty = new \ReflectionProperty(OtherObjectAccessClass::class, 'protectedProperty');
1268
1269
        $reflectionProtectedProperty->setAccessible(true);
1270
        $reflectionProtectedProperty->setValue($proxy, $protectedPropertyValue);
1271
1272
        $proxy->publicProperty = $publicPropertyValue;
1273
1274
        $friendObject = new OtherObjectAccessClass();
1275
1276
        self::assertInstanceOf(OtherObjectAccessClass::class, $proxy);
1277
1278
        if (! ($proxy instanceof OtherObjectAccessClass)) {
1279
            return;
1280
        }
1281
1282
        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...
1283
        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...
1284
        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...
1285
    }
1286
1287
    public function testClonedSkippedPropertiesArePreserved() : void
1288
    {
1289
        $proxyName = $this->generateProxy(
1290
            BaseClass::class,
1291
            [
1292
                'skippedProperties' => [
1293
                    "\0" . BaseClass::class . "\0privateProperty",
1294
                    "\0*\0protectedProperty",
1295
                    'publicProperty',
1296
                ],
1297
            ]
1298
        );
1299
1300
        /** @var BaseClass|GhostObjectInterface $proxy */
1301
        $proxy = $proxyName::staticProxyConstructor(function ($proxy) : void {
1302
            $proxy->setProxyInitializer(null);
1303
        });
1304
1305
        self::assertInstanceOf(BaseClass::class, $proxy);
1306
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1307
1308
        $reflectionPrivate   = new \ReflectionProperty(BaseClass::class, 'privateProperty');
1309
        $reflectionProtected = new \ReflectionProperty(BaseClass::class, 'protectedProperty');
1310
1311
        $reflectionPrivate->setAccessible(true);
1312
        $reflectionProtected->setAccessible(true);
1313
1314
        $privateValue   = uniqid('', true);
1315
        $protectedValue = uniqid('', true);
1316
        $publicValue    = uniqid('', true);
1317
1318
        $reflectionPrivate->setValue($proxy, $privateValue);
1319
        $reflectionProtected->setValue($proxy, $protectedValue);
1320
        $proxy->publicProperty = $publicValue;
1321
1322
        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...
1323
1324
        $clone = clone $proxy;
1325
1326
        self::assertFalse($proxy->isProxyInitialized());
1327
        self::assertTrue($clone->isProxyInitialized());
1328
1329
        self::assertSame($privateValue, $reflectionPrivate->getValue($proxy));
1330
        self::assertSame($privateValue, $reflectionPrivate->getValue($clone));
1331
        self::assertSame($protectedValue, $reflectionProtected->getValue($proxy));
1332
        self::assertSame($protectedValue, $reflectionProtected->getValue($clone));
1333
        self::assertSame($publicValue, $proxy->publicProperty);
1334
        self::assertSame($publicValue, $clone->publicProperty);
1335
    }
1336
1337
    /**
1338
     * @group 327
1339
     */
1340
    public function testWillExecuteLogicInAVoidMethod() : void
1341
    {
1342
        $proxyName = $this->generateProxy(VoidCounter::class);
1343
1344
        $initialCounter = random_int(10, 1000);
1345
1346
        /** @var VoidCounter|LazyLoadingInterface $proxy */
1347
        $proxy = $proxyName::staticProxyConstructor(
1348
            function (VoidCounter $proxy, $method, $params, & $initializer, array $props) use ($initialCounter) : bool {
1349
                $initializer = null;
1350
1351
                $props['counter'] = $initialCounter;
1352
1353
                return true;
1354
            }
1355
        );
1356
1357
        self::assertInstanceOf(VoidCounter::class, $proxy);
1358
        self::assertInstanceOf(LazyLoadingInterface::class, $proxy);
1359
1360
        $increment = random_int(1001, 10000);
1361
1362
        $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...
1363
1364
        self::assertSame($initialCounter + $increment, $proxy->counter);
1365
    }
1366
1367
    private static function isPropertyInitialized(object $object, ReflectionProperty $property) : bool
1368
    {
1369
        return array_key_exists(
1370
            ($property->isProtected() ? "\0*\0" : '')
1371
            . ($property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : '')
1372
            . $property->getName(),
1373
            (array) $object
1374
        );
1375
    }
1376
}
1377