Passed
Pull Request — master (#5)
by
unknown
16:01 queued 14:42
created

InjectorTest.php$1 ➔ getContainer()   A

Complexity

Conditions 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
cc 1
rs 9.6333

3 Methods

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

249
        $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...
250
            return $engine->getName();
251
        };
252
253
        $injector = new Injector($container);
254
255
        $this->expectException(MissingRequiredArgumentException::class);
256
        $injector->invoke($getEngineName);
257
    }
258
259
    public function testMissingRequiredNotTypedParameter(): void
260
    {
261
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
262
263
        $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

263
        $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...
264
            return $engine->getName();
265
        };
266
        $injector = new Injector($container);
267
268
        $this->expectException(MissingRequiredArgumentException::class);
269
270
        $injector->invoke($getEngineName);
271
    }
272
273
    public function testNotFoundException(): void
274
    {
275
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
276
277
        $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

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

372
        $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...
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

372
        $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...
373
374
        $result = (new Injector($container))->invoke($callable, [new DateTimeImmutable(), new DateTimeImmutable()]);
375
376
        $this->assertSame(4, $result);
377
    }
378
379
    public function testWrongNamedParam(): void
380
    {
381
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
382
383
        $callable = fn (EngineInterface $engine) => $engine;
384
385
        $this->expectException(\Throwable::class);
386
387
        (new Injector($container))->invoke($callable, ['engine' => new DateTimeImmutable()]);
388
    }
389
390
    public function testArrayArgumentWithUnnamedType(): void
391
    {
392
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
393
394
        $callable = fn (array $arg) => $arg;
395
396
        $this->expectException(MissingRequiredArgumentException::class);
397
398
        (new Injector($container))->invoke($callable, [['test']]);
399
    }
400
401
    public function testCallableArgumentWithUnnamedType(): void
402
    {
403
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
404
405
        $callable = fn (callable $arg) => $arg();
406
407
        $this->expectException(MissingRequiredArgumentException::class);
408
409
        (new Injector($container))->invoke($callable, [fn () => true]);
410
    }
411
412
    public function testIterableArgumentWithUnnamedType(): void
413
    {
414
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
415
416
        $callable = fn (iterable $arg) => $arg;
417
418
        $this->expectException(MissingRequiredArgumentException::class);
419
420
        (new Injector($container))->invoke($callable, [new \SplStack()]);
421
    }
422
423
    public function testUnnamedScalarParam(): void
424
    {
425
        $container = $this->getContainer();
426
427
        $getEngineName = fn () => 42;
428
429
        $this->expectException(InvalidArgumentException::class);
430
431
        (new Injector($container))->invoke($getEngineName, ['test']);
432
    }
433
434
    /**
435
     * Constructor method not defined
436
     */
437
    public function testMakeWithoutConstructor(): void
438
    {
439
        $container = $this->getContainer();
440
441
        $object = (new Injector($container))->make(MakeNoConstructor::class);
442
443
        $this->assertInstanceOf(MakeNoConstructor::class, $object);
444
    }
445
446
    /**
447
     * Constructor without arguments
448
     */
449
    public function testMakeWithoutArguments(): void
450
    {
451
        $container = $this->getContainer();
452
453
        $object = (new Injector($container))->make(MakeEmptyConstructor::class);
454
455
        $this->assertInstanceOf(MakeEmptyConstructor::class, $object);
456
    }
457
458
    /**
459
     * Private constructor unavailable from Injector context
460
     */
461
    public function testMakeWithPrivateConstructor(): void
462
    {
463
        $container = $this->getContainer();
464
465
        $this->expectException(\InvalidArgumentException::class);
466
        $this->expectExceptionMessageMatches('/not instantiable/');
467
468
        (new Injector($container))->make(MakePrivateConstructor::class);
469
    }
470
471
    public function testMakeInvalidClass(): void
472
    {
473
        $container = $this->getContainer();
474
475
        $this->expectException(\ReflectionException::class);
476
        $this->expectExceptionMessageMatches('/does not exist/');
477
478
        (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...
479
    }
480
481
    public function testMakeInternalClass(): void
482
    {
483
        $container = $this->getContainer();
484
        $object = (new Injector($container))->make(DateTimeImmutable::class);
485
        $this->assertInstanceOf(DateTimeImmutable::class, $object);
486
    }
487
488
    public function testMakeAbstractClass(): void
489
    {
490
        $container = $this->getContainer();
491
        $this->expectException(\InvalidArgumentException::class);
492
        $this->expectExceptionMessageMatches('/not instantiable/');
493
        (new Injector($container))->make(LightEngine::class);
494
    }
495
496
    public function testMakeInterface(): void
497
    {
498
        $container = $this->getContainer();
499
        $this->expectException(\InvalidArgumentException::class);
500
        $this->expectExceptionMessageMatches('/not instantiable/');
501
        (new Injector($container))->make(EngineInterface::class);
502
    }
503
504
    public function testMakeWithVariadicFromContainer(): void
505
    {
506
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
507
508
        $object = (new Injector($container))->make(MakeEngineCollector::class, []);
509
510
        $this->assertSame(1, count($object->engines));
511
        $this->assertSame([$container->get(EngineInterface::class)], $object->engines);
512
    }
513
514
    public function testMakeWithVariadicFromArguments(): void
515
    {
516
        $container = $this->getContainer();
517
518
        $values = [new EngineMarkTwo(), new EngineVAZ2101()];
519
        $object = (new Injector($container))->make(MakeEngineCollector::class, $values);
520
521
        $this->assertSame($values, $object->engines);
522
    }
523
524
    public function testMakeWithCustomParam(): void
525
    {
526
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
527
528
        $object = (new Injector($container))
529
            ->make(MakeEngineMatherWithParam::class, [new EngineVAZ2101(), 'parameter' => 'power']);
530
531
        $this->assertNotSame($object->engine1, $object->engine2);
532
        $this->assertInstanceOf(EngineVAZ2101::class, $object->engine1);
533
        $this->assertNotSame(EngineMarkTwo::class, $object->engine2);
534
        $this->assertSame($object->parameter, 'power');
535
    }
536
537
    public function testMakeWithInvalidCustomParam(): void
538
    {
539
        $container = $this->getContainer([EngineInterface::class => new EngineMarkTwo()]);
540
541
        $this->expectException(\TypeError::class);
542
543
        (new Injector($container))->make(MakeEngineMatherWithParam::class, ['parameter' => 100500]);
544
    }
545
546
    private function getContainer(array $definitions = []): ContainerInterface
547
    {
548
        return new class($definitions) implements ContainerInterface {
549
            private array $definitions = [];
550
            public function __construct(array $definitions = [])
551
            {
552
                $this->definitions = $definitions;
553
            }
554
            public function get($id)
555
            {
556
                if (!$this->has($id)) {
557
                    throw new class() extends \Exception implements NotFoundExceptionInterface {
558
                    };
559
                }
560
                return $this->definitions[$id];
561
            }
562
            public function has($id)
563
            {
564
                return array_key_exists($id, $this->definitions);
565
            }
566
        };
567
    }
568
}
569