Passed
Push — master ( 261e75...7da9e7 )
by Alex
36s queued 11s
created

anonymous//test/phpunit/ContainerTest.php$0   A

Complexity

Total Complexity 2

Size/Duplication

Total Lines 14
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 2
c 0
b 0
f 0
dl 0
loc 14
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArpTest\Container;
6
7
use Arp\Container\Container;
8
use Arp\Container\ContainerInterface;
9
use Arp\Container\Exception\CircularDependencyException;
10
use Arp\Container\Exception\ContainerException;
11
use Arp\Container\Exception\InvalidArgumentException;
12
use Arp\Container\Exception\NotFoundException;
13
use Arp\Container\Provider\Exception\ServiceProviderException;
14
use Arp\Container\Provider\ServiceProviderInterface;
15
use PHPUnit\Framework\MockObject\MockObject;
16
use PHPUnit\Framework\TestCase;
17
use Psr\Container\ContainerExceptionInterface;
18
19
/**
20
 * @covers  \Arp\Container\Container
21
 *
22
 * @author  Alex Patterson <[email protected]>
23
 * @package ArpTest\ContainerArray
24
 */
25
final class ContainerTest extends TestCase
26
{
27
    /**
28
     * Assert that the Container implements ContainerInterface.
29
     */
30
    public function testImplementsContainerInterface(): void
31
    {
32
        $container = new Container();
33
34
        $this->assertInstanceOf(ContainerInterface::class, $container);
35
    }
36
37
    /**
38
     * Assert that has() will return true for a service that has been set on the container
39
     *
40
     * @throws ContainerExceptionInterface
41
     */
42
    public function testHasWillAssertBooleanTrueForRegisteredService(): void
43
    {
44
        $container = new Container();
45
46
        $name = \stdClass::class;
47
        $service = new \stdClass();
48
49
        $this->assertFalse($container->has($name));
50
51
        $container->set($name, $service);
52
53
        $this->assertTrue($container->has($name));
54
    }
55
56
    /**
57
     * Assert that has() will return FALSE for a service that has NOT been set on the container
58
     *
59
     * @throws ContainerExceptionInterface
60
     */
61
    public function testHasWillAssertBooleanFalseForNonRegisteredService(): void
62
    {
63
        $container = new Container();
64
65
        $name = \stdClass::class;
66
67
        $this->assertFalse($container->has($name));
68
    }
69
70
    /**
71
     * Assert that a value can be set and returned from the container.
72
     *
73
     * @throws CircularDependencyException
74
     * @throws ContainerException
75
     * @throws NotFoundException
76
     */
77
    public function testGetWillReturnAServiceByName(): void
78
    {
79
        $container = new Container();
80
81
        $name = \stdClass::class;
82
        $service = new \stdClass();
83
84
        $container->set($name, $service);
85
86
        $this->assertSame($service, $container->get($name));
87
    }
88
89
    /**
90
     * Assert that calls to get with a registered service alias will return the named service
91
     *
92
     * @throws ContainerExceptionInterface
93
     */
94
    public function testGetWillReturnAServiceByAliasName(): void
95
    {
96
        $container = new Container();
97
98
        $alias = 'foo';
99
        $name = \stdClass::class;
100
        $service = new \stdClass();
101
102
        $container->set($name, $service);
103
        $container->setAlias($alias, $name);
104
105
        $this->assertSame($service, $container->get($alias));
106
    }
107
108
    /**
109
     * Assert that our README code example is working
110
     *
111
     * @throws CircularDependencyException
112
     * @throws ContainerException
113
     * @throws NotFoundException
114
     */
115
    public function testTheDateOfTodayExampleInReadMe(): void
116
    {
117
        $container = new Container();
118
119
        $name = 'TodaysDate';
120
        $service = new \DateTime('today');
121
        $container->set($name, $service);
122
123
        $this->assertSame($service, $container->get($name));
124
    }
125
126
    /**
127
     * Assert that a ContainerException is thrown when trying to register a service alias for an unregistered service
128
     *
129
     * @throws InvalidArgumentException
130
     */
131
    public function testSetAliasWillThrowContainerExceptionIfTheServiceNameAliasedHasNotBeenRegistered(): void
132
    {
133
        $container = new Container();
134
135
        $alias = 'FooService';
136
        $name = 'TestService';
137
138
        $this->expectException(ContainerException::class);
139
        $this->expectExceptionMessage(
140
            sprintf('Unable to configure alias \'%s\' for unknown service \'%s\'', $alias, $name)
141
        );
142
143
        $container->setAlias($alias, $name);
144
    }
145
146
    /**
147
     * Assert that a ContainerException is thrown when trying to register a service alias with a service that
148
     * has an identical name
149
     *
150
     * @throws InvalidArgumentException
151
     */
152
    public function testSetAliasWillThrowContainerExceptionIfTheServiceNameIsIdenticalToTheAlias(): void
153
    {
154
        $container = new Container();
155
156
        $alias = 'TestService';
157
        $name = 'TestService';
158
159
        $container->set($name, new \stdClass());
160
161
        $this->expectException(ContainerException::class);
162
        $this->expectExceptionMessage(
163
            sprintf('Unable to configure alias \'%s\' with identical service name \'%s\'', $alias, $name)
164
        );
165
166
        $container->setAlias($alias, $name);
167
    }
168
169
    /**
170
     * Assert that the container will throw a NotFoundException if the requested service cannot be found.
171
     *
172
     * @throws CircularDependencyException
173
     * @throws ContainerException
174
     * @throws NotFoundException
175
     */
176
    public function testGetWillThrowNotFoundExceptionIfRequestedServiceIsNotRegistered(): void
177
    {
178
        $container = new Container();
179
180
        $name = 'FooService';
181
182
        $this->expectException(NotFoundException::class);
183
        $this->expectExceptionMessage(
184
            sprintf('Service \'%s\' could not be found registered with the container', $name)
185
        );
186
187
        $container->get($name);
188
    }
189
190
    /**
191
     * Assert that a invalid/non-callable factory class will throw a ContainerException.
192
     *
193
     * @throws CircularDependencyException
194
     * @throws ContainerException
195
     * @throws NotFoundException
196
     */
197
    public function testGetWillThrowContainerExceptionIfTheRegisteredFactoryIsNotCallable(): void
198
    {
199
        $container = new Container();
200
201
        $name = 'FooService';
202
        $factoryClassName = \stdClass::class;
203
204
        $container->setFactoryClass($name, $factoryClassName);
205
206
        $this->expectException(ContainerException::class);
207
        $this->expectExceptionMessage(sprintf('Factory registered for service \'%s\', must be callable', $name));
208
209
        $container->get($name);
210
    }
211
212
    /**
213
     * Assert that we can pass a factory class name string to setFactory() and the service will be registered
214
     *
215
     * @throws ContainerException
216
     * @throws InvalidArgumentException
217
     */
218
    public function testStringFactoryPassedToSetFactoryIsRegistered(): void
219
    {
220
        $container = new Container();
221
222
        $this->assertFalse($container->has('Test'));
223
        $container->setFactory('Test', 'ThisIsAFactoryString');
224
        $this->assertTrue($container->has('Test'));
225
    }
226
227
    /**
228
     * Assert that a InvalidArgumentException is thrown if trying to set a non-string or not callable $factory
229
     * when calling setFactory().
230
     *
231
     * @throws ContainerException
232
     * @throws InvalidArgumentException
233
     */
234
    public function testSetFactoryWithNonStringOrCallableWillThrowInvalidArgumentException(): void
235
    {
236
        $container = new Container();
237
238
        $name = \stdClass::class;
239
        $factory = new \stdClass();
240
241
        $this->expectException(InvalidArgumentException::class);
242
        $this->expectExceptionMessage(
243
            sprintf(
244
                'The \'factory\' argument must be of type \'string\' or \'callable\';'
245
                . '\'%s\' provided for service \'%s\'',
246
                is_object($factory) ? get_class($factory) : gettype($factory),
247
                $name
248
            )
249
        );
250
251
        $container->setFactory($name, $factory);
0 ignored issues
show
Bug introduced by
$factory of type stdClass is incompatible with the type callable|string expected by parameter $factory of Arp\Container\Container::setFactory(). ( Ignorable by Annotation )

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

251
        $container->setFactory($name, /** @scrutinizer ignore-type */ $factory);
Loading history...
252
    }
253
254
    /**
255
     * Assert that circular dependencies between a service name and it's factory are resolved by throwing
256
     * a ContainerException
257
     *
258
     * @throws ContainerExceptionInterface
259
     */
260
    public function testCircularConfigurationDependencyWithFactoryClassNameWillThrowContainerException(): void
261
    {
262
        $name = CallableMock::class;
263
        $factoryClassName = CallableMock::class;
264
265
        $container = new Container();
266
        $container->setFactoryClass($name, $factoryClassName);
267
268
        $this->expectException(ContainerException::class);
269
        $this->expectDeprecationMessage(
270
            sprintf('A circular configuration dependency was detected for service \'%s\'', $name)
271
        );
272
273
        $container->get($name);
274
    }
275
276
    /**
277
     * Assert that the container will throw a ContainerException is the registered factory throws an exception.
278
     *
279
     * @throws ContainerException
280
     */
281
    public function testFactoryCreationErrorWillBeCaughtAndRethrownAsContainerException(): void
282
    {
283
        $container = new Container();
284
285
        $name = 'FooService';
286
        $exceptionMessage = 'This is another test exception message';
287
288
        $factory = static function () use ($exceptionMessage): void {
289
            throw new \RuntimeException($exceptionMessage);
290
        };
291
292
        $container->setFactory($name, $factory);
293
294
        $this->expectException(ContainerException::class);
295
        $this->expectExceptionMessage(
296
            sprintf('The service \'%s\' could not be created: %s', $name, $exceptionMessage)
297
        );
298
299
        $container->get($name);
300
    }
301
302
    /**
303
     * Assert that an unregistered service, which resolves to the name of a valid class, will be created and
304
     * registered with the container. Additional calls to the container's get() method should also return the same
305
     * service
306
     *
307
     * @throws CircularDependencyException
308
     * @throws ContainerException
309
     * @throws NotFoundException
310
     */
311
    public function testGetWillCreateAndReturnUnregisteredServiceIfTheNameResolvesToAValidClassName(): void
312
    {
313
        $container = new Container();
314
315
        $name = \stdClass::class;
316
        $this->assertFalse($container->has($name));
317
        $service = $container->get(\stdClass::class);
318
319
        $this->assertInstanceOf($name, $service);
320
        $this->assertTrue($container->has($name));
321
        $this->assertSame($service, $container->get($name));
322
    }
323
324
    /**
325
     * When creating factories with dependencies, ensure we catch any attempts to load services that depend on each
326
     * other by throwing a ContainerException
327
     *
328
     * @throws CircularDependencyException
329
     * @throws ContainerException
330
     * @throws InvalidArgumentException
331
     * @throws NotFoundException
332
     */
333
    public function testGetWillThrowContainerExceptionIfAFactoryDependencyCausesACircularCreationDependency(): void
334
    {
335
        $container = new Container();
336
337
        $factoryA = static function (ContainerInterface $container) {
338
            $serviceA = new \stdClass();
339
            $serviceA->serviceB = $container->get('ServiceB');
340
            return $serviceA;
341
        };
342
343
        $factoryB = static function (ContainerInterface $container) {
344
            $serviceB = new \stdClass();
345
            $serviceB->serviceA = $container->get('ServiceA');
346
            return $serviceB;
347
        };
348
349
        $container->setFactory('ServiceA', $factoryA);
350
        $container->setFactory('ServiceB', $factoryB);
351
352
        $this->expectException(CircularDependencyException::class);
353
        $this->expectExceptionMessage(
354
            sprintf(
355
                'A circular dependency has been detected for service \'%s\'. The dependency graph includes %s',
356
                'ServiceA',
357
                implode(',', ['ServiceA', 'ServiceB'])
358
            )
359
        );
360
361
        $container->get('ServiceA');
362
    }
363
364
    /**
365
     * When calling get() for a service that has an invalid (not callable) factory class name a ContainerException
366
     * should be thrown
367
     *
368
     * @throws ContainerException
369
     */
370
    public function testGetWillThrowContainerExceptionForInvalidRegisteredFactoryClassName(): void
371
    {
372
        $container = new Container();
373
374
        $serviceName = 'FooService';
375
        $factoryClassName = 'Foo\\Bar\\ClassNameThatDoesNotExist';
376
377
        // We should be able to add the invalid class without issues
378
        $container->setFactoryClass($serviceName, $factoryClassName);
379
380
        $this->expectException(ContainerException::class);
381
        $this->expectExceptionMessage(
382
            sprintf(
383
                'Failed to create the factory for service \'%s\': The factory class \'%s\' could not be found',
384
                $factoryClassName,
385
                $serviceName
386
            )
387
        );
388
389
        // It is only when we requested the service via get that the factory creation should fail
390
        $container->get($serviceName);
391
    }
392
393
    /**
394
     * Assert that if we try to build a service and we cannot resolve a factory from then a NotFoundException is thrown
395
     *
396
     * @throws ContainerException
397
     */
398
    public function testBuildWillThrowNotFoundExceptionIfTheFactoryCannotBeResolvedFromName(): void
399
    {
400
        $container = new Container();
401
402
        $name = 'FooService';
403
404
        $this->expectException(NotFoundException::class);
405
        $this->expectExceptionMessage(
406
            sprintf('Unable to build service \'%s\': No valid factory could be found', $name)
407
        );
408
409
        $container->build($name);
410
    }
411
412
    /**
413
     * Assert that when creating a service via build(), any previously set service matching the provided $name
414
     * will be ignored and a new instance will be returned. We additional check that the build also will not modify
415
     * or change the previous service and calls to get() will return the existing value
416
     *
417
     * @throws CircularDependencyException
418
     * @throws ContainerException
419
     * @throws InvalidArgumentException
420
     * @throws NotFoundException
421
     */
422
    public function testBuildWillIgnorePreviouslySetServiceWhenCreatingViaFactory(): void
423
    {
424
        $container = new Container();
425
426
        $serviceName = 'ServiceName';
427
428
        // Define our service
429
        $container->setFactory(
430
            $serviceName,
431
            static function () {
432
                return new \stdClass();
433
            }
434
        );
435
436
        // Request it by it's service name  so we 'set' the service
437
        $service = $container->get($serviceName);
438
439
        $builtService = $container->build($serviceName);
440
441
        $this->assertInstanceOf(\stdClass::class, $service);
442
        $this->assertInstanceOf(\stdClass::class, $builtService);
443
444
        // The services should not be the same object instance
445
        $this->assertNotSame($service, $builtService);
446
447
        // We expect the existing service to not have been modified and additional calls to get
448
        // resolve to the existing set service (and will not execute the factory)
449
        $this->assertSame($service, $container->get($serviceName));
450
    }
451
452
453
    /**
454
     * Assert that an alias service name will correctly resolve the the correct service when calling build()
455
     *
456
     * @throws ContainerException
457
     * @throws InvalidArgumentException
458
     * @throws NotFoundException
459
     */
460
    public function testBuildWillResolveAliasToServiceName(): void
461
    {
462
        $container = new Container();
463
464
        $alias = 'FooAliasName';
465
        $name = 'FooServiceName';
466
467
        // Define our service
468
        $container->setFactory(
469
            $name,
470
            static function () {
471
                return new \stdClass();
472
            }
473
        );
474
        $container->setAlias($alias, $name);
475
476
        $this->assertInstanceOf(\stdClass::class, $container->build($alias));
477
    }
478
479
    /**
480
     * Assert that configuration errors will raise a ContainerException
481
     *
482
     * @throws ContainerException
483
     */
484
    public function testConfigureWillThrowAContainerExceptionIfTheConfigurationFails(): void
485
    {
486
        $container = new Container();
487
488
        /** @var ServiceProviderInterface|MockObject $provider */
489
        $provider = $this->getMockForAbstractClass(ServiceProviderInterface::class);
490
491
        $exceptionMessage = 'This is a test exception message';
492
        $exceptionCode = 12345;
493
        $exception = new ServiceProviderException($exceptionMessage, $exceptionCode);
494
495
        $provider->expects($this->once())
496
            ->method('registerServices')
497
            ->with($container)
498
            ->willThrowException($exception);
499
500
        $this->expectException(ContainerException::class);
501
        $this->expectExceptionCode($exceptionCode);
502
        $this->expectExceptionMessage(
503
            sprintf(
504
                'Failed to register services using provider \'%s\': %s',
505
                get_class($provider),
506
                $exceptionMessage
507
            )
508
        );
509
510
        $container->configure($provider);
511
    }
512
513
    /**
514
     * Assert that configure() will correctly configure the expected container services
515
     *
516
     * @param bool $viaConstructor
517
     *
518
     * @dataProvider getConfigureData
519
     *
520
     * @throws CircularDependencyException
521
     * @throws ContainerException
522
     * @throws NotFoundException
523
     */
524
    public function testConfigure(bool $viaConstructor): void
525
    {
526
        $fooService = new \stdClass();
527
        $barServiceFactory = static function () {
528
            return new \stdClass();
529
        };
530
531
        $serviceProvider = new class ($fooService, $barServiceFactory) implements ServiceProviderInterface {
532
            private \stdClass $fooService;
533
            private \Closure $barServiceFactory;
534
535
            public function __construct($fooService, $barServiceFactory)
536
            {
537
                $this->fooService = $fooService;
538
                $this->barServiceFactory = $barServiceFactory;
539
            }
540
541
            public function registerServices(Container $container): void
542
            {
543
                $container->set('FooService', $this->fooService);
544
                $container->setFactory('BarService', $this->barServiceFactory);
545
            }
546
        };
547
548
        if ($viaConstructor) {
549
            $container = new Container($serviceProvider);
550
        } else {
551
            $container = new Container();
552
            $container->configure($serviceProvider);
553
        }
554
        $this->assertSame($fooService, $container->get('FooService'));
555
        $this->assertInstanceOf(\stdClass::class, $container->get('BarService'));
556
    }
557
558
    /**
559
     * @return array
560
     */
561
    public function getConfigureData(): array
562
    {
563
        return [
564
            [true],
565
            [false],
566
        ];
567
    }
568
}
569