Passed
Pull Request — master (#8)
by
unknown
04:39
created

InjectorTest.php$0 ➔ testInvokePrivateMethods()   A

Complexity

Conditions 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
cc 2
rs 9.9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Injector\Tests;
6
7
use DateTime;
8
use DateTimeImmutable;
9
use DateTimeInterface;
10
use PHPUnit\Framework\TestCase;
11
use Psr\Container\ContainerInterface;
12
use Psr\Container\NotFoundExceptionInterface;
13
use stdClass;
14
use Yiisoft\Injector\Injector;
15
use Yiisoft\Injector\InvalidArgumentException;
16
use Yiisoft\Injector\MissingRequiredArgumentException;
17
use Yiisoft\Injector\Tests\Support\ColorInterface;
18
use Yiisoft\Injector\Tests\Support\ContextMethod;
19
use Yiisoft\Injector\Tests\Support\EngineInterface;
20
use Yiisoft\Injector\Tests\Support\EngineMarkTwo;
21
use Yiisoft\Injector\Tests\Support\EngineZIL130;
22
use Yiisoft\Injector\Tests\Support\EngineVAZ2101;
23
use Yiisoft\Injector\Tests\Support\Invokeable;
24
use Yiisoft\Injector\Tests\Support\LightEngine;
25
use Yiisoft\Injector\Tests\Support\MakeEmptyConstructor;
26
use Yiisoft\Injector\Tests\Support\MakeEngineCollector;
27
use Yiisoft\Injector\Tests\Support\MakeEngineMatherWithParam;
28
use Yiisoft\Injector\Tests\Support\MakeNoConstructor;
29
use Yiisoft\Injector\Tests\Support\MakePrivateConstructor;
30
31
class InjectorTest extends TestCase
32
{
33
    /**
34
     * Injector should be able to invoke closure.
35
     */
36
    public function testInvokeClosure(): void
37
    {
38
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
39
40
        $getEngineName = fn (EngineInterface $engine) => $engine->getName();
41
42
        $engineName = (new Injector($container))->invoke($getEngineName);
43
44
        $this->assertSame('Mark Two', $engineName);
45
    }
46
47
    /**
48
     * Injector should be able to invoke array callable.
49
     */
50
    public function testInvokeCallableArray(): void
51
    {
52
        $container = $this->getContainer();
53
54
        $object = new EngineVAZ2101();
55
56
        $engine = (new Injector($container))->invoke([$object, 'rust'], ['index' => 5.0]);
57
58
        $this->assertInstanceOf(EngineVAZ2101::class, $engine);
59
    }
60
61
    /**
62
     * Injector should be able to invoke static method.
63
     */
64
    public function testInvokeStatic(): void
65
    {
66
        $container = $this->getContainer();
67
68
        $result = (new Injector($container))->invoke([EngineVAZ2101::class, 'isWroomWroom']);
69
70
        $this->assertIsBool($result);
71
    }
72
73
    /**
74
     * Injector should be able to invoke static method.
75
     */
76
    public function testInvokeAnonymousClass(): void
77
    {
78
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
79
        $class = new class() {
80
            public EngineInterface $engine;
81
            public function setEngine(EngineInterface $engine)
82
            {
83
                $this->engine = $engine;
84
            }
85
        };
86
87
        (new Injector($container))->invoke([$class, 'setEngine']);
88
89
        $this->assertInstanceOf(EngineInterface::class, $class->engine);
90
    }
91
92
    /**
93
     * Injector should be able to invoke method without arguments.
94
     */
95
    public function testInvokeWithoutArguments(): void
96
    {
97
        $container = $this->getContainer();
98
99
        $true = fn () => true;
100
101
        $result = (new Injector($container))->invoke($true);
102
103
        $this->assertTrue($result);
104
    }
105
106
    /**
107
     * Nullable arguments should be searched in container.
108
     */
109
    public function testWithNullableArgument(): void
110
    {
111
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
112
113
        $nullable = fn (?EngineInterface $engine) => $engine;
114
115
        $result = (new Injector($container))->invoke($nullable);
116
117
        $this->assertNotNull($result);
118
    }
119
120
    /**
121
     * Nullable arguments not found in container should be passed as `null`.
122
     */
123
    public function testWithNullableArgumentAndEmptyContainer(): void
124
    {
125
        $container = $this->getContainer();
126
127
        $nullable = fn (?EngineInterface $engine) => $engine;
128
129
        $result = (new Injector($container))->invoke($nullable);
130
131
        $this->assertNull($result);
132
    }
133
134
    /**
135
     * Nullable scalars should be set with `null` if not specified by name explicitly.
136
     */
137
    public function testWithNullableScalarArgument(): void
138
    {
139
        $container = $this->getContainer();
140
141
        $nullableInt = fn (?int $number) => $number;
142
143
        $result = (new Injector($container))->invoke($nullableInt);
144
145
        $this->assertNull($result);
146
    }
147
148
    /**
149
     * Optional scalar arguments should be set with default value if not specified by name explicitly.
150
     */
151
    public function testWithNullableOptionalArgument(): void
152
    {
153
        $container = $this->getContainer();
154
155
        $nullableInt = fn (?int $number = 6) => $number;
156
157
        $result = (new Injector($container))->invoke($nullableInt);
158
159
        $this->assertSame(6, $result);
160
    }
161
162
    /**
163
     * Optional arguments with `null` by default should be set with `null` if other value not specified in parameters
164
     * or container.
165
     */
166
    public function testWithNullableOptionalArgumentThatNull(): void
167
    {
168
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
169
170
        $callable = fn (EngineInterface $engine = null) => $engine;
171
172
        $result = (new Injector($container))->invoke($callable);
173
174
        $this->assertNotNull($result);
175
    }
176
177
    /**
178
     * An object for a typed argument can be specified in parameters without named key and without following the order.
179
     */
180
    public function testCustomDependency(): void
181
    {
182
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
183
        $needleEngine = new EngineZIL130();
184
185
        $getEngineName = fn (EngineInterface $engine) => $engine->getName();
186
187
        $engineName = (new Injector($container))->invoke(
188
            $getEngineName,
189
            [new stdClass(), $needleEngine, new DateTimeImmutable()]
190
        );
191
192
        $this->assertSame(EngineZIL130::NAME, $engineName);
193
    }
194
195
    /**
196
     * In this case, first argument will be set from parameters, and second argument from container.
197
     */
198
    public function testTwoEqualCustomArgumentsWithOneCustom(): void
199
    {
200
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
201
202
        $compareEngines = static function (EngineInterface $engine1, EngineInterface $engine2) {
203
            return $engine1->getPower() <=> $engine2->getPower();
204
        };
205
        $zilEngine = new EngineZIL130();
206
207
        $result = (new Injector($container))->invoke($compareEngines, [$zilEngine]);
208
209
        $this->assertSame(-1, $result);
210
    }
211
212
    /**
213
     * In this case, second argument will be set from parameters by name, and first argument from container.
214
     */
215
    public function testTwoEqualCustomArgumentsWithOneCustomNamedParameter(): void
216
    {
217
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
218
219
        $compareEngines = static function (EngineInterface $engine1, EngineInterface $engine2) {
220
            return $engine1->getPower() <=> $engine2->getPower();
221
        };
222
        $zilEngine = new EngineZIL130();
223
224
        $result = (new Injector($container))->invoke($compareEngines, ['engine2' => $zilEngine]);
225
226
        $this->assertSame(1, $result);
227
    }
228
229
    /**
230
     * Values for arguments are not matched by the greater similarity of parameter types and arguments, but simply pass
231
     * in order as is.
232
     */
233
    public function testExtendedArgumentsWithOneCustomNamedParameter2(): void
234
    {
235
        $container = $this->getContainer([LightEngine::class => new EngineVAZ2101()]);
236
237
        $concatEngineNames = static function (EngineInterface $engine1, LightEngine $engine2) {
238
            return $engine1->getName() . $engine2->getName();
239
        };
240
241
        $result = (new Injector($container))->invoke($concatEngineNames, [
242
            new EngineMarkTwo(), // LightEngine, EngineInterface
243
            new EngineZIL130(), // EngineInterface
244
        ]);
245
246
        $this->assertSame(EngineMarkTwo::NAME . EngineVAZ2101::NAME, $result);
247
    }
248
249
    public function testMissingRequiredTypedParameter(): void
250
    {
251
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
252
253
        $getEngineName = static function (EngineInterface $engine, string $two) {
0 ignored issues
show
Unused Code introduced by
The parameter $two is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

253
        $getEngineName = static function (EngineInterface $engine, /** @scrutinizer ignore-unused */ string $two) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
254
            return $engine->getName();
255
        };
256
257
        $injector = new Injector($container);
258
259
        $this->expectException(MissingRequiredArgumentException::class);
260
        $injector->invoke($getEngineName);
261
    }
262
263
    public function testMissingRequiredNotTypedParameter(): void
264
    {
265
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
266
267
        $getEngineName = static function (EngineInterface $engine, $two) {
0 ignored issues
show
Unused Code introduced by
The parameter $two is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

267
        $getEngineName = static function (EngineInterface $engine, /** @scrutinizer ignore-unused */ $two) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
268
            return $engine->getName();
269
        };
270
        $injector = new Injector($container);
271
272
        $this->expectException(MissingRequiredArgumentException::class);
273
274
        $injector->invoke($getEngineName);
275
    }
276
277
    public function testNotFoundException(): void
278
    {
279
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
280
281
        $getEngineName = static function (EngineInterface $engine, ColorInterface $color) {
0 ignored issues
show
Unused Code introduced by
The parameter $color is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

281
        $getEngineName = static function (EngineInterface $engine, /** @scrutinizer ignore-unused */ ColorInterface $color) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
282
            return $engine->getName();
283
        };
284
285
        $injector = new Injector($container);
286
287
        $this->expectException(NotFoundExceptionInterface::class);
288
        $injector->invoke($getEngineName);
289
    }
290
291
    /**
292
     * A values collection for a variadic argument can be passed as an array in a named parameter.
293
     */
294
    public function testAloneScalarVariadicArgumentAnsNamedParam(): void
295
    {
296
        $container = $this->getContainer();
297
298
        $callable = fn (...$var) => array_sum($var);
299
300
        $result = (new Injector($container))->invoke($callable, ['var' => [1, 2, 3]]);
301
302
        $this->assertSame(6, $result);
303
    }
304
305
    /**
306
     * If type of a variadic argument is a class and named parameter with values collection is not set then injector
307
     * will search for objects by class name among all unnamed parameters.
308
     */
309
    public function testVariadicArgumentUnnamedParams(): void
310
    {
311
        $container = $this->getContainer([DateTimeInterface::class => new DateTimeImmutable()]);
312
313
        $callable = fn (DateTimeInterface $dateTime, EngineInterface ...$engines) => count($engines);
314
315
        $result = (new Injector($container))->invoke(
316
            $callable,
317
            [new EngineZIL130(), new EngineVAZ2101(), new stdClass(), new EngineMarkTwo(), new stdClass()]
318
        );
319
320
        $this->assertSame(3, $result);
321
    }
322
323
    /**
324
     * If calling method have an untyped variadic argument then all remaining unnamed parameters will be passed.
325
     */
326
    public function testVariadicMixedArgumentWithMixedParams(): void
327
    {
328
        $container = $this->getContainer([DateTimeInterface::class => new DateTimeImmutable()]);
329
330
        $callable = fn (...$engines) => $engines;
331
332
        $result = (new Injector($container))->invoke(
333
            $callable,
334
            [new EngineZIL130(), new EngineVAZ2101(), new EngineMarkTwo(), new stdClass()]
335
        );
336
337
        $this->assertCount(4, $result);
338
    }
339
340
    /**
341
     * Any unnamed parameter can only be an object. Scalar, array, null and other values can only be named parameters.
342
     */
343
    public function testVariadicStringArgumentWithUnnamedStringsParams(): void
344
    {
345
        $container = $this->getContainer([DateTimeInterface::class => new DateTimeImmutable()]);
346
347
        $callable = fn (string ...$engines) => $engines;
348
349
        $this->expectException(\Exception::class);
350
351
        (new Injector($container))->invoke($callable, ['str 1', 'str 2', 'str 3']);
352
    }
353
354
    /**
355
     * In the absence of other values to a nullable variadic argument `null` is not passed by default.
356
     */
357
    public function testNullableVariadicArgument(): void
358
    {
359
        $container = $this->getContainer();
360
361
        $callable = fn (?EngineInterface ...$engines) => $engines;
362
363
        $result = (new Injector($container))->invoke($callable, []);
364
365
        $this->assertSame([], $result);
366
    }
367
368
    /**
369
     * Parameters that were passed but were not used are appended to the call so they could be obtained
370
     * with func_get_args().
371
     */
372
    public function testAppendingUnusedParams(): void
373
    {
374
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
375
376
        $callable = fn (?EngineInterface $engine, $id = 'test') => func_num_args();
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

376
        $callable = fn (?EngineInterface $engine, /** @scrutinizer ignore-unused */ $id = 'test') => func_num_args();

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $engine is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

376
        $callable = fn (/** @scrutinizer ignore-unused */ ?EngineInterface $engine, $id = 'test') => func_num_args();

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
377
378
        $result = (new Injector($container))->invoke($callable, [new DateTimeImmutable(), new DateTimeImmutable()]);
379
380
        $this->assertSame(4, $result);
381
    }
382
383
    /**
384
     * Object type may be passed as unnamed parameter
385
     */
386
    public function testInvokeWithObjectType(): void
387
    {
388
        $container = $this->getContainer();
389
        $callable = fn (object $object) => get_class($object);
390
391
        $result = (new Injector($container))->invoke($callable, [new DateTimeImmutable()]);
392
393
        $this->assertSame(DateTimeImmutable::class, $result);
394
    }
395
396
    /**
397
     * Required `object` type should not be requested from the container
398
     */
399
    public function testInvokeWithRequiredObjectTypeWithoutInstance(): void
400
    {
401
        $container = $this->getContainer();
402
        $callable = fn (object $object) => get_class($object);
403
404
        $this->expectException(MissingRequiredArgumentException::class);
405
406
        (new Injector($container))->invoke($callable);
407
    }
408
409
    /**
410
     * Arguments passed by reference
411
     */
412
    public function testInvokeReferencedArguments(): void
413
    {
414
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
415
        $foo = 1;
416
        $bar = new stdClass();
417
        $baz = null;
418
        $callable = static function (
419
            int &$foo,
420
            object &$bar,
421
            &$baz,
422
            ?ColorInterface &$nullable,
423
            EngineInterface &$object, // from container
424
            DateTimeInterface &...$dates // collect all unnamed DateTimeInterface objects
425
        ) {
426
            $return = func_get_args();
427
            $bar = new DateTimeImmutable();
428
            $baz = false;
429
            $foo = count($dates);
430
            return $return;
431
        };
432
        $result = (new Injector($container))
433
            ->invoke($callable, [
434
                new DateTimeImmutable(),
435
                new DateTime(),
436
                new DateTime(),
437
                'foo' => &$foo,
438
                'bar' => $bar,
439
                'baz' => &$baz,
440
            ]);
441
442
        // passed
443
        $this->assertSame(1, $result[0]);
444
        $this->assertInstanceOf(stdClass::class, $result[1]);
445
        $this->assertNull($result[2]);
446
        $this->assertNull($result[3]);
447
        $this->assertInstanceOf(EngineMarkTwo::class, $result[4]);
448
        // transformed
449
        $this->assertSame(3, $foo); // count of DateTimeInterface objects
450
        $this->assertInstanceOf(stdClass::class, $bar);
451
        $this->assertFalse($baz);
452
    }
453
454
    public function testInvokeReferencedArgumentNamedVariadic(): void
455
    {
456
        $container = $this->getContainer();
457
458
        $callable = static function (DateTimeInterface &...$dates) {
459
            $dates[0] = false;
460
            $dates[1] = false;
461
            return count($dates);
462
        };
463
        $foo = new DateTimeImmutable();
464
        $bar = new DateTimeImmutable();
465
        $result = (new Injector($container))
466
            ->invoke($callable, [
467
                $foo,
468
                &$bar,
469
                new DateTime(),
470
            ]);
471
472
        $this->assertSame(3, $result);
473
        $this->assertInstanceOf(DateTimeImmutable::class, $foo);
474
        $this->assertFalse($bar);
475
    }
476
477
    /**
478
     * If argument passed by reference but it is not supported by function
479
     */
480
    public function testInvokeReferencedArgument(): void
481
    {
482
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
483
        $foo = 1;
484
        $callable = fn (int $foo) => ++$foo;
485
        $result = (new Injector($container))->invoke($callable, ['foo' => &$foo]);
486
487
        // $foo has been not changed
488
        $this->assertSame(1, $foo);
489
        $this->assertSame(2, $result);
490
    }
491
492
    public function testWrongNamedParam(): void
493
    {
494
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
495
496
        $callable = fn (EngineInterface $engine) => $engine;
497
498
        $this->expectException(\Throwable::class);
499
500
        (new Injector($container))->invoke($callable, ['engine' => new DateTimeImmutable()]);
501
    }
502
503
    public function testArrayArgumentWithUnnamedType(): void
504
    {
505
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
506
507
        $callable = fn (array $arg) => $arg;
508
509
        $this->expectException(MissingRequiredArgumentException::class);
510
511
        (new Injector($container))->invoke($callable, [['test']]);
512
    }
513
514
    public function testCallableArgumentWithUnnamedType(): void
515
    {
516
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
517
518
        $callable = fn (callable $arg) => $arg();
519
520
        $this->expectException(MissingRequiredArgumentException::class);
521
522
        (new Injector($container))->invoke($callable, [fn () => true]);
523
    }
524
525
    public function testIterableArgumentWithUnnamedType(): void
526
    {
527
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
528
529
        $callable = fn (iterable $arg) => $arg;
530
531
        $this->expectException(MissingRequiredArgumentException::class);
532
533
        (new Injector($container))->invoke($callable, [new \SplStack()]);
534
    }
535
536
    public function testUnnamedScalarParam(): void
537
    {
538
        $container = $this->getContainer();
539
540
        $getEngineName = fn () => 42;
541
542
        $this->expectException(InvalidArgumentException::class);
543
544
        (new Injector($container))->invoke($getEngineName, ['test']);
545
    }
546
547
    public function testInvokeable(): void
548
    {
549
        $container = $this->getContainer();
550
        $result = (new Injector($container))->invoke(new Invokeable());
551
        $this->assertSame(42, $result);
552
    }
553
554
    // Injector::make tests
555
556
    /**
557
     * Constructor method not defined
558
     */
559
    public function testMakeWithoutConstructor(): void
560
    {
561
        $container = $this->getContainer();
562
563
        $object = (new Injector($container))->make(MakeNoConstructor::class);
564
565
        $this->assertInstanceOf(MakeNoConstructor::class, $object);
566
    }
567
568
    /**
569
     * Constructor without arguments
570
     */
571
    public function testMakeWithoutArguments(): void
572
    {
573
        $container = $this->getContainer();
574
575
        $object = (new Injector($container))->make(MakeEmptyConstructor::class);
576
577
        $this->assertInstanceOf(MakeEmptyConstructor::class, $object);
578
    }
579
580
    /**
581
     * Private constructor unavailable from Injector context
582
     */
583
    public function testMakeWithPrivateConstructor(): void
584
    {
585
        $container = $this->getContainer();
586
587
        $this->expectException(\InvalidArgumentException::class);
588
        $this->expectExceptionMessageMatches('/not instantiable/');
589
590
        (new Injector($container))->make(MakePrivateConstructor::class);
591
    }
592
593
    public function testMakeInvalidClass(): void
594
    {
595
        $container = $this->getContainer();
596
597
        $this->expectException(\ReflectionException::class);
598
        $this->expectExceptionMessageMatches('/does not exist/');
599
600
        (new Injector($container))->make(UndefinedClassThatShouldNotBeDefined::class);
0 ignored issues
show
Bug introduced by
The type Yiisoft\Injector\Tests\U...sThatShouldNotBeDefined was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
601
    }
602
603
    public function testMakeInternalClass(): void
604
    {
605
        $container = $this->getContainer();
606
        $object = (new Injector($container))->make(DateTimeImmutable::class);
607
        $this->assertInstanceOf(DateTimeImmutable::class, $object);
608
    }
609
610
    public function testMakeAbstractClass(): void
611
    {
612
        $container = $this->getContainer();
613
        $this->expectException(\InvalidArgumentException::class);
614
        $this->expectExceptionMessageMatches('/not instantiable/');
615
        (new Injector($container))->make(LightEngine::class);
616
    }
617
618
    public function testMakeInterface(): void
619
    {
620
        $container = $this->getContainer();
621
        $this->expectException(\InvalidArgumentException::class);
622
        $this->expectExceptionMessageMatches('/not instantiable/');
623
        (new Injector($container))->make(EngineInterface::class);
624
    }
625
626
    public function testMakeWithVariadicFromContainer(): void
627
    {
628
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
629
630
        $object = (new Injector($container))->make(MakeEngineCollector::class, []);
631
632
        $this->assertSame(1, count($object->engines));
633
        $this->assertSame([$container->get(EngineInterface::class)], $object->engines);
634
    }
635
636
    public function testMakeWithVariadicFromArguments(): void
637
    {
638
        $container = $this->getContainer();
639
640
        $values = [new EngineMarkTwo(), new EngineVAZ2101()];
641
        $object = (new Injector($container))->make(MakeEngineCollector::class, $values);
642
643
        $this->assertSame($values, $object->engines);
644
    }
645
646
    public function testMakeWithCustomParam(): void
647
    {
648
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
649
650
        $object = (new Injector($container))
651
            ->make(MakeEngineMatherWithParam::class, [new EngineVAZ2101(), 'parameter' => 'power']);
652
653
        $this->assertNotSame($object->engine1, $object->engine2);
654
        $this->assertInstanceOf(EngineVAZ2101::class, $object->engine1);
655
        $this->assertNotSame(EngineMarkTwo::class, $object->engine2);
656
        $this->assertSame($object->parameter, 'power');
657
    }
658
659
    public function testMakeWithInvalidCustomParam(): void
660
    {
661
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
662
663
        $this->expectException(\TypeError::class);
664
665
        (new Injector($container))->make(MakeEngineMatherWithParam::class, ['parameter' => 100500]);
666
    }
667
668
    // Context binding tests
669
670
    /**
671
     * Bind closure to object
672
     */
673
    public function testClosureBinding(): void
674
    {
675
        $container = $this->getContainer();
676
        $closure = function () {
677
            /** @var EngineInterface $this */
678
            return $this->getName();
679
        };
680
        $engine = new EngineMarkTwo();
681
682
        $engineName = (new Injector($container))->invoke($closure, [], $engine);
683
684
        $this->assertSame('Mark Two', $engineName);
685
    }
686
687
    public function testStaticClosureBindingFail(): void
688
    {
689
        $container = $this->getContainer();
690
        $closure = static function () {
691
            return $this->getName();
692
        };
693
        $engine = new EngineMarkTwo();
694
695
        $this->expectError();
696
        $this->expectErrorMessage('Cannot bind an instance to a static closure');
697
698
        (new Injector($container))->invoke($closure, [], $engine);
699
    }
700
701
    /**
702
     * Binding static closure to static context. Test returning of `self`
703
     */
704
    public function testStaticClosureToStaticContextBinding(): void
705
    {
706
        $container = $this->getContainer();
707
        $closure = static function () {
708
            return self::class;
709
        };
710
711
        $result = (new Injector($container))->invoke($closure, [], EngineMarkTwo::class);
712
713
        $this->assertSame(EngineMarkTwo::class, $result);
714
    }
715
716
    /**
717
     * Binding static closure to static context. Test returning of `self`
718
     */
719
    public function testStaticClosureToStaticContextAndCallPrivateStaticFunction(): void
720
    {
721
        $container = $this->getContainer();
722
        $closure = static function () {
723
            /** @see ContextMethod::staticMethod() */
724
            return self::staticMethod();
0 ignored issues
show
Bug introduced by
The method staticMethod() does not exist on Yiisoft\Injector\Tests\InjectorTest. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

724
            return self::/** @scrutinizer ignore-call */ staticMethod();

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...
725
        };
726
727
        $result = (new Injector($container))->invoke($closure, [], ContextMethod::class);
728
729
        $this->assertSame(ContextMethod::class, $result);
730
    }
731
732
    public function testClosureToInternalContextBinding(): void
733
    {
734
        $container = $this->getContainer();
735
        $closure = function () {
736
            return get_class($this);
737
        };
738
739
        $this->expectError();
740
        $this->expectErrorMessage('Cannot bind closure to scope of internal class DateTime');
741
742
        (new Injector($container))->invoke($closure, [], new DateTime());
743
    }
744
745
    public function testStaticClosureToInternalStaticContextBinding(): void
746
    {
747
        $container = $this->getContainer();
748
        $closure = static function () {
749
            return self::class;
750
        };
751
752
        $this->expectError();
753
        $this->expectErrorMessage('Cannot bind closure to scope of internal class DateTime');
754
755
        (new Injector($container))->invoke($closure, [], DateTime::class);
756
    }
757
758
    /**
759
     * Bind closure with access to protected property
760
     */
761
    public function testClosureBindingWithAccessToProtectedProperty(): void
762
    {
763
        $container = $this->getContainer();
764
        $getPower = fn () => $this->power;
0 ignored issues
show
Bug Best Practice introduced by
The property power does not exist on Yiisoft\Injector\Tests\InjectorTest. Did you maybe forget to declare it?
Loading history...
765
        $engine = new EngineVAZ2101();
766
767
        $power = (new Injector($container))->invoke($getPower, [], $engine);
768
769
        /** @see EngineVAZ2101::$power */
770
        $this->assertSame(59, $power);
771
    }
772
773
    /**
774
     * Bind closure with access to protected and private methods
775
     */
776
    public function testInvokePrivateMethods(): void
777
    {
778
        $container = $this->getContainer();
779
        /** @see ContextMethod::privateMethod() */
780
        /** @see ContextMethod::protectedMethod() */
781
        $closure = fn () => $this->privateMethod() && $this->protectedMethod();
0 ignored issues
show
Bug introduced by
The method privateMethod() does not exist on Yiisoft\Injector\Tests\InjectorTest. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

781
        $closure = fn () => $this->/** @scrutinizer ignore-call */ privateMethod() && $this->protectedMethod();

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...
Bug introduced by
The method protectedMethod() does not exist on Yiisoft\Injector\Tests\InjectorTest. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

781
        $closure = fn () => $this->privateMethod() && $this->/** @scrutinizer ignore-call */ protectedMethod();

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...
782
        $context = new ContextMethod();
783
784
        $result = (new Injector($container))->invoke($closure, [], $context);
785
786
        $this->assertTrue($result);
787
    }
788
789
    /**
790
     * Rebinding static method to other class object.
791
     */
792
    public function testBindStaticMethodToOtherClassObject(): void
793
    {
794
        $container = $this->getContainer();
795
        $engine = new EngineMarkTwo();
796
797
        $this->expectError();
798
        $this->expectErrorMessage('Cannot bind an instance to a static closure');
799
800
        (new Injector($container))->invoke([ContextMethod::class, 'publicStaticMethod'], [], $engine);
801
    }
802
803
    /**
804
     * Rebinding static method to other static context.
805
     */
806
    public function testBindStaticMethodToOtherClass(): void
807
    {
808
        $this->expectError();
809
        $this->expectErrorMessageMatches('/^Cannot rebind scope/');
810
811
        (new Injector($this->getContainer()))
812
            ->invoke([ContextMethod::class, 'publicStaticMethod'], [], EngineMarkTwo::class);
813
    }
814
815
    /**
816
     * Rebinding method to other object.
817
     */
818
    public function testInjectorInvokeBinding(): void
819
    {
820
        $container = $this->getContainer();
821
        $injector = new Injector($container);
822
        $context = new ContextMethod();
823
824
        $this->expectError();
825
        $this->expectErrorMessageMatches('/^Cannot bind method/');
826
827
        (new Injector($container))->invoke([$injector, 'invoke'], [
828
            /** @see ContextMethod::privateMethod() */
829
            'callable' => [$context, 'privateMethod'],
830
        ], $context);
831
    }
832
833
    private function getContainer(array $definitions = []): ContainerInterface
834
    {
835
        return new class($definitions) implements ContainerInterface {
836
            private array $definitions = [];
837
            public function __construct(array $definitions = [])
838
            {
839
                $this->definitions = $definitions;
840
            }
841
            public function get($id)
842
            {
843
                if (!$this->has($id)) {
844
                    throw new class() extends \Exception implements NotFoundExceptionInterface {
845
                    };
846
                }
847
                return $this->definitions[$id];
848
            }
849
            public function has($id)
850
            {
851
                return array_key_exists($id, $this->definitions);
852
            }
853
        };
854
    }
855
}
856