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 PHPUnit\Framework\ExpectationFailedException;
8
use PHPUnit\Framework\TestCase;
9
use ProxyManager\Configuration;
10
use ProxyManager\Exception\UnsupportedProxiedClassException;
11
use ProxyManager\Factory\AccessInterceptorScopeLocalizerFactory;
12
use ProxyManager\Generator\ClassGenerator;
13
use ProxyManager\Generator\Util\UniqueIdentifierGenerator;
14
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
15
use ProxyManager\Proxy\AccessInterceptorInterface;
16
use ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator;
17
use ProxyManager\ProxyGenerator\Util\Properties;
18
use ProxyManagerTest\Assert;
19
use ProxyManagerTestAsset\BaseClass;
20
use ProxyManagerTestAsset\CallableInterface;
21
use ProxyManagerTestAsset\ClassWithCounterConstructor;
22
use ProxyManagerTestAsset\ClassWithDynamicArgumentsMethod;
23
use ProxyManagerTestAsset\ClassWithMethodWithByRefVariadicFunction;
24
use ProxyManagerTestAsset\ClassWithMethodWithVariadicFunction;
25
use ProxyManagerTestAsset\ClassWithParentHint;
26
use ProxyManagerTestAsset\ClassWithPublicArrayProperty;
27
use ProxyManagerTestAsset\ClassWithPublicArrayPropertyAccessibleViaMethod;
28
use ProxyManagerTestAsset\ClassWithPublicProperties;
29
use ProxyManagerTestAsset\ClassWithSelfHint;
30
use ProxyManagerTestAsset\EmptyClass;
31
use ProxyManagerTestAsset\VoidCounter;
32
use ReflectionClass;
33
use stdClass;
34
use function array_values;
35
use function get_class;
36
use function random_int;
37
use function serialize;
38
use function uniqid;
39
use function unserialize;
40
41
/**
42
 * Tests for {@see \ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator} produced objects
43
 *
44
 * @group Functional
45
 * @coversNothing
46
 */
47
final class AccessInterceptorScopeLocalizerFunctionalTest extends TestCase
48
{
49
    /**
50
     * @param mixed[] $params
51
     * @param mixed   $expectedValue
52
     *
53
     * @dataProvider getProxyMethods
54
     *
55
     * @psalm-template OriginalClass
56
     * @psalm-param class-string<OriginalClass> $className
57
     * @psalm-param OriginalClass $instance
58
     */
59
    public function testMethodCalls(string $className, object $instance, string $method, array $params, $expectedValue
60
    ) : void
61
    {
62
        $proxy = $this->makeProxy($className, $instance);
63
64
        $this->assertProxySynchronized($instance, $proxy);
65
66
        $callback = [$proxy, $method];
67
68
        self::assertIsCallable($callback);
69
        self::assertSame($expectedValue, $callback(...array_values($params)));
70
71
        $listener = $this->createMock(CallableInterface::class);
72
        $listener
73
            ->expects(self::once())
74
            ->method('__invoke')
75
            ->with($proxy, $proxy, $method, $params, false);
76
77
        $proxy->setMethodPrefixInterceptor(
78
            $method,
79
            static function (
80
                AccessInterceptorInterface $proxy,
81
                object $instance,
82
                string $method,
83
                array $params,
84
                bool & $returnEarly
85
            ) use ($listener) : void {
86
                $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...
87
            }
88
        );
89
90
        self::assertSame($expectedValue, $callback(...array_values($params)));
91
92
        $random = uniqid('', true);
93
94
        $proxy->setMethodPrefixInterceptor(
95
            $method,
96
            static function (
97
                AccessInterceptorInterface $proxy,
98
                object $instance,
99
                string $method,
100
                array $params,
101
                bool & $returnEarly
102
            ) use ($random) : string {
103
                $returnEarly = true;
104
105
                return $random;
106
            }
107
        );
108
109
        self::assertSame($random, $callback(...array_values($params)));
110
111
        $this->assertProxySynchronized($instance, $proxy);
112
    }
113
114
    /**
115
     * @param mixed[] $params
116
     * @param mixed   $expectedValue
117
     *
118
     * @dataProvider getProxyMethods
119
     *
120
     * @psalm-template OriginalClass
121
     * @psalm-param class-string<OriginalClass> $className
122
     * @psalm-param OriginalClass $instance
123
     */
124
    public function testMethodCallsWithSuffixListener(
125
        string $className,
126
        object $instance,
127
        string $method,
128
        array $params,
129
        $expectedValue
130
    ) : void {
131
        $proxy    = $this->makeProxy($className, $instance);
132
        $callback = [$proxy, $method];
133
134
        self::assertIsCallable($callback);
135
136
        $listener = $this->createMock(CallableInterface::class);
137
        $listener
138
            ->expects(self::once())
139
            ->method('__invoke')
140
            ->with($proxy, $proxy, $method, $params, $expectedValue, false);
141
142
        $proxy->setMethodSuffixInterceptor(
143
            $method,
144
            /** @param mixed $returnValue */
145
            static function (
146
                AccessInterceptorInterface $proxy,
147
                object $instance,
148
                string $method,
149
                array $params,
150
                $returnValue,
151
                bool & $returnEarly
152
            ) use ($listener) : void {
153
                $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...
154
            }
155
        );
156
157
        self::assertSame($expectedValue, $callback(...array_values($params)));
158
159
        $random = uniqid('', true);
160
161
        $proxy->setMethodSuffixInterceptor(
162
            $method,
163
            /** @param mixed $returnValue */
164
            static function (
165
                AccessInterceptorInterface $proxy,
166
                object $instance,
167
                string $method,
168
                array $params,
169
                $returnValue,
170
                bool & $returnEarly
171
            ) use ($random) : string {
172
                $returnEarly = true;
173
174
                return $random;
175
            }
176
        );
177
178
        self::assertSame($random, $callback(...array_values($params)));
179
180
        $this->assertProxySynchronized($instance, $proxy);
181
    }
182
183
    /**
184
     * @param mixed[] $params
185
     * @param mixed   $expectedValue
186
     *
187
     * @dataProvider getProxyMethods
188
     *
189
     * @psalm-template OriginalClass
190
     * @psalm-param class-string<OriginalClass> $className
191
     * @psalm-param OriginalClass $instance
192
     */
193
    public function testMethodCallsAfterUnSerialization(
194
        string $className,
195
        object $instance,
196
        string $method,
197
        array $params,
198
        $expectedValue
199
    ) : void {
200
        /** @var AccessInterceptorInterface $proxy */
201
        $proxy = unserialize(serialize($this->makeProxy($className, $instance)));
202
203
        $callback = [$proxy, $method];
204
205
        self::assertIsCallable($callback);
206
        self::assertSame($expectedValue, $callback(...array_values($params)));
207
        $this->assertProxySynchronized($instance, $proxy);
208
    }
209
210
    /**
211
     * @param mixed[] $params
212
     * @param mixed   $expectedValue
213
     *
214
     * @dataProvider getProxyMethods
215
     *
216
     * @psalm-template OriginalClass
217
     * @psalm-param class-string<OriginalClass> $className
218
     * @psalm-param OriginalClass $instance
219
     */
220
    public function testMethodCallsAfterCloning(
221
        string $className,
222
        object $instance,
223
        string $method,
224
        array $params,
225
        $expectedValue
226
    ) : void {
227
        $proxy    = $this->makeProxy($className, $instance);
228
        $cloned   = clone $proxy;
229
        $callback = [$cloned, $method];
230
231
        $this->assertProxySynchronized($instance, $proxy);
232
        self::assertIsCallable($callback);
233
        self::assertSame($expectedValue, $callback(...array_values($params)));
234
        $this->assertProxySynchronized($instance, $proxy);
235
    }
236
237
    /**
238
     * @param mixed $propertyValue
239
     *
240
     * @dataProvider getPropertyAccessProxies
241
     */
242
    public function testPropertyReadAccess(
243
        object $instance,
244
        AccessInterceptorInterface $proxy,
245
        string $publicProperty,
246
        $propertyValue
247
    ) : void {
248
        self::assertSame($propertyValue, $proxy->$publicProperty);
249
        $this->assertProxySynchronized($instance, $proxy);
250
    }
251
252
    /**
253
     * @dataProvider getPropertyAccessProxies
254
     */
255
    public function testPropertyWriteAccess(object $instance, AccessInterceptorInterface $proxy, string $publicProperty
256
    ) : void
257
    {
258
        $newValue               = uniqid('value', true);
259
        $proxy->$publicProperty = $newValue;
260
261
        self::assertSame($newValue, $proxy->$publicProperty);
262
        $this->assertProxySynchronized($instance, $proxy);
263
    }
264
265
    /**
266
     * @dataProvider getPropertyAccessProxies
267
     */
268
    public function testPropertyExistence(object $instance, AccessInterceptorInterface $proxy, string $publicProperty
269
    ) : void
270
    {
271
        self::assertSame(isset($instance->$publicProperty), isset($proxy->$publicProperty));
272
        $this->assertProxySynchronized($instance, $proxy);
273
274
        $instance->$publicProperty = null;
275
        self::assertFalse(isset($proxy->$publicProperty));
276
        $this->assertProxySynchronized($instance, $proxy);
277
    }
278
279
    /**
280
     * @dataProvider getPropertyAccessProxies
281
     */
282
    public function testPropertyUnset(object $instance, AccessInterceptorInterface $proxy, string $publicProperty
283
    ) : void
284
    {
285
        self::markTestSkipped('It is currently not possible to synchronize properties un-setting');
286
        unset($proxy->$publicProperty);
287
288
        self::assertFalse(isset($instance->$publicProperty));
289
        self::assertFalse(isset($proxy->$publicProperty));
290
        $this->assertProxySynchronized($instance, $proxy);
291
    }
292
293
    /**
294
     * Verifies that accessing a public property containing an array behaves like in a normal context
295
     */
296
    public function testCanWriteToArrayKeysInPublicProperty() : void
297
    {
298
        $instance  = new ClassWithPublicArrayPropertyAccessibleViaMethod();
299
        $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...
300
301
        $proxy->arrayProperty['foo'] = 'bar';
0 ignored issues
show
Bug introduced by
Accessing arrayProperty on the interface ProxyManager\Proxy\AccessInterceptorInterface 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...
302
303
        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...ssInterceptorInterface>.

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...
304
305
        $proxy->arrayProperty = ['tab' => 'taz'];
0 ignored issues
show
Bug introduced by
Accessing arrayProperty on the interface ProxyManager\Proxy\AccessInterceptorInterface 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(['tab' => 'taz'], $proxy->arrayProperty);
0 ignored issues
show
Bug introduced by
Accessing arrayProperty on the interface ProxyManager\Proxy\AccessInterceptorInterface 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...
308
309
        $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...
310
    }
311
312
    /**
313
     * Verifies that public properties retrieved via `__get` don't get modified in the object state
314
     */
315
    public function testWillNotModifyRetrievedPublicProperties() : void
316
    {
317
        $instance  = new ClassWithPublicProperties();
318
        $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...
319
320
        $variable = $proxy->property0;
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\AccessInterceptorInterface 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...
321
322
        self::assertByRefVariableValueSame('property0', $variable);
323
324
        $variable = 'foo';
325
326
        self::assertByRefVariableValueSame('property0', $proxy->property0);
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\AccessInterceptorInterface 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...
327
328
        $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...
329
330
        self::assertByRefVariableValueSame('foo', $variable);
331
    }
332
333
    /**
334
     * Verifies that public properties references retrieved via `__get` modify in the object state
335
     */
336
    public function testWillModifyByRefRetrievedPublicProperties() : void
337
    {
338
        $instance  = new ClassWithPublicProperties();
339
        $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...
340
341
        $variable = &$proxy->property0;
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\AccessInterceptorInterface 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...
342
343
        self::assertByRefVariableValueSame('property0', $variable);
344
345
        $variable = 'foo';
346
347
        self::assertByRefVariableValueSame('foo', $proxy->property0);
0 ignored issues
show
Bug introduced by
Accessing property0 on the interface ProxyManager\Proxy\AccessInterceptorInterface 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...
348
349
        $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...
350
351
        self::assertByRefVariableValueSame('foo', $variable);
352
    }
353
354
    /**
355
     * @group 115
356
     * @group 175
357
     */
358
    public function testWillBehaveLikeObjectWithNormalConstructor() : void
359
    {
360
        $instance = new ClassWithCounterConstructor(10);
361
362
        self::assertSame(10, $instance->amount, 'Verifying that test asset works as expected');
363
        self::assertSame(10, $instance->getAmount(), 'Verifying that test asset works as expected');
364
        $instance->__construct(3);
365
        self::assertSame(13, $instance->amount, 'Verifying that test asset works as expected');
366
        self::assertSame(13, $instance->getAmount(), 'Verifying that test asset works as expected');
367
368
        $proxyName = $this->generateProxy(ClassWithCounterConstructor::class);
369
370
        $proxy = new $proxyName(15);
371
372
        self::assertSame(15, $proxy->amount, 'Verifying that the proxy constructor works as expected');
373
        self::assertSame(15, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
374
        $proxy->__construct(5);
375
        self::assertSame(20, $proxy->amount, 'Verifying that the proxy constructor works as expected');
376
        self::assertSame(20, $proxy->getAmount(), 'Verifying that the proxy constructor works as expected');
377
    }
378
379
    /**
380
     * Generates a proxy for the given class name, and retrieves its class name
381
     *
382
     * @throws UnsupportedProxiedClassException
383
     *
384
     * @psalm-template OriginalClass
385
     * @psalm-param class-string<OriginalClass> $originalClassName
386
     * @psalm-return class-string<OriginalClass>
387
     * @psalm-suppress MoreSpecificReturnType
388
     */
389
    private function generateProxy(string $originalClassName) : string
390
    {
391
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
392
        $generator          = new AccessInterceptorScopeLocalizerGenerator();
393
        $generatedClass     = new ClassGenerator($generatedClassName);
394
        $strategy           = new EvaluatingGeneratorStrategy();
395
396
        $generator->generate(new ReflectionClass($originalClassName), $generatedClass);
397
        $strategy->generate($generatedClass);
398
399
        /** @psalm-suppress LessSpecificReturnStatement */
400
        return $generatedClassName;
401
    }
402
403
    /**
404
     * @psalm-template OriginalClass
405
     * @psalm-param class-string<OriginalClass> $originalClassName
406
     * @psalm-param OriginalClass $realInstance
407
     * @psalm-return AccessInterceptorInterface<OriginalClass>&OriginalClass
408
     * @psalm-suppress MixedInferredReturnType
409
     * @psalm-suppress MoreSpecificReturnType
410
     */
411
    private function makeProxy(string $originalClassName, object $realInstance) : AccessInterceptorInterface
412
    {
413
        $proxyClassName = $this->generateProxy($originalClassName);
414
415
        /**
416
         * @psalm-suppress MixedMethodCall
417
         * @psalm-suppress MixedReturnStatement
418
         */
419
        return $proxyClassName::staticProxyConstructor($realInstance);
420
    }
421
422
    /**
423
     * Generates a list of object | invoked method | parameters | expected result
424
     *
425
     * @return array<int, array<class-string|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...
426
     */
427
    public static function getProxyMethods() : array
428
    {
429
        $selfHintParam = new ClassWithSelfHint();
430
        $empty         = new EmptyClass();
431
432
        return [
433
            [
434
                BaseClass::class,
435
                new BaseClass(),
436
                'publicMethod',
437
                [],
438
                'publicMethodDefault',
439
            ],
440
            [
441
                BaseClass::class,
442
                new BaseClass(),
443
                'publicTypeHintedMethod',
444
                ['param' => new stdClass()],
445
                'publicTypeHintedMethodDefault',
446
            ],
447
            [
448
                BaseClass::class,
449
                new BaseClass(),
450
                'publicByReferenceMethod',
451
                [],
452
                'publicByReferenceMethodDefault',
453
            ],
454
            [
455
                ClassWithSelfHint::class,
456
                new ClassWithSelfHint(),
457
                'selfHintMethod',
458
                ['parameter' => $selfHintParam],
459
                $selfHintParam,
460
            ],
461
            [
462
                ClassWithParentHint::class,
463
                new ClassWithParentHint(),
464
                'parentHintMethod',
465
                ['parameter' => $empty],
466
                $empty,
467
            ],
468
        ];
469
    }
470
471
    /**
472
     * Generates proxies and instances with a public property to feed to the property accessor methods
473
     *
474
     * @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...
475
     */
476
    public function getPropertyAccessProxies() : array
477
    {
478
        $instance  = new BaseClass();
479
480
        return [
481
            [
482
                $instance,
483
                $this->makeProxy(BaseClass::class, $instance),
0 ignored issues
show
Documentation introduced by
$instance 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...
484
                'publicProperty',
485
                'publicPropertyDefault',
486
            ],
487
        ];
488
    }
489
490
    private function assertProxySynchronized(object $instance, AccessInterceptorInterface $proxy) : void
491
    {
492
        $reflectionClass = new ReflectionClass($instance);
493
494
        foreach (Properties::fromReflectionClass($reflectionClass)->getInstanceProperties() as $property) {
495
            $property->setAccessible(true);
496
497
            self::assertSame(
498
                $property->getValue($instance),
499
                $property->getValue($proxy),
500
                'Property "' . $property->getName() . '" is synchronized between instance and proxy'
501
            );
502
        }
503
    }
504
505
    public function testWillForwardVariadicArguments() : void
506
    {
507
        $configuration = new Configuration();
508
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
509
        $targetObject  = new ClassWithMethodWithVariadicFunction();
510
511
        $object = $factory->createProxy(
512
            $targetObject,
513
            [
514
                'bar' => static function () : string {
515
                    return 'Foo Baz';
516
                },
517
            ]
518
        );
519
520
        self::assertNull($object->bar);
521
        self::assertNull($object->baz);
522
523
        $object->foo('Ocramius', 'Malukenho', 'Danizord');
524
        self::assertSame('Ocramius', $object->bar);
525
        self::assertSame(['Malukenho', 'Danizord'], Assert::readAttribute($object, 'baz'));
526
    }
527
528
    /**
529
     * @group 265
530
     */
531
    public function testWillForwardVariadicByRefArguments() : void
532
    {
533
        $configuration = new Configuration();
534
        $factory       = new AccessInterceptorScopeLocalizerFactory($configuration);
535
        $targetObject  = new ClassWithMethodWithByRefVariadicFunction();
536
537
        $object = $factory->createProxy(
538
            $targetObject,
539
            [
540
                'bar' => static function () : string {
541
                    return 'Foo Baz';
542
                },
543
            ]
544
        );
545
546
        $parameters = ['a', 'b', 'c'];
547
548
        // first, testing normal variadic behavior (verifying we didn't screw up in the test asset)
549
        self::assertSame(['a', 'changed', 'c'], (new ClassWithMethodWithByRefVariadicFunction())->tuz(...$parameters));
550
        self::assertSame(['a', 'changed', 'c'], $object->tuz(...$parameters));
551
        self::assertSame(['a', 'changed', 'c'], $parameters, 'by-ref variadic parameter was changed');
552
    }
553
554
    /**
555
     * This test documents a known limitation: `func_get_args()` (and similar) don't work in proxied APIs.
556
     * If you manage to make this test pass, then please do send a patch
557
     *
558
     * @group 265
559
     */
560
    public function testWillNotForwardDynamicArguments() : void
561
    {
562
        $object = (new AccessInterceptorScopeLocalizerFactory())
563
            ->createProxy(
564
                new ClassWithDynamicArgumentsMethod(),
565
                [
566
                    'dynamicArgumentsMethod' => static function () : string {
567
                        return 'Foo Baz';
568
                    },
569
                ]
570
            );
571
572
        self::assertSame(['a', 'b'], (new ClassWithDynamicArgumentsMethod())->dynamicArgumentsMethod('a', 'b'));
573
574
        $this->expectException(ExpectationFailedException::class);
575
576
        self::assertSame(['a', 'b'], $object->dynamicArgumentsMethod('a', 'b'));
577
    }
578
579
    /**
580
     * @group 327
581
     */
582
    public function testWillInterceptAndReturnEarlyOnVoidMethod() : void
583
    {
584
        $skip      = random_int(100, 200);
585
        $addMore   = random_int(201, 300);
586
        $increment = random_int(301, 400);
587
588
        $object = (new AccessInterceptorScopeLocalizerFactory())
589
            ->createProxy(
590
                new VoidCounter(),
591
                [
592
                    'increment' => static function (
593
                        AccessInterceptorInterface $proxy,
594
                        VoidCounter $instance,
595
                        string $method,
596
                        array $params,
597
                        ?bool & $returnEarly
598
                    ) use ($skip) : void {
599
                        if ($skip !== $params['amount']) {
600
                            return;
601
                        }
602
603
                        $returnEarly = true;
604
                    },
605
                ],
606
                [
607
                    'increment' => static function (
608
                        AccessInterceptorInterface $proxy,
609
                        VoidCounter $instance,
610
                        string $method,
611
                        array $params,
612
                        ?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...
613
                    ) use ($addMore) : void {
614
                        if ($addMore !== $params['amount']) {
615
                            return;
616
                        }
617
618
                        $instance->counter += 1;
619
                    },
620
                ]
621
            );
622
623
        $object->increment($skip);
624
        self::assertSame(0, $object->counter);
625
626
        $object->increment($increment);
627
        self::assertSame($increment, $object->counter);
628
629
        $object->increment($addMore);
630
        self::assertSame($increment + $addMore + 1, $object->counter);
631
    }
632
633
    /**
634
     * @param mixed $expected
635
     * @param mixed $actual
636
     */
637
    private static function assertByRefVariableValueSame($expected, & $actual) : void
638
    {
639
        self::assertSame($expected, $actual);
640
    }
641
}
642