Passed
Pull Request — master (#4)
by Alex
01:43
created

testGetWillCreateAndReturnUnregisteredServiceIfTheNameResolvesToAValidClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A ContainerTest::testStringFactoryPassedToSetFactoryIsRegistered() 0 7 1
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(
187
            sprintf(
188
                'Factory \'%s\' registered for service \'%s\', must be callable',
189
                $factoryClassName,
190
                $name
191
            )
192
        );
193
194
        $container->get($name);
195
    }
196
197
    /**
198
     * Assert that we can pass a factory class name string to setFactory() and the service will be registered
199
     *
200
     * @throws ContainerException
201
     * @throws InvalidArgumentException
202
     */
203
    public function testStringFactoryPassedToSetFactoryIsRegistered(): void
204
    {
205
        $container = new Container();
206
207
        $this->assertFalse($container->has('Test'));
208
        $container->setFactory('Test', 'ThisIsAFactoryString');
209
        $this->assertTrue($container->has('Test'));
210
    }
211
212
    /**
213
     * Assert that a InvalidArgumentException is thrown if trying to set a non-string or not callable $factory
214
     * when calling setFactory().
215
     *
216
     * @throws ContainerException
217
     * @throws InvalidArgumentException
218
     */
219
    public function testSetFactoryWithNonStringOrCallableWillThrowInvalidArgumentException(): void
220
    {
221
        $container = new Container();
222
223
        $name = \stdClass::class;
224
        $factory = new \stdClass();
225
226
        $this->expectException(InvalidArgumentException::class);
227
        $this->expectExceptionMessage(
228
            sprintf(
229
                'The \'factory\' argument must be of type \'string\' or \'callable\';'
230
                . '\'%s\' provided for service \'%s\'',
231
                is_object($factory) ? get_class($factory) : gettype($factory),
232
                $name
233
            )
234
        );
235
236
        $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

236
        $container->setFactory($name, /** @scrutinizer ignore-type */ $factory);
Loading history...
237
    }
238
239
    /**
240
     * Assert that circular dependencies between a service name and it's factory are resolved by throwing
241
     * a ContainerException
242
     *
243
     * @throws ContainerExceptionInterface
244
     */
245
    public function testCircularConfigurationDependencyWithFactoryClassNameWillThrowContainerException(): void
246
    {
247
        $name = CallableMock::class;
248
        $factoryClassName = CallableMock::class;
249
250
        $container = new Container();
251
        $container->setFactoryClass($name, $factoryClassName);
252
253
        $this->expectException(ContainerException::class);
254
        $this->expectDeprecationMessage(
255
            sprintf('A circular configuration dependency was detected for service \'%s\'', $name)
256
        );
257
258
        $container->get($name);
259
    }
260
261
    /**
262
     * Assert that the container will throw a ContainerException is the registered factory throws an exception.
263
     *
264
     * @throws ContainerException
265
     */
266
    public function testFactoryCreationErrorWillBeCaughtAndRethrownAsContainerException(): void
267
    {
268
        $container = new Container();
269
270
        $name = 'FooService';
271
        $exceptionMessage = 'This is another test exception message';
272
273
        $factory = static function () use ($exceptionMessage): void {
274
            throw new \RuntimeException($exceptionMessage);
275
        };
276
277
        $container->setFactory($name, $factory);
278
279
        $this->expectException(ContainerException::class);
280
        $this->expectExceptionMessage(
281
            sprintf('The service \'%s\' could not be created: %s', $name, $exceptionMessage)
282
        );
283
284
        $container->get($name);
285
    }
286
287
    /**
288
     * Assert that an unregistered service, which resolves to the name of a valid class, will be created and
289
     * registered with the container. Additional calls to the container's get() method should also return the same
290
     * service
291
     *
292
     * @throws CircularDependencyException
293
     * @throws ContainerException
294
     * @throws NotFoundException
295
     */
296
    public function testGetWillCreateAndReturnUnregisteredServiceIfTheNameResolvesToAValidClassName(): void
297
    {
298
        $container = new Container();
299
300
        $name = \stdClass::class;
301
        $this->assertFalse($container->has($name));
302
        $service = $container->get(\stdClass::class);
303
304
        $this->assertInstanceOf($name, $service);
305
        $this->assertTrue($container->has($name));
306
        $this->assertSame($service, $container->get($name));
307
    }
308
309
    /**
310
     * When creating factories with dependencies, ensure we catch any attempts to load services that depend on each
311
     * other by throwing a ContainerException
312
     *
313
     * @throws CircularDependencyException
314
     * @throws ContainerException
315
     * @throws InvalidArgumentException
316
     * @throws NotFoundException
317
     */
318
    public function testGetWillThrowContainerExceptionIfAFactoryDependencyCausesACircularCreationDependency(): void
319
    {
320
        $container = new Container();
321
322
        $factoryA = static function (ContainerInterface $container) {
323
            $serviceA = new \stdClass();
324
            $serviceA->serviceB = $container->get('ServiceB');
325
            return $serviceA;
326
        };
327
328
        $factoryB = static function (ContainerInterface $container) {
329
            $serviceB = new \stdClass();
330
            $serviceB->serviceA = $container->get('ServiceA');
331
            return $serviceB;
332
        };
333
334
        $container->setFactory('ServiceA', $factoryA);
335
        $container->setFactory('ServiceB', $factoryB);
336
337
        $this->expectException(CircularDependencyException::class);
338
        $this->expectExceptionMessage(
339
            sprintf(
340
                'A circular dependency has been detected for service \'%s\'. The dependency graph includes %s',
341
                'ServiceA',
342
                implode(',', ['ServiceA', 'ServiceB'])
343
            )
344
        );
345
346
        $container->get('ServiceA');
347
    }
348
349
    /**
350
     * When calling get() for a service that has an invalid (not callable) factory class name a ContainerException
351
     * should be thrown
352
     *
353
     * @throws ContainerException
354
     */
355
    public function testGetWillThrowContainerExceptionForInvalidRegisteredFactoryClassName(): void
356
    {
357
        $container = new Container();
358
359
        $serviceName = 'FooService';
360
        $factoryClassName = 'Foo\\Bar\\ClassNameThatDoesNotExist';
361
362
        // We should be able to add the invalid class without issues
363
        $container->setFactoryClass($serviceName, $factoryClassName);
364
365
        $this->expectException(ContainerException::class);
366
        $this->expectExceptionMessage(
367
            sprintf(
368
                'The factory service \'%s\', registered for service \'%s\', is not a valid service or class name',
369
                $factoryClassName,
370
                $serviceName
371
            )
372
        );
373
374
        // It is only when we requested the service via get that the factory creation should fail
375
        $container->get($serviceName);
376
    }
377
378
}
379