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

makeProxy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
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 Generator;
8
use PHPUnit\Framework\ExpectationFailedException;
9
use PHPUnit\Framework\MockObject\MockObject;
10
use PHPUnit\Framework\TestCase;
11
use ProxyManager\Factory\AccessInterceptorValueHolderFactory;
12
use ProxyManager\Generator\ClassGenerator;
13
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
14
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
15
use ProxyManager\Proxy\AccessInterceptorInterface;
16
use ProxyManager\Proxy\ValueHolderInterface;
17
use ProxyManager\Proxy\AccessInterceptorValueHolderInterface;
18
use ProxyManager\ProxyGenerator\AccessInterceptorValueHolderGenerator;
19
use ProxyManagerTest\Assert;
20
use ProxyManagerTestAsset\BaseClass;
21
use ProxyManagerTestAsset\BaseInterface;
22
use ProxyManagerTestAsset\CallableInterface;
23
use ProxyManagerTestAsset\ClassWithCounterConstructor;
24
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
25
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
26
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
27
use ProxyManagerTestAsset\ClassWithParentHint;
28
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
29
use ProxyManagerTestAsset\ClassWithPublicArrayPropertyAccessibleViaMethod;
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 stdClass;
37
use function array_values;
38
use function get_class;
39
use function random_int;
40
use function serialize;
41
use function ucfirst;
42
use function uniqid;
43
use function unserialize;
44
45
/**
46
 * Tests for {@see \ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator} produced objects
47
 *
48
 * @group Functional
49
 * @coversNothing
50
 */
51
final class AccessInterceptorValueHolderFunctionalTest extends TestCase
52
{
53
    /**
54
     * @param mixed[] $params
55
     * @param mixed   $expectedValue
56
     *
57
     * @dataProvider getProxyMethods
58
     *
59
     * @psalm-template OriginalClass
60
     * @psalm-param class-string<OriginalClass> $className
61
     * @psalm-param OriginalClass $instance
62
     */
63
    public function testMethodCalls(string $className, object $instance, string $method, array $params, $expectedValue) : void
64
    {
65
        $proxy    = $this->makeProxy($className, $instance);
66
        $callback = [$proxy, $method];
67
68
        self::assertIsCallable($callback);
69
        self::assertSame($instance, $proxy->getWrappedValueHolderValue());
70
        self::assertSame($expectedValue, $callback(...array_values($params)));
71
72
        $listener = $this->createMock(CallableInterface::class);
73
        $listener
74
            ->expects(self::once())
75
            ->method('__invoke')
76
            ->with($proxy, $instance, $method, $params, false);
77
78
        $proxy->setMethodPrefixInterceptor(
79
            $method,
80
            static function (
81
                AccessInterceptorInterface $proxy,
82
                object $instance,
83
                string $method,
84
                array $params,
85
                bool & $returnEarly
86
            ) use ($listener) : void {
87
                $listener->__invoke($proxy, $instance, $method, $params, $returnEarly);
0 ignored issues
show
Bug introduced by
The method __invoke() does not seem to exist on object<PHPUnit\Framework\MockObject\MockObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
88
            }
89
        );
90
91
        self::assertSame($expectedValue, $callback(...array_values($params)));
92
93
        $random = uniqid('', true);
94
95
        $proxy->setMethodPrefixInterceptor(
96
            $method,
97
            static function (
98
                AccessInterceptorInterface $proxy,
99
                object $instance,
100
                string $method,
101
                array $params,
102
                bool & $returnEarly
103
            ) use ($random) : string {
104
                $returnEarly = true;
105
106
                return $random;
107
            }
108
        );
109
110
        self::assertSame($random, $callback(...array_values($params)));
111
    }
112
113
    /**
114
     * @param mixed[] $params
115
     * @param mixed   $expectedValue
116
     *
117
     * @dataProvider getProxyMethods
118
     *
119
     * @psalm-template OriginalClass
120
     * @psalm-param class-string<OriginalClass> $className
121
     * @psalm-param OriginalClass $instance
122
     */
123
    public function testMethodCallsWithSuffixListener(
124
        string $className,
125
        object $instance,
126
        string $method,
127
        array $params,
128
        $expectedValue
129
    ) : void {
130
        $proxy    = $this->makeProxy($className, $instance);
131
        $callback = [$proxy, $method];
132
133
        self::assertIsCallable($callback);
134
135
        $listener = $this->createMock(CallableInterface::class);
136
        $listener
137
            ->expects(self::once())
138
            ->method('__invoke')
139
            ->with($proxy, $instance, $method, $params, $expectedValue, false);
140
141
        $proxy->setMethodSuffixInterceptor(
142
            $method,
143
            /** @param mixed $returnValue */
144
            static function (
145
                AccessInterceptorInterface $proxy,
146
                object $instance,
147
                string $method,
148
                array $params,
149
                $returnValue,
150
                bool & $returnEarly
151
            ) use ($listener) : void {
152
                $listener->__invoke($proxy, $instance, $method, $params, $returnValue, $returnEarly);
0 ignored issues
show
Bug introduced by
The method __invoke() does not seem to exist on object<PHPUnit\Framework\MockObject\MockObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
153
            }
154
        );
155
156
        self::assertSame($expectedValue, $callback(...array_values($params)));
157
158
        $random = uniqid('', true);
159
160
        $proxy->setMethodSuffixInterceptor(
161
            $method,
162
            /** @param mixed $returnValue */
163
            static function (
164
                AccessInterceptorInterface $proxy,
165
                object $instance,
166
                string $method,
167
                array $params,
168
                $returnValue,
169
                bool & $returnEarly
170
            ) use ($random) : string {
171
                $returnEarly = true;
172
173
                return $random;
174
            }
175
        );
176
177
        self::assertSame($random, $callback(...array_values($params)));
178
    }
179
180
    /**
181
     * @param mixed[] $params
182
     * @param mixed   $expectedValue
183
     *
184
     * @dataProvider getProxyMethods
185
     *
186
     * @psalm-template OriginalClass
187
     * @psalm-param class-string<OriginalClass> $className
188
     * @psalm-param OriginalClass $instance
189
     */
190
    public function testMethodCallsAfterUnSerialization(
191
        string $className,
192
        object $instance,
193
        string $method,
194
        array $params,
195
        $expectedValue
196
    ) : void {
197
        /** @var AccessInterceptorValueHolderInterface $proxy */
198
        $proxy    = unserialize(serialize($this->makeProxy($className, $instance)));
199
        $callback = [$proxy, $method];
200
201
        self::assertIsCallable($callback);
202
        self::assertSame($expectedValue, $callback(...array_values($params)));
203
        self::assertEquals($instance, $proxy->getWrappedValueHolderValue());
204
    }
205
206
    /**
207
     * @param mixed[] $params
208
     * @param mixed   $expectedValue
209
     *
210
     * @dataProvider getProxyMethods
211
     *
212
     * @psalm-template OriginalClass
213
     * @psalm-param class-string<OriginalClass> $className
214
     * @psalm-param OriginalClass $instance
215
     */
216
    public function testMethodCallsAfterCloning(
217
        string $className,
218
        object $instance,
219
        string $method,
220
        array $params,
221
        $expectedValue
222
    ) : void {
223
        $proxy    = $this->makeProxy($className, $instance);
224
        $cloned   = clone $proxy;
225
        $callback = [$cloned, $method];
226
227
        self::assertIsCallable($callback);
228
        self::assertNotSame($proxy->getWrappedValueHolderValue(), $cloned->getWrappedValueHolderValue());
229
        self::assertSame($expectedValue, $callback(...array_values($params)));
230
        self::assertEquals($instance, $cloned->getWrappedValueHolderValue());
231
    }
232
233
    /**
234
     * @param mixed $propertyValue
235
     *
236
     * @dataProvider getPropertyAccessProxies
237
     */
238
    public function testPropertyReadAccess(
239
        object $instance,
240
        AccessInterceptorValueHolderInterface $proxy,
241
        string $publicProperty,
242
        $propertyValue
243
    ) : void {
244
        self::assertSame($propertyValue, $proxy->$publicProperty);
245
        self::assertEquals($instance, $proxy->getWrappedValueHolderValue());
246
    }
247
248
    /**
249
     * @dataProvider getPropertyAccessProxies
250
     */
251
    public function testPropertyWriteAccess(
252
        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...
253
        AccessInterceptorValueHolderInterface $proxy,
254
        string $publicProperty
255
    ) : void {
256
        $newValue               = uniqid('', true);
257
        $proxy->$publicProperty = $newValue;
258
259
        self::assertSame($newValue, $proxy->$publicProperty);
260
261
        $wrappedValue = $proxy->getWrappedValueHolderValue();
262
263
        self::assertNotNull($wrappedValue);
264
        self::assertSame($newValue, $wrappedValue->$publicProperty);
265
    }
266
267
    /**
268
     * @dataProvider getPropertyAccessProxies
269
     */
270
    public function testPropertyExistence(
271
        object $instance,
272
        AccessInterceptorValueHolderInterface $proxy,
273
        string $publicProperty
274
    ) : void {
275
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
276
        self::assertEquals($instance, $proxy->getWrappedValueHolderValue());
277
278
        $proxy->getWrappedValueHolderValue()->$publicProperty = null;
279
        self::assertFalse(isset($proxy->$publicProperty));
280
    }
281
282
    /**
283
     * @dataProvider getPropertyAccessProxies
284
     */
285
    public function testPropertyUnset(
286
        object $instance,
287
        AccessInterceptorValueHolderInterface $proxy,
288
        string $publicProperty
289
    ) : void {
290
        $instance = $proxy->getWrappedValueHolderValue() ?: $instance;
291
        unset($proxy->$publicProperty);
292
293
        self::assertFalse(isset($instance->$publicProperty));
294
        self::assertFalse(isset($proxy->$publicProperty));
295
    }
296
297
    /**
298
     * Verifies that accessing a public property containing an array behaves like in a normal context
299
     */
300
    public function testCanWriteToArrayKeysInPublicProperty() : void
301
    {
302
        $instance  = new ClassWithPublicArrayPropertyAccessibleViaMethod();
303
        $proxy     = $this->makeProxy(ClassWithPublicArrayPropertyAccessibleViaMethod::class, $instance);
0 ignored issues
show
Documentation introduced by
$instance is of type object<ProxyManagerTestA...rtyAccessibleViaMethod>, 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...
304
305
        $proxy->arrayProperty['foo'] = 'bar';
0 ignored issues
show
Bug introduced by
Accessing arrayProperty on the interface ProxyManager\Proxy\Acces...torValueHolderInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
306
307
        self::assertSame('bar', $proxy->getArrayProperty()['foo']);
0 ignored issues
show
Bug introduced by
The method getArrayProperty() does not seem to exist on object<ProxyManager\Prox...orValueHolderInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
308
309
        $proxy->arrayProperty = ['tab' => 'taz'];
0 ignored issues
show
Bug introduced by
Accessing arrayProperty on the interface ProxyManager\Proxy\Acces...torValueHolderInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
310
311
        self::assertSame(['tab' => 'taz'], $proxy->getArrayProperty());
0 ignored issues
show
Bug introduced by
The method getArrayProperty() does not seem to exist on object<ProxyManager\Prox...orValueHolderInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
312
    }
313
314
    /**
315
     * Verifies that public properties retrieved via `__get` don't get modified in the object state
316
     */
317
    public function testWillNotModifyRetrievedPublicProperties() : void
318
    {
319
        $instance  = new ClassWithPublicProperties();
320
        $proxy    = $this->makeProxy(ClassWithPublicProperties::class, $instance);
0 ignored issues
show
Documentation introduced by
$instance is of type object<ProxyManagerTestA...ssWithPublicProperties>, but the function expects a object<ProxyManagerTest\Functional\object>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
321
        $variable = $proxy->property0;
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\Acces...torValueHolderInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
322
323
        self::assertByRefVariableValueSame('property0', $variable);
324
325
        $variable = 'foo';
326
327
        self::assertByRefVariableValueSame('property0', $proxy->property0);
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\Acces...torValueHolderInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
328
        self::assertByRefVariableValueSame('foo', $variable);
329
    }
330
331
    /**
332
     * Verifies that public properties references retrieved via `__get` modify in the object state
333
     */
334
    public function testWillModifyByRefRetrievedPublicProperties() : void
335
    {
336
        $instance  = new ClassWithPublicProperties();
337
        $proxy    = $this->makeProxy(ClassWithPublicProperties::class, $instance);
0 ignored issues
show
Documentation introduced by
$instance is of type object<ProxyManagerTestA...ssWithPublicProperties>, but the function expects a object<ProxyManagerTest\Functional\object>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
338
        $variable = &$proxy->property0;
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\Acces...torValueHolderInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
339
340
        self::assertByRefVariableValueSame('property0', $variable);
341
342
        $variable = 'foo';
343
344
        self::assertByRefVariableValueSame('foo', $proxy->property0);
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\Acces...torValueHolderInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
345
        self::assertByRefVariableValueSame('foo', $variable);
346
    }
347
348
    /**
349
     * @group 115
350
     * @group 175
351
     */
352
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
353
    {
354
        $instance = new ClassWithCounterConstructor(10);
355
356
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
357
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
358
        $instance->__construct(3);
359
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
360
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
361
362
        $proxyName = $this->generateProxy(ClassWithCounterConstructor::class);
363
364
        $proxy = new $proxyName(15);
365
366
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
367
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
368
        $proxy->__construct(5);
369
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
370
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
371
    }
372
373
    public function testWillForwardVariadicArguments() : void
374
    {
375
        $factory      = new AccessInterceptorValueHolderFactory();
376
        $targetObject = new ClassWithMethodWithVariadicFunction();
377
378
        $object = $factory->createProxy(
379
            $targetObject,
380
            [
381
                'bar' => static function () : string {
382
                    return 'Foo Baz';
383
                },
384
            ]
385
        );
386
387
        self::assertNull($object->bar);
388
        self::assertNull($object->baz);
389
390
        $object->foo('Ocramius', 'Malukenho', 'Danizord');
391
        self::assertSame('Ocramius', $object->bar);
392
        self::assertSame(['Malukenho', 'Danizord'], Assert::readAttribute($object, 'baz'));
393
    }
394
395
    /**
396
     * @group 265
397
     */
398
    public function testWillForwardVariadicByRefArguments() : void
399
    {
400
        $object = (new AccessInterceptorValueHolderFactory())->createProxy(
401
            new ClassWithMethodWithByRefVariadicFunction(),
402
            [
403
                'bar' => static function () : string {
404
                    return 'Foo Baz';
405
                },
406
            ]
407
        );
408
409
        $arguments = ['Ocramius', 'Malukenho', 'Danizord'];
410
411
        self::assertSame(
412
            ['Ocramius', 'changed', 'Danizord'],
413
            (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$arguments),
414
            'Verifying that the implementation of the test asset is correct before proceeding'
415
        );
416
        self::assertSame(['Ocramius', 'changed', 'Danizord'], $object->tuz(...$arguments));
417
        self::assertSame(['Ocramius', 'changed', 'Danizord'], $arguments, 'By-ref arguments were changed');
418
    }
419
420
    /**
421
     * This test documents a known limitation: `func_get_args()` (and similars) don't work in proxied APIs.
422
     * If you manage to make this test pass, then please do send a patch
423
     *
424
     * @group 265
425
     */
426
    public function testWillNotForwardDynamicArguments() : void
427
    {
428
        $object = $this->makeProxy(ClassWithDynamicArgumentsMethod::class, new ClassWithDynamicArgumentsMethod());
0 ignored issues
show
Documentation introduced by
new \ProxyManagerTestAss...ynamicArgumentsMethod() is of type object<ProxyManagerTestA...DynamicArgumentsMethod>, 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...
429
430
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
431
432
        $this->expectException(ExpectationFailedException::class);
433
434
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
0 ignored issues
show
Bug introduced by
The method dynamicArgumentsMethod() does not seem to exist on object<ProxyManager\Prox...orValueHolderInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
435
    }
436
437
    /**
438
     * Generates a proxy for the given class name, and retrieves its class name
439
     *
440
     * @psalm-template OriginalClass
441
     * @psalm-param class-string<OriginalClass> $parentClassName
442
     * @psalm-return class-string<OriginalClass>
443
     * @psalm-suppress MoreSpecificReturnType
444
     */
445
    private function generateProxy(string $parentClassName) : string
446
    {
447
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
448
        $generator          = new AccessInterceptorValueHolderGenerator();
449
        $generatedClass     = new ClassGenerator($generatedClassName);
450
        $strategy           = new EvaluatingGeneratorStrategy();
451
452
        $generator->generate(new ReflectionClass($parentClassName), $generatedClass);
453
        $strategy->generate($generatedClass);
454
455
        /**
456
         * @psalm-suppress LessSpecificReturnStatement
457
         */
458
        return $generatedClassName;
459
    }
460
461
    /**
462
     * Generates a list of object | invoked method | parameters | expected result
463
     *
464
     * @return string[][]|object[][]|mixed[][]
465
     */
466
    public function getProxyMethods() : array
467
    {
468
        $selfHintParam = new ClassWithSelfHint();
469
        $empty         = new EmptyClass();
470
471
        return [
472
            [
473
                BaseClass::class,
474
                new BaseClass(),
475
                'publicMethod',
476
                [],
477
                'publicMethodDefault',
478
            ],
479
            [
480
                BaseClass::class,
481
                new BaseClass(),
482
                'publicTypeHintedMethod',
483
                ['param' => new stdClass()],
484
                'publicTypeHintedMethodDefault',
485
            ],
486
            [
487
                BaseClass::class,
488
                new BaseClass(),
489
                'publicByReferenceMethod',
490
                [],
491
                'publicByReferenceMethodDefault',
492
            ],
493
            [
494
                BaseInterface::class,
495
                new BaseClass(),
496
                'publicMethod',
497
                [],
498
                'publicMethodDefault',
499
            ],
500
            [
501
                ClassWithSelfHint::class,
502
                new ClassWithSelfHint(),
503
                'selfHintMethod',
504
                ['parameter' => $selfHintParam],
505
                $selfHintParam,
506
            ],
507
            [
508
                ClassWithParentHint::class,
509
                new ClassWithParentHint(),
510
                'parentHintMethod',
511
                ['parameter' => $empty],
512
                $empty,
513
            ],
514
        ];
515
    }
516
517
    /**
518
     * Generates proxies and instances with a public property to feed to the property accessor methods
519
     *
520
     * @return array<int, array<int, object|AccessInterceptorValueHolderInterface|string>>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (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...
521
     */
522
    public function getPropertyAccessProxies() : array
523
    {
524
        $instance1  = new BaseClass();
525
        $instance2  = new BaseClass();
526
        /** @var AccessInterceptorValueHolderInterface $serialized */
527
        $serialized = unserialize(serialize($this->makeProxy(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...
528
529
        return [
530
            [
531
                $instance1,
532
                $this->makeProxy(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...
533
                'publicProperty',
534
                'publicPropertyDefault',
535
            ],
536
            [
537
                $instance2,
538
                $serialized,
539
                'publicProperty',
540
                'publicPropertyDefault',
541
            ],
542
        ];
543
    }
544
545
    /**
546
     * @group        276
547
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
548
     */
549
    public function testWillInterceptAccessToPropertiesViaFriendClassAccess(
550
        object $callerObject,
551
        object $realInstance,
552
        string $method,
553
        string $expectedValue,
554
        string $propertyName
555
    ) : void {
556
        $proxy    = $this->makeProxy(get_class($realInstance), $realInstance);
557
        $listener = $this->createMock(CallableInterface::class);
558
559
        $listener
560
            ->expects(self::once())
561
            ->method('__invoke')
562
            ->with($proxy, $realInstance, '__get', ['name' => $propertyName]);
563
564
        $proxy->setMethodPrefixInterceptor(
565
            '__get',
566
            static function ($proxy, $instance, $method, $params, & $returnEarly) use ($listener) : void {
567
                $listener->__invoke($proxy, $instance, $method, $params, $returnEarly);
0 ignored issues
show
Bug introduced by
The method __invoke() does not seem to exist on object<PHPUnit\Framework\MockObject\MockObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
568
            }
569
        );
570
571
        /** @var callable $accessor */
572
        $accessor = [$callerObject, $method];
573
574
        self::assertSame($expectedValue, $accessor($proxy));
575
    }
576
577
    /**
578
     * @group        276
579
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
580
     */
581
    public function testWillInterceptAccessToPropertiesViaFriendClassAccessEvenIfDeSerialized(
582
        object $callerObject,
583
        object $realInstance,
584
        string $method,
585
        string $expectedValue,
586
        string $propertyName
587
    ) : void {
588
        /** @var AccessInterceptorValueHolderInterface $proxy */
589
        $proxy    = unserialize(serialize($this->makeProxy(get_class($realInstance), $realInstance)));
590
        $listener = $this->createMock(CallableInterface::class);
591
592
        $listener
593
            ->expects(self::once())
594
            ->method('__invoke')
595
            ->with($proxy, $realInstance, '__get', ['name' => $propertyName]);
596
597
        $proxy->setMethodPrefixInterceptor(
598
            '__get',
599
            static function ($proxy, $instance, $method, $params, & $returnEarly) use ($listener) : void {
600
                $listener->__invoke($proxy, $instance, $method, $params, $returnEarly);
0 ignored issues
show
Bug introduced by
The method __invoke() does not seem to exist on object<PHPUnit\Framework\MockObject\MockObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
601
            }
602
        );
603
604
        /** @var callable $accessor */
605
        $accessor = [$callerObject, $method];
606
607
        self::assertSame($expectedValue, $accessor($proxy));
608
    }
609
610
    /**
611
     * @group        276
612
     * @dataProvider getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope
613
     */
614
    public function testWillInterceptAccessToPropertiesViaFriendClassAccessEvenIfCloned(
615
        object $callerObject,
616
        object $realInstance,
617
        string $method,
618
        string $expectedValue,
619
        string $propertyName
620
    ) : void {
621
        $proxy = clone $this->makeProxy(get_class($realInstance), $realInstance);
622
623
        $listener = $this->createMock(CallableInterface::class);
624
625
        $listener
626
            ->expects(self::once())
627
            ->method('__invoke')
628
            ->with($proxy, $realInstance, '__get', ['name' => $propertyName]);
629
630
        $proxy->setMethodPrefixInterceptor(
631
            '__get',
632
            static function ($proxy, $instance, $method, $params, & $returnEarly) use ($listener) : void {
633
                $listener->__invoke($proxy, $instance, $method, $params, $returnEarly);
0 ignored issues
show
Bug introduced by
The method __invoke() does not seem to exist on object<PHPUnit\Framework\MockObject\MockObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
634
            }
635
        );
636
637
        /** @var callable $accessor */
638
        $accessor = [$callerObject, $method];
639
640
        self::assertSame($expectedValue, $accessor($proxy));
641
    }
642
643
    public function getMethodsThatAccessPropertiesOnOtherObjectsInTheSameScope() : Generator
644
    {
645
        foreach ((new ReflectionClass(OtherObjectAccessClass::class))->getProperties() as $property) {
646
            $property->setAccessible(true);
647
648
            $propertyName  = $property->getName();
649
            $realInstance  = new OtherObjectAccessClass();
650
            $expectedValue = uniqid('', true);
651
652
            $property->setValue($realInstance, $expectedValue);
653
654
            // callee is an actual object
655
            yield OtherObjectAccessClass::class . '#$' . $propertyName => [
656
                new OtherObjectAccessClass(),
657
                $realInstance,
658
                'get' . ucfirst($propertyName),
659
                $expectedValue,
660
                $propertyName,
661
            ];
662
663
            $realInstance  = new OtherObjectAccessClass();
664
            $expectedValue = uniqid('', true);
665
666
            $property->setValue($realInstance, $expectedValue);
667
668
            // callee is a proxy (not to be lazy-loaded!)
669
            yield '(proxy) ' . OtherObjectAccessClass::class . '#$' . $propertyName => [
670
                $this->makeProxy(OtherObjectAccessClass::class, new OtherObjectAccessClass()),
0 ignored issues
show
Documentation introduced by
new \ProxyManagerTestAss...therObjectAccessClass() is of type object<ProxyManagerTestA...OtherObjectAccessClass>, 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...
671
                $realInstance,
672
                'get' . ucfirst($propertyName),
673
                $expectedValue,
674
                $propertyName,
675
            ];
676
        }
677
    }
678
679
    /**
680
     * @group 327
681
     */
682
    public function testWillInterceptAndReturnEarlyOnVoidMethod() : void
683
    {
684
        $skip      = random_int(100, 200);
685
        $addMore   = random_int(201, 300);
686
        $increment = random_int(301, 400);
687
688
        $object = (new AccessInterceptorValueHolderFactory())->createProxy(
689
            new VoidCounter(),
690
            [
691
                'increment' => static function (
692
                    AccessInterceptorInterface $proxy,
693
                    VoidCounter $instance,
694
                    string $method,
695
                    array $params,
696
                    ?bool & $returnEarly
697
                ) use ($skip) : void {
698
                    if ($skip !== $params['amount']) {
699
                        return;
700
                    }
701
702
                    $returnEarly = true;
703
                },
704
            ],
705
            [
706
                'increment' => static function (
707
                    AccessInterceptorInterface $proxy,
708
                    VoidCounter $instance,
709
                    string $method,
710
                    array $params,
711
                    ?bool & $returnEarly
0 ignored issues
show
Unused Code introduced by
The parameter $returnEarly 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...
712
                ) use ($addMore) : void {
713
                    if ($addMore !== $params['amount']) {
714
                        return;
715
                    }
716
717
                    /** @noinspection IncrementDecrementOperationEquivalentInspection */
718
                    $instance->counter += 1;
719
                },
720
            ]
721
        );
722
723
        $object->increment($skip);
724
        self::assertSame(0, $object->counter);
725
726
        $object->increment($increment);
727
        self::assertSame($increment, $object->counter);
728
729
        $object->increment($addMore);
730
        self::assertSame($increment + $addMore + 1, $object->counter);
731
    }
732
733
    /**
734
     * @psalm-template OriginalClass
735
     * @psalm-param class-string<OriginalClass> $originalClassName
736
     * @psalm-param OriginalClass $realInstance
737
     * @psalm-return AccessInterceptorValueHolderInterface<OriginalClass>&ValueHolderInterface<OriginalClass>&OriginalClass
738
     *
739
     * @psalm-suppress MixedInferredReturnType
740
     * @psalm-suppress MoreSpecificReturnType
741
     */
742
    private function makeProxy(string $originalClassName, object $realInstance) : AccessInterceptorValueHolderInterface
743
    {
744
        $proxyClassName = $this->generateProxy($originalClassName);
745
746
        /**
747
         * @psalm-suppress MixedMethodCall
748
         * @psalm-suppress MixedReturnStatement
749
         */
750
        return $proxyClassName::staticProxyConstructor($realInstance);
751
    }
752
753
    /**
754
     * @param mixed $expected
755
     * @param mixed $actual
756
     */
757
    private static function assertByRefVariableValueSame($expected, & $actual) : void
758
    {
759
        self::assertSame($expected, $actual);
760
    }
761
}
762