Passed
Pull Request — master (#4)
by Alexander
01:23
created

InjectorTest::testMakeWithCustomParam()

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

375
        $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

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