Test Failed
Push — main ( 917e6f...6ea8e5 )
by Chema
04:26 queued 01:24
created

ClassContainerTest::test_get_bindings()

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 5
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GacelaTest\Unit;
6
7
use ArrayObject;
8
use Gacela\Container\Container;
9
use Gacela\Container\Exception\CircularDependencyException;
10
use Gacela\Container\Exception\ContainerException;
11
use Gacela\Container\Exception\DependencyInvalidArgumentException;
12
use GacelaTest\Fake\CircularA;
13
use GacelaTest\Fake\CircularC;
14
use GacelaTest\Fake\ClassWithDependencyWithoutDependencies;
15
use GacelaTest\Fake\ClassWithInnerObjectDependencies;
16
use GacelaTest\Fake\ClassWithInterfaceDependencies;
17
use GacelaTest\Fake\ClassWithObjectDependencies;
18
use GacelaTest\Fake\ClassWithoutDependencies;
19
use GacelaTest\Fake\ClassWithRelationship;
20
use GacelaTest\Fake\ControllerUsingService;
21
use GacelaTest\Fake\DatabaseRepository;
22
use GacelaTest\Fake\Person;
23
use GacelaTest\Fake\PersonInterface;
24
use GacelaTest\Fake\RepositoryInterface;
25
use GacelaTest\Fake\ServiceWithRepository;
26
use PHPUnit\Framework\TestCase;
27
28
final class ClassContainerTest extends TestCase
29
{
30
    public function test_static_create_without_dependencies(): void
31
    {
32
        $actual = Container::create(ClassWithoutDependencies::class);
33
34
        self::assertEquals(new ClassWithoutDependencies(), $actual);
35
    }
36
37
    public function test_static_create_class_with_inner_dependencies_without_dependencies(): void
38
    {
39
        $actual = Container::create(ClassWithDependencyWithoutDependencies::class);
40
41
        self::assertEquals(new ClassWithDependencyWithoutDependencies(new ClassWithoutDependencies()), $actual);
42
    }
43
44
    public function test_static_create_class_with_inner_dependencies_with_many_dependencies(): void
45
    {
46
        $actual = Container::create(ClassWithInnerObjectDependencies::class);
47
48
        self::assertEquals(new ClassWithInnerObjectDependencies(new ClassWithRelationship(new Person(), new Person())), $actual);
49
    }
50
51
    public function test_static_create_with_dependencies(): void
52
    {
53
        $actual = Container::create(ClassWithObjectDependencies::class);
54
55
        self::assertEquals(new ClassWithObjectDependencies(new Person()), $actual);
56
    }
57
58
    public function test_static_create_with_many_dependencies(): void
59
    {
60
        $actual = Container::create(ClassWithRelationship::class);
61
62
        self::assertEquals(new ClassWithRelationship(new Person(), new Person()), $actual);
63
    }
64
65
    public function test_without_dependencies(): void
66
    {
67
        $container = new Container();
68
        $actual = $container->get(ClassWithoutDependencies::class);
69
70
        self::assertEquals(new ClassWithoutDependencies(), $actual);
71
    }
72
73
    public function test_object_with_resolvable_dependencies(): void
74
    {
75
        $container = new Container();
76
        $actual = $container->get(ClassWithObjectDependencies::class);
77
78
        self::assertEquals(new ClassWithObjectDependencies(new Person()), $actual);
79
    }
80
81
    public function test_interface_dependency(): void
82
    {
83
        $container = new Container([
84
            PersonInterface::class => Person::class,
85
        ]);
86
        $actual = $container->get(ClassWithObjectDependencies::class);
87
88
        self::assertEquals(new ClassWithObjectDependencies(new Person()), $actual);
89
    }
90
91
    public function test_use_mapped_interface_dependency(): void
92
    {
93
        $person = new Person();
94
        $person->name = 'anything';
95
96
        $container = new Container([
97
            PersonInterface::class => $person,
98
        ]);
99
        $actual = $container->get(ClassWithInterfaceDependencies::class);
100
101
        self::assertEquals(new ClassWithInterfaceDependencies($person), $actual);
102
    }
103
104
    public function test_has_not_existing_class(): void
105
    {
106
        $container = new Container();
107
        $actual = $container->has(InexistentClass::class);
108
109
        self::assertFalse($actual);
110
    }
111
112
    public function test_resolve_object_from_interface(): void
113
    {
114
        $person = new Person();
115
        $person->name = 'person-name';
116
117
        $container = new Container([
118
            PersonInterface::class => $person,
119
        ]);
120
        $resolvedPerson = $container->get(PersonInterface::class);
121
122
        self::assertSame($resolvedPerson, $person);
123
    }
124
125
    public function test_resolve_new_object(): void
126
    {
127
        // We are registering 'PersonInterface::class', but 'Person::class' was not.
128
        // As result, a 'new Person()' will be resolved.
129
        $person = new Person();
130
        $person->name = 'person-name';
131
132
        $container = new Container([
133
            PersonInterface::class => $person,
134
        ]);
135
        $resolvedPerson = $container->get(Person::class);
136
137
        self::assertEquals($resolvedPerson, new Person()); // different objects!
138
    }
139
140
    public function test_interface_not_registered_returns_null(): void
141
    {
142
        $container = new Container([
143
            Person::class => new Person(),
144
        ]);
145
        $resolvedPerson = $container->get(PersonInterface::class);
146
147
        self::assertNull($resolvedPerson);
148
    }
149
150
    public function test_resolve_object_from_classname(): void
151
    {
152
        $container = new Container([
153
            PersonInterface::class => Person::class,
154
        ]);
155
        $resolvedPerson = $container->get(PersonInterface::class);
156
157
        self::assertEquals($resolvedPerson, new Person());
158
    }
159
160
    public function test_resolve_object_from_instance_in_a_callable(): void
161
    {
162
        $person = new Person();
163
        $person->name = 'person-name';
164
165
        $container = new Container([
166
            PersonInterface::class => static fn () => $person,
167
        ]);
168
        $resolvedPerson = $container->get(PersonInterface::class);
169
170
        self::assertEquals($resolvedPerson, $person);
171
    }
172
173
    public function test_resolve_object_from_callable_classname_in_a_callable(): void
174
    {
175
        $container = new Container([
176
            PersonInterface::class => static fn () => Person::class,
177
        ]);
178
        $resolvedPerson = $container->get(PersonInterface::class);
179
180
        self::assertEquals($resolvedPerson, new Person());
181
    }
182
183
    public function test_get_non_existing_service(): void
184
    {
185
        $container = new Container();
186
187
        self::assertNull($container->get('unknown-service_name'));
188
    }
189
190
    public function test_has_service(): void
191
    {
192
        $container = new Container();
193
        $container->set('service_name', 'value');
194
195
        self::assertTrue($container->has('service_name'));
196
        self::assertFalse($container->has('unknown-service_name'));
197
    }
198
199
    public function test_remove_existing_service(): void
200
    {
201
        $container = new Container();
202
        $container->set('service_name', 'value');
203
        $container->remove('service_name');
204
205
        self::assertNull($container->get('service_name'));
206
    }
207
208
    public function test_resolve_service_as_raw_string(): void
209
    {
210
        $container = new Container();
211
        $container->set('service_name', 'value');
212
213
        $resolvedService = $container->get('service_name');
214
        self::assertSame('value', $resolvedService);
215
216
        $cachedResolvedService = $container->get('service_name');
217
        self::assertSame('value', $cachedResolvedService);
218
    }
219
220
    public function test_resolve_service_as_function(): void
221
    {
222
        $container = new Container();
223
        $container->set('service_name', static fn () => 'value');
224
225
        $resolvedService = $container->get('service_name');
226
        self::assertSame('value', $resolvedService);
227
228
        $cachedResolvedService = $container->get('service_name');
229
        self::assertSame('value', $cachedResolvedService);
230
    }
231
232
    public function test_resolve_service_as_callable_class(): void
233
    {
234
        $container = new Container();
235
        $container->set(
236
            'service_name',
237
            new class() {
238
                public function __invoke(): string
239
                {
240
                    return 'value';
241
                }
242
            },
243
        );
244
245
        $resolvedService = $container->get('service_name');
246
        self::assertSame('value', $resolvedService);
247
248
        $cachedResolvedService = $container->get('service_name');
249
        self::assertSame('value', $cachedResolvedService);
250
    }
251
252
    public function test_resolve_non_factory_service_with_random(): void
253
    {
254
        $container = new Container();
255
        $container->set(
256
            'service_name',
257
            static fn () => 'value_' . random_int(0, PHP_INT_MAX),
258
        );
259
260
        self::assertSame(
261
            $container->get('service_name'),
262
            $container->get('service_name'),
263
        );
264
    }
265
266
    public function test_resolve_factory_service_with_random(): void
267
    {
268
        $container = new Container();
269
        $container->set(
270
            'service_name',
271
            $container->factory(
272
                static fn () => 'value_' . random_int(0, PHP_INT_MAX),
273
            ),
274
        );
275
276
        self::assertNotSame(
277
            $container->get('service_name'),
278
            $container->get('service_name'),
279
        );
280
    }
281
282
    public function test_extend_existing_factory_service(): void
283
    {
284
        $container = new Container();
285
        $container->set('n3', 3);
286
        $container->set(
287
            'service_name',
288
            $container->factory(
289
                static fn () => new ArrayObject([1, 2]),
290
            ),
291
        );
292
293
        $container->extend(
294
            'service_name',
295
            static function (ArrayObject $arrayObject, Container $container): ArrayObject {
296
                $arrayObject->append($container->get('n3'));
297
298
                return $arrayObject;
299
            },
300
        );
301
302
        /** @var ArrayObject $first */
303
        $first = $container->get('service_name');
304
        /** @var ArrayObject $second */
305
        $second = $container->get('service_name');
306
307
        self::assertEquals(new ArrayObject([1, 2, 3]), $first);
308
        self::assertEquals(new ArrayObject([1, 2, 3]), $second);
309
        self::assertNotSame($first, $second);
310
    }
311
312
    public function test_extend_existing_callable_service(): void
313
    {
314
        $container = new Container();
315
        $container->set('n3', 3);
316
        $container->set('service_name', static fn () => new ArrayObject([1, 2]));
317
318
        $container->extend(
319
            'service_name',
320
            static function (ArrayObject $arrayObject, Container $container) {
321
                $arrayObject->append($container->get('n3'));
322
                return $arrayObject;
323
            },
324
        );
325
326
        $container->extend(
327
            'service_name',
328
            static fn (ArrayObject $arrayObject) => $arrayObject->append(4),
329
        );
330
331
        /** @var ArrayObject $actual */
332
        $actual = $container->get('service_name');
333
334
        self::assertEquals(new ArrayObject([1, 2, 3, 4]), $actual);
335
    }
336
337
    public function test_set_after_extend(): void
338
    {
339
        $container = new Container();
340
341
        $container->extend(
342
            'service_name',
343
            static fn (ArrayObject $arrayObject) => $arrayObject->append(3),
344
        );
345
346
        $container->set('service_name', static fn () => new ArrayObject([1, 2]));
347
348
        /** @var ArrayObject $actual */
349
        $actual = $container->get('service_name');
350
351
        self::assertEquals(new ArrayObject([1, 2, 3]), $actual);
352
    }
353
354
    public function test_extend_existing_object_service(): void
355
    {
356
        $container = new Container();
357
        $container->set('n3', 3);
358
        $container->set('service_name', new ArrayObject([1, 2]));
359
360
        $container->extend(
361
            'service_name',
362
            static function (ArrayObject $arrayObject, Container $container) {
363
                $arrayObject->append($container->get('n3'));
364
                return $arrayObject;
365
            },
366
        );
367
368
        $container->extend(
369
            'service_name',
370
            static function (ArrayObject $arrayObject): void {
371
                $arrayObject->append(4);
372
            },
373
        );
374
375
        /** @var ArrayObject $actual */
376
        $actual = $container->get('service_name');
377
378
        self::assertEquals(new ArrayObject([1, 2, 3, 4]), $actual);
379
    }
380
381
    public function test_extend_existing_array_service(): void
382
    {
383
        $container = new Container();
384
        $container->set('service_name', [1, 2]);
385
386
        $container->extend(
387
            'service_name',
388
            static function (array $arrayObject): array {
389
                $arrayObject[] = 3;
390
                return $arrayObject;
391
            },
392
        );
393
394
        $container->extend(
395
            'service_name',
396
            static function (array &$arrayObject): void {
397
                $arrayObject[] = 4;
398
            },
399
        );
400
401
        /** @var ArrayObject $actual */
402
        $actual = $container->get('service_name');
403
404
        self::assertEquals([1, 2, 3, 4], $actual);
405
    }
406
407
    public function test_extend_non_existing_service(): void
408
    {
409
        $container = new Container();
410
        $container->extend('service_name', static fn () => '');
411
412
        self::assertNull($container->get('service_name'));
413
    }
414
415
    public function test_service_not_extendable(): void
416
    {
417
        $container = new Container();
418
        $container->set('service_name', 'raw string');
419
420
        $this->expectExceptionObject(ContainerException::instanceNotExtendable());
421
        $container->extend('service_name', static fn (string $str) => $str);
422
    }
423
424
    public function test_extend_existing_used_object_service_is_allowed(): void
425
    {
426
        $container = new Container();
427
        $container->set('service_name', new ArrayObject([1, 2]));
428
        $container->get('service_name'); // and get frozen
429
430
        $this->expectExceptionObject(ContainerException::frozenInstanceExtend('service_name'));
431
432
        $container->extend(
433
            'service_name',
434
            static fn (ArrayObject $arrayObject) => $arrayObject->append(3),
435
        );
436
    }
437
438
    public function test_extend_existing_used_callable_service_then_error(): void
439
    {
440
        $container = new Container();
441
        $container->set('service_name', static fn () => new ArrayObject([1, 2]));
442
        $container->get('service_name'); // and get frozen
443
444
        $this->expectExceptionObject(ContainerException::frozenInstanceExtend('service_name'));
445
446
        $container->extend(
447
            'service_name',
448
            static fn (ArrayObject $arrayObject) => $arrayObject->append(3),
449
        );
450
    }
451
452
    public function test_extend_later_existing_frozen_object_service_then_error(): void
453
    {
454
        $container = new Container();
455
        $container->extend(
456
            'service_name',
457
            static fn (ArrayObject $arrayObject) => $arrayObject->append(3),
458
        );
459
460
        $container->set('service_name', new ArrayObject([1, 2]));
461
        $container->get('service_name'); // and get frozen
462
463
        $this->expectExceptionObject(ContainerException::frozenInstanceExtend('service_name'));
464
465
        $container->extend(
466
            'service_name',
467
            static fn (ArrayObject $arrayObject) => $arrayObject->append(4),
468
        );
469
    }
470
471
    public function test_extend_later_existing_frozen_callable_service_then_error(): void
472
    {
473
        $container = new Container();
474
        $container->extend(
475
            'service_name',
476
            static fn (ArrayObject $arrayObject) => $arrayObject->append(3),
477
        );
478
479
        $container->set('service_name', static fn () => new ArrayObject([1, 2]));
480
        $container->get('service_name'); // and get frozen
481
482
        $this->expectExceptionObject(ContainerException::frozenInstanceExtend('service_name'));
483
484
        $container->extend(
485
            'service_name',
486
            static fn (ArrayObject $arrayObject) => $arrayObject->append(4),
487
        );
488
    }
489
490
    public function test_set_existing_frozen_service(): void
491
    {
492
        $container = new Container();
493
        $container->set('service_name', static fn () => new ArrayObject([1, 2]));
494
        $container->get('service_name'); // and get frozen
495
496
        $this->expectExceptionObject(ContainerException::frozenInstanceOverride('service_name'));
497
        $container->set('service_name', static fn () => new ArrayObject([3]));
498
    }
499
500
    public function test_protect_service_is_not_resolved(): void
501
    {
502
        $container = new Container();
503
        $service = static fn () => 'value';
504
        $container->set('service_name', $container->protect($service));
505
506
        self::assertSame($service, $container->get('service_name'));
507
    }
508
509
    public function test_protect_service_cannot_be_extended(): void
510
    {
511
        $container = new Container();
512
        $container->set(
513
            'service_name',
514
            $container->protect(static fn () => new ArrayObject([1, 2])),
515
        );
516
517
        $this->expectExceptionObject(ContainerException::instanceProtected('service_name'));
518
519
        $container->extend(
520
            'service_name',
521
            static fn (ArrayObject $arrayObject) => $arrayObject,
522
        );
523
    }
524
525
    public function test_circular_dependency_two_classes(): void
526
    {
527
        $this->expectException(CircularDependencyException::class);
528
        $this->expectExceptionMessage('Circular dependency detected: GacelaTest\Fake\CircularB -> GacelaTest\Fake\CircularA -> GacelaTest\Fake\CircularB');
529
530
        Container::create(CircularA::class);
531
    }
532
533
    public function test_circular_dependency_three_classes(): void
534
    {
535
        $this->expectException(CircularDependencyException::class);
536
        $this->expectExceptionMessage('Circular dependency detected: GacelaTest\Fake\CircularD -> GacelaTest\Fake\CircularE -> GacelaTest\Fake\CircularC -> GacelaTest\Fake\CircularD');
537
538
        Container::create(CircularC::class);
539
    }
540
541
    public function test_get_registered_services(): void
542
    {
543
        $container = new Container();
544
        self::assertSame([], $container->getRegisteredServices());
545
546
        $container->set('service1', 'value1');
547
        $container->set('service2', 'value2');
548
549
        self::assertSame(['service1', 'service2'], $container->getRegisteredServices());
550
    }
551
552
    public function test_is_factory(): void
553
    {
554
        $container = new Container();
555
        $factory = $container->factory(static fn () => new ArrayObject());
556
        $nonFactory = static fn () => new ArrayObject();
557
558
        $container->set('factory_service', $factory);
559
        $container->set('non_factory_service', $nonFactory);
560
561
        self::assertTrue($container->isFactory('factory_service'));
562
        self::assertFalse($container->isFactory('non_factory_service'));
563
        self::assertFalse($container->isFactory('non_existent'));
564
    }
565
566
    public function test_is_frozen(): void
567
    {
568
        $container = new Container();
569
        $container->set('service', 'value');
570
571
        self::assertFalse($container->isFrozen('service'));
572
573
        $container->get('service');
574
575
        self::assertTrue($container->isFrozen('service'));
576
    }
577
578
    public function test_get_bindings(): void
579
    {
580
        $bindings = [
581
            PersonInterface::class => Person::class,
582
            'some_service' => static fn () => 'value',
583
        ];
584
585
        $container = new Container($bindings);
586
587
        self::assertSame($bindings, $container->getBindings());
588
    }
589
590
    public function test_warm_up_caches_dependencies(): void
591
    {
592
        $container = new Container();
593
594
        // Warm up should pre-resolve dependencies
595
        $container->warmUp([
596
            ClassWithObjectDependencies::class,
597
            ClassWithRelationship::class,
598
            Person::class,
599
        ]);
600
601
        // After warm-up, instantiation should be faster (dependencies cached)
602
        $result = $container->get(ClassWithObjectDependencies::class);
603
604
        self::assertInstanceOf(ClassWithObjectDependencies::class, $result);
605
        self::assertInstanceOf(Person::class, $result->person);
606
    }
607
608
    public function test_warm_up_skips_non_existent_classes(): void
609
    {
610
        $container = new Container();
611
612
        // Should not throw exception for non-existent class
613
        $container->warmUp([
614
            'NonExistentClass',
615
            Person::class,
616
        ]);
617
618
        self::assertInstanceOf(Person::class, $container->get(Person::class));
619
    }
620
621
    public function test_interface_binding_with_constructor_dependencies(): void
622
    {
623
        // This test ensures that when an interface is bound to a concrete implementation
624
        // that has constructor dependencies, those dependencies are properly resolved
625
        $bindings = [
626
            RepositoryInterface::class => DatabaseRepository::class,
627
        ];
628
629
        $container = new Container($bindings);
630
        $service = $container->get(ServiceWithRepository::class);
631
632
        self::assertInstanceOf(ServiceWithRepository::class, $service);
633
        self::assertInstanceOf(DatabaseRepository::class, $service->repository);
634
        self::assertInstanceOf(Person::class, $service->repository->person);
635
        self::assertInstanceOf(ClassWithoutDependencies::class, $service->repository->config);
636
    }
637
638
    public function test_alias_for_service(): void
639
    {
640
        $container = new Container();
641
        $container->set(Person::class, new Person());
642
        $container->alias('person', Person::class);
643
644
        self::assertTrue($container->has('person'));
645
        self::assertInstanceOf(Person::class, $container->get('person'));
646
        self::assertSame($container->get(Person::class), $container->get('person'));
647
    }
648
649
    public function test_alias_with_factory(): void
650
    {
651
        $container = new Container();
652
        $factory = $container->factory(fn() => new Person());
653
        $container->set(Person::class, $factory);
654
        $container->alias('person', Person::class);
655
656
        self::assertTrue($container->isFactory(Person::class));
657
        self::assertTrue($container->isFactory('person'));
658
659
        $p1 = $container->get('person');
660
        $p2 = $container->get('person');
661
662
        self::assertInstanceOf(Person::class, $p1);
663
        self::assertInstanceOf(Person::class, $p2);
664
        self::assertNotSame($p1, $p2);
665
    }
666
667
    public function test_alias_frozen_state(): void
668
    {
669
        $container = new Container();
670
        $container->set('service', 'value');
671
        $container->alias('svc', 'service');
672
673
        self::assertFalse($container->isFrozen('service'));
674
        self::assertFalse($container->isFrozen('svc'));
675
676
        $container->get('service');
677
678
        self::assertTrue($container->isFrozen('service'));
679
        self::assertTrue($container->isFrozen('svc'));
680
    }
681
682
    public function test_remove_via_alias(): void
683
    {
684
        $container = new Container();
685
        $container->set('service', 'value');
686
        $container->alias('svc', 'service');
687
688
        self::assertTrue($container->has('svc'));
689
690
        $container->remove('svc');
691
692
        self::assertFalse($container->has('service'));
693
        self::assertFalse($container->has('svc'));
694
    }
695
696
    public function test_get_dependency_tree_simple(): void
697
    {
698
        $container = new Container();
699
        $tree = $container->getDependencyTree(ClassWithObjectDependencies::class);
700
701
        self::assertContains(Person::class, $tree);
702
    }
703
704
    public function test_get_dependency_tree_nested(): void
705
    {
706
        $container = new Container();
707
        $tree = $container->getDependencyTree(ClassWithInnerObjectDependencies::class);
708
709
        self::assertContains(ClassWithRelationship::class, $tree);
710
        self::assertContains(Person::class, $tree);
711
    }
712
713
    public function test_get_dependency_tree_with_bindings(): void
714
    {
715
        $bindings = [
716
            RepositoryInterface::class => DatabaseRepository::class,
717
        ];
718
719
        $container = new Container($bindings);
720
        $tree = $container->getDependencyTree(ServiceWithRepository::class);
721
722
        self::assertContains(DatabaseRepository::class, $tree);
723
        self::assertContains(Person::class, $tree);
724
        self::assertContains(ClassWithoutDependencies::class, $tree);
725
    }
726
727
    public function test_get_dependency_tree_no_dependencies(): void
728
    {
729
        $container = new Container();
730
        $tree = $container->getDependencyTree(ClassWithoutDependencies::class);
731
732
        self::assertSame([], $tree);
733
    }
734
735
    public function test_error_includes_resolution_chain(): void
736
    {
737
        $this->expectException(DependencyInvalidArgumentException::class);
738
        $this->expectExceptionMessageMatches('/Resolution chain:.*ServiceWithScalarDependency/');
739
740
        $container = new Container();
741
        $container->get(ControllerUsingService::class);
742
    }
743
}
744