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

  A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ProxyManagerTest\Functional;
6
7
use PHPUnit\Framework\ExpectationFailedException;
8
use PHPUnit\Framework\TestCase;
9
use ProxyManager\Configuration;
10
use ProxyManager\Factory\AccessInterceptorScopeLocalizerFactory;
11
use ProxyManager\Proxy\AccessInterceptorInterface;
12
use ProxyManager\ProxyGenerator\Util\Properties;
13
use ProxyManagerTest\Assert;
14
use ProxyManagerTestAsset\BaseClass;
15
use ProxyManagerTestAsset\CallableInterface;
16
use ProxyManagerTestAsset\ClassWithCounterConstructor;
17
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
18
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
19
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
20
use ProxyManagerTestAsset\ClassWithParentHint;
21
use ProxyManagerTestAsset\ClassWithPublicArrayPropertyAccessibleViaMethod;
22
use ProxyManagerTestAsset\ClassWithPublicProperties;
23
use ProxyManagerTestAsset\ClassWithSelfHint;
24
use ProxyManagerTestAsset\EmptyClass;
25
use ProxyManagerTestAsset\VoidCounter;
26
use ReflectionClass;
27
use stdClass;
28
use function array_values;
29
use function random_int;
30
use function serialize;
31
use function uniqid;
32
use function unserialize;
33
34
/**
35
 * Tests for {@see \ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator} produced objects
36
 *
37
 * @group Functional
38
 * @coversNothing
39
 */
40
final class AccessInterceptorScopeLocalizerFunctionalTest extends TestCase
41
{
42
    /**
43
     * @param mixed[] $params
44
     * @param mixed   $expectedValue
45
     *
46
     * @dataProvider getProxyMethods
47
     */
48
    public function testMethodCalls(object $instance, string $method, array $params, $expectedValue) : void
49
    {
50
        $proxy = (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance);
51
52
        $this->assertProxySynchronized($instance, $proxy);
53
54
        $callback = [$proxy, $method];
55
56
        self::assertIsCallable($callback);
57
        self::assertSame($expectedValue, $callback(...array_values($params)));
58
59
        $listener = $this->createMock(CallableInterface::class);
60
        $listener
61
            ->expects(self::once())
62
            ->method('__invoke')
63
            ->with($proxy, $proxy, $method, $params, false);
64
65
        $proxy->setMethodPrefixInterceptor(
66
            $method,
67
            static function (
68
                AccessInterceptorInterface $proxy,
69
                object $instance,
70
                string $method,
71
                array $params,
72
                bool & $returnEarly
73
            ) use ($listener) : void {
74
                $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...
75
            }
76
        );
77
78
        self::assertSame($expectedValue, $callback(...array_values($params)));
79
80
        $random = uniqid('', true);
81
82
        $proxy->setMethodPrefixInterceptor(
83
            $method,
84
            static function (
85
                AccessInterceptorInterface $proxy,
86
                object $instance,
87
                string $method,
88
                array $params,
89
                bool & $returnEarly
90
            ) use ($random) : string {
91
                $returnEarly = true;
92
93
                return $random;
94
            }
95
        );
96
97
        self::assertSame($random, $callback(...array_values($params)));
98
99
        $this->assertProxySynchronized($instance, $proxy);
100
    }
101
102
    /**
103
     * @param mixed[] $params
104
     * @param mixed   $expectedValue
105
     *
106
     * @dataProvider getProxyMethods
107
     */
108
    public function testMethodCallsWithSuffixListener(
109
        object $instance,
110
        string $method,
111
        array $params,
112
        $expectedValue
113
    ) : void {
114
        $proxy    = (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance);
115
        $callback = [$proxy, $method];
116
117
        self::assertIsCallable($callback);
118
119
        $listener = $this->createMock(CallableInterface::class);
120
        $listener
121
            ->expects(self::once())
122
            ->method('__invoke')
123
            ->with($proxy, $proxy, $method, $params, $expectedValue, false);
124
125
        $proxy->setMethodSuffixInterceptor(
126
            $method,
127
            /** @param mixed $returnValue */
128
            static function (
129
                AccessInterceptorInterface $proxy,
130
                object $instance,
131
                string $method,
132
                array $params,
133
                $returnValue,
134
                bool & $returnEarly
135
            ) use ($listener) : void {
136
                $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...
137
            }
138
        );
139
140
        self::assertSame($expectedValue, $callback(...array_values($params)));
141
142
        $random = uniqid('', true);
143
144
        $proxy->setMethodSuffixInterceptor(
145
            $method,
146
            /** @param mixed $returnValue */
147
            static function (
148
                AccessInterceptorInterface $proxy,
149
                object $instance,
150
                string $method,
151
                array $params,
152
                $returnValue,
153
                bool & $returnEarly
154
            ) use ($random) : string {
155
                $returnEarly = true;
156
157
                return $random;
158
            }
159
        );
160
161
        self::assertSame($random, $callback(...array_values($params)));
162
163
        $this->assertProxySynchronized($instance, $proxy);
164
    }
165
166
    /**
167
     * @param mixed[] $params
168
     * @param mixed   $expectedValue
169
     *
170
     * @dataProvider getProxyMethods
171
     */
172
    public function testMethodCallsAfterUnSerialization(
173
        object $instance,
174
        string $method,
175
        array $params,
176
        $expectedValue
177
    ) : void {
178
        /** @var AccessInterceptorInterface $proxy */
179
        $proxy = unserialize(serialize((new AccessInterceptorScopeLocalizerFactory())->createProxy($instance)));
180
181
        $callback = [$proxy, $method];
182
183
        self::assertIsCallable($callback);
184
        self::assertSame($expectedValue, $callback(...array_values($params)));
185
        $this->assertProxySynchronized($instance, $proxy);
186
    }
187
188
    /**
189
     * @param mixed[] $params
190
     * @param mixed   $expectedValue
191
     *
192
     * @dataProvider getProxyMethods
193
     */
194
    public function testMethodCallsAfterCloning(
195
        object $instance,
196
        string $method,
197
        array $params,
198
        $expectedValue
199
    ) : void {
200
        $proxy    = (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance);
201
        $cloned   = clone $proxy;
202
        $callback = [$cloned, $method];
203
204
        $this->assertProxySynchronized($instance, $proxy);
205
        self::assertIsCallable($callback);
206
        self::assertSame($expectedValue, $callback(...array_values($params)));
207
        $this->assertProxySynchronized($instance, $proxy);
208
    }
209
210
    /**
211
     * @param mixed $propertyValue
212
     *
213
     * @dataProvider getPropertyAccessProxies
214
     */
215
    public function testPropertyReadAccess(
216
        object $instance,
217
        AccessInterceptorInterface $proxy,
218
        string $publicProperty,
219
        $propertyValue
220
    ) : void {
221
        self::assertSame($propertyValue, $proxy->$publicProperty);
222
        $this->assertProxySynchronized($instance, $proxy);
223
    }
224
225
    /**
226
     * @dataProvider getPropertyAccessProxies
227
     */
228
    public function testPropertyWriteAccess(object $instance, AccessInterceptorInterface $proxy, string $publicProperty
229
    ) : void
230
    {
231
        $newValue               = uniqid('value', true);
232
        $proxy->$publicProperty = $newValue;
233
234
        self::assertSame($newValue, $proxy->$publicProperty);
235
        $this->assertProxySynchronized($instance, $proxy);
236
    }
237
238
    /**
239
     * @dataProvider getPropertyAccessProxies
240
     */
241
    public function testPropertyExistence(object $instance, AccessInterceptorInterface $proxy, string $publicProperty
242
    ) : void
243
    {
244
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
245
        $this->assertProxySynchronized($instance, $proxy);
246
247
        $instance->$publicProperty = null;
248
        self::assertFalse(isset($proxy->$publicProperty));
249
        $this->assertProxySynchronized($instance, $proxy);
250
    }
251
252
    /**
253
     * @dataProvider getPropertyAccessProxies
254
     */
255
    public function testPropertyUnset(object $instance, AccessInterceptorInterface $proxy, string $publicProperty
256
    ) : void
257
    {
258
        self::markTestSkipped('It is currently not possible to synchronize properties un-setting');
259
        unset($proxy->$publicProperty);
260
261
        self::assertFalse(isset($instance->$publicProperty));
262
        self::assertFalse(isset($proxy->$publicProperty));
263
        $this->assertProxySynchronized($instance, $proxy);
264
    }
265
266
    /**
267
     * Verifies that accessing a public property containing an array behaves like in a normal context
268
     */
269
    public function testCanWriteToArrayKeysInPublicProperty() : void
270
    {
271
        $instance  = new ClassWithPublicArrayPropertyAccessibleViaMethod();
272
        $proxy     = (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance);
273
274
        $proxy->arrayProperty['foo'] = 'bar';
275
276
        self::assertSame('bar', $proxy->getArrayProperty()['foo']);
277
278
        $proxy->arrayProperty = ['tab' => 'taz'];
279
280
        self::assertSame(['tab' => 'taz'], $proxy->arrayProperty);
281
282
        $this->assertProxySynchronized($instance, $proxy);
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...
283
    }
284
285
    /**
286
     * Verifies that public properties retrieved via `__get` don't get modified in the object state
287
     */
288
    public function testWillNotModifyRetrievedPublicProperties() : void
289
    {
290
        $instance  = new ClassWithPublicProperties();
291
        $proxy     = (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance);
292
293
        $variable = $proxy->property0;
294
295
        self::assertByRefVariableValueSame('property0', $variable);
296
297
        $variable = 'foo';
298
299
        self::assertByRefVariableValueSame('property0', $proxy->property0);
300
301
        $this->assertProxySynchronized($instance, $proxy);
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...
302
303
        self::assertByRefVariableValueSame('foo', $variable);
304
    }
305
306
    /**
307
     * Verifies that public properties references retrieved via `__get` modify in the object state
308
     */
309
    public function testWillModifyByRefRetrievedPublicProperties() : void
310
    {
311
        $instance  = new ClassWithPublicProperties();
312
        $proxy     = (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance);
313
314
        $variable = &$proxy->property0;
315
316
        self::assertByRefVariableValueSame('property0', $variable);
317
318
        $variable = 'foo';
319
320
        self::assertByRefVariableValueSame('foo', $proxy->property0);
321
322
        $this->assertProxySynchronized($instance, $proxy);
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...
323
324
        self::assertByRefVariableValueSame('foo', $variable);
325
    }
326
327
    /**
328
     * @group 115
329
     * @group 175
330
     */
331
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
332
    {
333
        $instance = new ClassWithCounterConstructor(10);
334
335
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
336
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
337
        $instance->__construct(3);
338
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
339
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
340
341
        $proxyName = get_class(
342
            (new AccessInterceptorScopeLocalizerFactory())
343
                ->createProxy(new ClassWithCounterConstructor(0))
344
        );
345
346
        $proxy = new $proxyName(15);
347
348
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
349
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
350
        $proxy->__construct(5);
351
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
352
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
353
    }
354
355
    /**
356
     * Generates a list of object | invoked method | parameters | expected result
357
     *
358
     * @return array<int, array<object|array<string, mixed>|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...
359
     */
360
    public static function getProxyMethods() : array
361
    {
362
        $selfHintParam = new ClassWithSelfHint();
363
        $empty         = new EmptyClass();
364
365
        return [
366
            [
367
                new BaseClass(),
368
                'publicMethod',
369
                [],
370
                'publicMethodDefault',
371
            ],
372
            [
373
                new BaseClass(),
374
                'publicTypeHintedMethod',
375
                ['param' => new stdClass()],
376
                'publicTypeHintedMethodDefault',
377
            ],
378
            [
379
                new BaseClass(),
380
                'publicByReferenceMethod',
381
                [],
382
                'publicByReferenceMethodDefault',
383
            ],
384
            [
385
                new ClassWithSelfHint(),
386
                'selfHintMethod',
387
                ['parameter' => $selfHintParam],
388
                $selfHintParam,
389
            ],
390
            [
391
                new ClassWithParentHint(),
392
                'parentHintMethod',
393
                ['parameter' => $empty],
394
                $empty,
395
            ],
396
        ];
397
    }
398
399
    /**
400
     * Generates proxies and instances with a public property to feed to the property accessor methods
401
     *
402
     * @return array<int, array<int, object|AccessInterceptorInterface|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...
403
     */
404
    public function getPropertyAccessProxies() : array
405
    {
406
        $instance  = new BaseClass();
407
408
        return [
409
            [
410
                $instance,
411
                (new AccessInterceptorScopeLocalizerFactory())->createProxy($instance),
412
                'publicProperty',
413
                'publicPropertyDefault',
414
            ],
415
        ];
416
    }
417
418
    private function assertProxySynchronized(object $instance, AccessInterceptorInterface $proxy) : void
419
    {
420
        $reflectionClass = new ReflectionClass($instance);
421
422
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
423
            $property->setAccessible(true);
424
425
            self::assertSame(
426
                $property->getValue($instance),
427
                $property->getValue($proxy),
428
                'Property "' . $property->getName() . '" is synchronized between instance and proxy'
429
            );
430
        }
431
    }
432
433
    public function testWillForwardVariadicArguments() : void
434
    {
435
        $configuration = new Configuration();
436
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
437
        $targetObject  = new ClassWithMethodWithVariadicFunction();
438
439
        $object = $factory->createProxy(
440
            $targetObject,
441
            [
442
                'bar' => static function () : string {
443
                    return 'Foo Baz';
444
                },
445
            ]
446
        );
447
448
        self::assertNull($object->bar);
449
        self::assertNull($object->baz);
450
451
        $object->foo('Ocramius', 'Malukenho', 'Danizord');
452
        self::assertSame('Ocramius', $object->bar);
453
        self::assertSame(['Malukenho', 'Danizord'], Assert::readAttribute($object, 'baz'));
454
    }
455
456
    /**
457
     * @group 265
458
     */
459
    public function testWillForwardVariadicByRefArguments() : void
460
    {
461
        $configuration = new Configuration();
462
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
463
        $targetObject  = new ClassWithMethodWithByRefVariadicFunction();
464
465
        $object = $factory->createProxy(
466
            $targetObject,
467
            [
468
                'bar' => static function () : string {
469
                    return 'Foo Baz';
470
                },
471
            ]
472
        );
473
474
        $parameters = ['a', 'b', 'c'];
475
476
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
477
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
478
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
479
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
480
    }
481
482
    /**
483
     * This test documents a known limitation: `func_get_args()` (and similar) don't work in proxied APIs.
484
     * If you manage to make this test pass, then please do send a patch
485
     *
486
     * @group 265
487
     */
488
    public function testWillNotForwardDynamicArguments() : void
489
    {
490
        $object = (new AccessInterceptorScopeLocalizerFactory())
491
            ->createProxy(
492
                new ClassWithDynamicArgumentsMethod(),
493
                [
494
                    'dynamicArgumentsMethod' => static function () : string {
495
                        return 'Foo Baz';
496
                    },
497
                ]
498
            );
499
500
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
501
502
        $this->expectException(ExpectationFailedException::class);
503
504
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
505
    }
506
507
    /**
508
     * @group 327
509
     */
510
    public function testWillInterceptAndReturnEarlyOnVoidMethod() : void
511
    {
512
        $skip      = random_int(100, 200);
513
        $addMore   = random_int(201, 300);
514
        $increment = random_int(301, 400);
515
516
        $object = (new AccessInterceptorScopeLocalizerFactory())
517
            ->createProxy(
518
                new VoidCounter(),
519
                [
520
                    'increment' => static function (
521
                        AccessInterceptorInterface $proxy,
522
                        VoidCounter $instance,
523
                        string $method,
524
                        array $params,
525
                        ?bool & $returnEarly
526
                    ) use ($skip) : void {
527
                        if ($skip !== $params['amount']) {
528
                            return;
529
                        }
530
531
                        $returnEarly = true;
532
                    },
533
                ],
534
                [
535
                    'increment' => static function (
536
                        AccessInterceptorInterface $proxy,
537
                        VoidCounter $instance,
538
                        string $method,
539
                        array $params,
540
                        ?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...
541
                    ) use ($addMore) : void {
542
                        if ($addMore !== $params['amount']) {
543
                            return;
544
                        }
545
546
                        $instance->counter += 1;
547
                    },
548
                ]
549
            );
550
551
        $object->increment($skip);
552
        self::assertSame(0, $object->counter);
553
554
        $object->increment($increment);
555
        self::assertSame($increment, $object->counter);
556
557
        $object->increment($addMore);
558
        self::assertSame($increment + $addMore + 1, $object->counter);
559
    }
560
561
    /**
562
     * @param mixed $expected
563
     * @param mixed $actual
564
     */
565
    private static function assertByRefVariableValueSame($expected, & $actual) : void
566
    {
567
        self::assertSame($expected, $actual);
568
    }
569
}
570