Passed
Push — master ( 208710...261e75 )
by Alex
35s queued 10s
created

testGetWillThrowNotFoundExceptionIfRequestedServiceIsNotRegistered()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

230
        $container->setFactory($name, /** @scrutinizer ignore-type */ $factory);
Loading history...
231
    }
232
233
    /**
234
     * Assert that circular dependencies between a service name and it's factory are resolved by throwing
235
     * a ContainerException
236
     *
237
     * @throws ContainerExceptionInterface
238
     */
239
    public function testCircularConfigurationDependencyWithFactoryClassNameWillThrowContainerException(): void
240
    {
241
        $name = CallableMock::class;
242
        $factoryClassName = CallableMock::class;
243
244
        $container = new Container();
245
        $container->setFactoryClass($name, $factoryClassName);
246
247
        $this->expectException(ContainerException::class);
248
        $this->expectDeprecationMessage(
249
            sprintf('A circular configuration dependency was detected for service \'%s\'', $name)
250
        );
251
252
        $container->get($name);
253
    }
254
255
    /**
256
     * Assert that the container will throw a ContainerException is the registered factory throws an exception.
257
     *
258
     * @throws ContainerException
259
     */
260
    public function testFactoryCreationErrorWillBeCaughtAndRethrownAsContainerException(): void
261
    {
262
        $container = new Container();
263
264
        $name = 'FooService';
265
        $exceptionMessage = 'This is another test exception message';
266
267
        $factory = static function () use ($exceptionMessage): void {
268
            throw new \RuntimeException($exceptionMessage);
269
        };
270
271
        $container->setFactory($name, $factory);
272
273
        $this->expectException(ContainerException::class);
274
        $this->expectExceptionMessage(
275
            sprintf('The service \'%s\' could not be created: %s', $name, $exceptionMessage)
276
        );
277
278
        $container->get($name);
279
    }
280
281
    /**
282
     * Assert that an unregistered service, which resolves to the name of a valid class, will be created and
283
     * registered with the container. Additional calls to the container's get() method should also return the same
284
     * service
285
     *
286
     * @throws CircularDependencyException
287
     * @throws ContainerException
288
     * @throws NotFoundException
289
     */
290
    public function testGetWillCreateAndReturnUnregisteredServiceIfTheNameResolvesToAValidClassName(): void
291
    {
292
        $container = new Container();
293
294
        $name = \stdClass::class;
295
        $this->assertFalse($container->has($name));
296
        $service = $container->get(\stdClass::class);
297
298
        $this->assertInstanceOf($name, $service);
299
        $this->assertTrue($container->has($name));
300
        $this->assertSame($service, $container->get($name));
301
    }
302
303
    /**
304
     * When creating factories with dependencies, ensure we catch any attempts to load services that depend on each
305
     * other by throwing a ContainerException
306
     *
307
     * @throws CircularDependencyException
308
     * @throws ContainerException
309
     * @throws InvalidArgumentException
310
     * @throws NotFoundException
311
     */
312
    public function testGetWillThrowContainerExceptionIfAFactoryDependencyCausesACircularCreationDependency(): void
313
    {
314
        $container = new Container();
315
316
        $factoryA = static function (ContainerInterface $container) {
317
            $serviceA = new \stdClass();
318
            $serviceA->serviceB = $container->get('ServiceB');
319
            return $serviceA;
320
        };
321
322
        $factoryB = static function (ContainerInterface $container) {
323
            $serviceB = new \stdClass();
324
            $serviceB->serviceA = $container->get('ServiceA');
325
            return $serviceB;
326
        };
327
328
        $container->setFactory('ServiceA', $factoryA);
329
        $container->setFactory('ServiceB', $factoryB);
330
331
        $this->expectException(CircularDependencyException::class);
332
        $this->expectExceptionMessage(
333
            sprintf(
334
                'A circular dependency has been detected for service \'%s\'. The dependency graph includes %s',
335
                'ServiceA',
336
                implode(',', ['ServiceA', 'ServiceB'])
337
            )
338
        );
339
340
        $container->get('ServiceA');
341
    }
342
343
    /**
344
     * When calling get() for a service that has an invalid (not callable) factory class name a ContainerException
345
     * should be thrown
346
     *
347
     * @throws ContainerException
348
     */
349
    public function testGetWillThrowContainerExceptionForInvalidRegisteredFactoryClassName(): void
350
    {
351
        $container = new Container();
352
353
        $serviceName = 'FooService';
354
        $factoryClassName = 'Foo\\Bar\\ClassNameThatDoesNotExist';
355
356
        // We should be able to add the invalid class without issues
357
        $container->setFactoryClass($serviceName, $factoryClassName);
358
359
        $this->expectException(ContainerException::class);
360
        $this->expectExceptionMessage(
361
            sprintf(
362
                'The factory service \'%s\', registered for service \'%s\', is not a valid service or class name',
363
                $factoryClassName,
364
                $serviceName
365
            )
366
        );
367
368
        // It is only when we requested the service via get that the factory creation should fail
369
        $container->get($serviceName);
370
    }
371
372
    /**
373
     * Assert that if we try to build a service and we cannot resolve a factory from then a NotFoundException is thrown
374
     *
375
     * @throws ContainerException
376
     */
377
    public function testBuildWillThrowNotFoundExceptionIfTheFactoryCannotBeResolvedFromName(): void
378
    {
379
        $container = new Container();
380
381
        $name = 'FooService';
382
383
        $this->expectException(NotFoundException::class);
384
        $this->expectExceptionMessage(
385
            sprintf('Unable to build service \'%s\': No valid factory could be found', $name)
386
        );
387
388
        $container->build($name);
389
    }
390
391
    /**
392
     * Assert that when creating a service via build(), any previously set service matching the provided $name
393
     * will be ignored and a new instance will be returned. We additional check that the build also will not modify
394
     * or change the previous service and calls to get() will return the existing value
395
     *
396
     * @throws CircularDependencyException
397
     * @throws ContainerException
398
     * @throws InvalidArgumentException
399
     * @throws NotFoundException
400
     */
401
    public function testBuildWillIgnorePreviouslySetServiceWhenCreatingViaFactory(): void
402
    {
403
        $container = new Container();
404
405
        $serviceName = 'ServiceName';
406
407
        // Define our service
408
        $container->setFactory(
409
            $serviceName,
410
            static function () {
411
                return new \stdClass();
412
            }
413
        );
414
415
        // Request it by it's service name  so we 'set' the service
416
        $service = $container->get($serviceName);
417
418
        $builtService = $container->build($serviceName);
419
420
        $this->assertInstanceOf(\stdClass::class, $service);
421
        $this->assertInstanceOf(\stdClass::class, $builtService);
422
423
        // The services should not be the same object instance
424
        $this->assertNotSame($service, $builtService);
425
426
        // We expect the existing service to not have been modified and additional calls to get
427
        // resolve to the existing set service (and will not execute the factory)
428
        $this->assertSame($service, $container->get($serviceName));
429
    }
430
431
432
    /**
433
     * Assert that an alias service name will correctly resolve the the correct service when calling build()
434
     *
435
     * @throws InvalidArgumentException
436
     * @throws NotFoundException
437
     */
438
    public function testBuildWillResolveAliasToServiceName(): void
439
    {
440
        $container = new Container();
441
442
        $alias = 'FooAliasName';
443
        $name = 'FooServiceName';
444
445
        // Define our service
446
        $container->setFactory(
447
            $name,
448
            static function () {
449
                return new \stdClass();
450
            }
451
        );
452
        $container->setAlias($alias, $name);
453
454
        $this->assertInstanceOf(\stdClass::class, $container->build($alias));
455
    }
456
}
457