Passed
Pull Request — master (#3)
by Alex
07:00
created

testNotSupportExceptionIsThrownIsStringFactoryIsNotSupportedByTheAdapter()

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 27
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArpTest\Container\Provider;
6
7
use Arp\Container\Adapter\AliasAwareInterface;
8
use Arp\Container\Adapter\ContainerAdapterInterface;
9
use Arp\Container\Adapter\Exception\AdapterException;
10
use Arp\Container\Adapter\FactoryClassAwareInterface;
11
use Arp\Container\Provider\ConfigServiceProvider;
12
use Arp\Container\Provider\Exception\NotSupportedException;
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
18
/**
19
 * @covers  \Arp\Container\Provider\ConfigServiceProvider
20
 *
21
 * @author  Alex Patterson <[email protected]>
22
 * @package ArpTest\Container\Provider
23
 */
24
final class ConfigServiceProviderTest extends TestCase
25
{
26
    /**
27
     * @var ContainerAdapterInterface|MockObject
28
     */
29
    private $adapter;
30
31
    /**
32
     * Prepare the test case dependencies
33
     */
34
    public function setUp(): void
35
    {
36
        $this->adapter = $this->createMock(ContainerAdapterInterface::class);
37
    }
38
39
    /**
40
     * Assert that the class implements ServiceProviderInterface.
41
     */
42
    public function testImplementsServiceProviderInterface(): void
43
    {
44
        $serviceProvider = new ConfigServiceProvider([]);
45
46
        $this->assertInstanceOf(ServiceProviderInterface::class, $serviceProvider);
47
    }
48
49
    /**
50
     * Assert that is the adapter raises an exception when executing setService() a ServiceProviderException
51
     * exception is thrown instead
52
     *
53
     * @throws NotSupportedException
54
     * @throws ServiceProviderException
55
     */
56
    public function testRegisterServicesWillThrowServiceProviderExceptionIfServiceCannotBeRegistered(): void
57
    {
58
        $name = 'FooService';
59
        $service = new \stdClass();
60
        $config = [
61
            ConfigServiceProvider::SERVICES => [
62
                $name => $service,
63
            ],
64
        ];
65
66
        $exceptionMessage = 'This is a test exception message from the adapter';
67
        $exceptionCode = 123454;
68
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
69
70
        $this->adapter->expects($this->once())
71
            ->method('setService')
72
            ->with($name, $service)
73
            ->willThrowException($exception);
74
75
        $this->expectException(ServiceProviderException::class);
76
        $this->expectExceptionCode($exceptionCode);
77
        $this->expectExceptionMessage(sprintf('Failed to register service \'%s\': %s', $name, $exceptionMessage));
78
79
        (new ConfigServiceProvider($config))->registerServices($this->adapter);
80
    }
81
82
    /**
83
     * Assert that invalid factories will raise a ServiceProviderException
84
     *
85
     * @throws ServiceProviderException
86
     */
87
    public function testRegisterServicesWillThrowServiceProviderExceptionIfProvidedFactoryIsInvalid(): void
88
    {
89
        $serviceName = 'FooService';
90
        $serviceFactory = false; // this is our invalid factory
91
92
        $serviceProvider = new ConfigServiceProvider(
93
            [
94
                'factories' => [
95
                    $serviceName => $serviceFactory,
96
                ],
97
            ]
98
        );
99
100
        $this->expectException(ServiceProviderException::class);
101
        $this->expectExceptionMessage(
102
            sprintf('Failed to register service \'%s\': The factory provided is not callable', $serviceName)
103
        );
104
105
        $serviceProvider->registerServices($this->adapter);
106
    }
107
108
    /**
109
     * Assert that AdapterException thrown from the adapter are caught and rethrown as ServiceProviderException.
110
     *
111
     * @throws ServiceProviderException
112
     */
113
    public function testRegisterServicesWillCatchAdapterExceptionAndRethrowAsServiceProviderException(): void
114
    {
115
        $serviceName = 'Foo';
116
        $serviceFactory = static function (): \stdClass {
117
            return new \stdClass();
118
        };
119
120
        $config = [
121
            'factories' => [
122
                $serviceName => $serviceFactory,
123
            ],
124
        ];
125
126
        $exceptionMessage = 'This is a test exception message';
127
        $exceptionCode = 3456;
128
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
129
130
        $this->adapter->expects($this->once())
131
            ->method('setFactory')
132
            ->with($serviceName, $serviceFactory)
133
            ->willThrowException($exception);
134
135
        $this->expectException(ServiceProviderException::class);
136
        $this->expectExceptionCode($exceptionCode);
137
        $this->expectExceptionMessage(
138
            sprintf('Failed to register service \'%s\': %s', $serviceName, $exceptionMessage),
139
        );
140
141
        (new ConfigServiceProvider($config))->registerServices($this->adapter);
142
    }
143
144
    /**
145
     * @throws NotSupportedException
146
     * @throws ServiceProviderException
147
     */
148
    public function testRegisterServicesWillThrowServiceProviderExceptionIfTheServiceAliasCannotBeSet(): void
149
    {
150
        $service = new \stdClass();
151
        $serviceName = 'FooService';
152
        $aliasName = 'FooAlias';
153
154
        $config = [
155
            'services' => [
156
                $serviceName => $service,
157
            ],
158
            'aliases'  => [
159
                $aliasName => $serviceName,
160
            ],
161
        ];
162
163
        $exceptionMessage = 'Test exception message';
164
        $exceptionCode = 12345;
165
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
166
167
        /** @var AliasAwareInterface|MockObject $adapter */
168
        $adapter = $this->getMockForAbstractClass(AliasAwareInterface::class);
169
170
        $adapter->expects($this->once())
171
            ->method('setService')
172
            ->with($serviceName, $service);
173
174
        $adapter->expects($this->once())
175
            ->method('setAlias')
176
            ->with($aliasName, $serviceName)
177
            ->willThrowException($exception);
178
179
        $this->expectException(ServiceProviderException::class);
180
        $this->expectExceptionCode($exceptionCode);
181
        $this->expectExceptionMessage(
182
            sprintf(
183
                'Failed to register alias \'%s\' for service \'%s\': %s',
184
                $aliasName,
185
                $serviceName,
186
                $exceptionMessage
187
            )
188
        );
189
190
        (new ConfigServiceProvider($config))->registerServices($adapter);
191
    }
192
193
    /**
194
     * @throws NotSupportedException
195
     * @throws ServiceProviderException
196
     */
197
    public function testRegisterServicesWillThrowServiceProviderExceptionIfTheArrayServiceIsInvalid(): void
198
    {
199
        $serviceName = 'FooService';
200
201
        $config = [
202
            'factories' => [
203
                $serviceName => [],
204
            ],
205
        ];
206
207
        $this->expectException(ServiceProviderException::class);
208
        $this->expectExceptionMessage(
209
            sprintf('Failed to register service \'%s\': The provided array configuration is invalid', $serviceName)
210
        );
211
212
        (new ConfigServiceProvider($config))->registerServices($this->adapter);
213
    }
214
215
    /**
216
     * Assert that register services will correctly register the provided services and factories defined in $config.
217
     *
218
     * @param array $config The services that should be set
219
     *
220
     * @dataProvider getRegisterServicesWithFactoriesAndServicesData
221
     *
222
     * @throws ServiceProviderException
223
     */
224
    public function testRegisterServicesWithFactoriesAndServices(array $config): void
225
    {
226
        $serviceProvider = new ConfigServiceProvider($config);
227
228
        $factories = $config[ConfigServiceProvider::FACTORIES] ?? [];
229
        $services = $config[ConfigServiceProvider::SERVICES] ?? [];
230
231
        $setFactoryArgs = $setServiceArgs = $setFactoryClassArgs = [];
232
233
        /** @var FactoryClassAwareInterface|MockObject $adapter */
234
        $adapter = $this->getMockForAbstractClass(FactoryClassAwareInterface::class);
235
        foreach ($factories as $name => $factory) {
236
            $methodName = null;
237
238
            if (is_array($factory)) {
239
                $methodName = $factory[1] ?? null;
240
                $factory = $factory[0] ?? null;
241
242
                if (is_string($factory)) {
243
                    $setFactoryClassArgs[] = [$name, $factory, $methodName];
244
                    continue;
245
                }
246
                if (!is_callable($factory) && !$factory instanceof \Closure) {
247
                    $factory = [$factory, $methodName];
248
                }
249
            }
250
251
            if (is_string($factory)) {
252
                $setFactoryClassArgs[] = [$name, $factory, $methodName];
253
                continue;
254
            }
255
            $setFactoryArgs[] = [$name, $factory];
256
        }
257
258
        foreach ($services as $name => $service) {
259
            $setServiceArgs[] = [$name, $service];
260
        }
261
262
        $adapter->expects($this->exactly(count($setFactoryClassArgs)))
263
            ->method('setFactoryClass')
264
            ->withConsecutive(...$setFactoryClassArgs);
265
266
        $adapter->expects($this->exactly(count($setFactoryArgs)))
267
            ->method('setFactory')
268
            ->withConsecutive(...$setFactoryArgs);
269
270
        $adapter->expects($this->exactly(count($setServiceArgs)))
271
            ->method('setService')
272
            ->withConsecutive(...$setServiceArgs);
273
274
        $serviceProvider->registerServices($adapter);
275
    }
276
277
    /**
278
     * @return array
279
     */
280
    public function getRegisterServicesWithFactoriesAndServicesData(): array
281
    {
282
        return [
283
            [
284
                [], // empty config test
285
            ],
286
287
            [
288
                [
289
                    ConfigServiceProvider::FACTORIES => [
290
                        'FooService' => static function () {
291
                            return 'Hi';
292
                        },
293
                    ],
294
                ],
295
            ],
296
297
            [
298
                [
299
                    'services' => [
300
                        'FooService' => new \stdClass(),
301
                        'BarService' => new \stdClass(),
302
                        'Baz'        => 123,
303
                    ],
304
                ],
305
            ],
306
307
            // Array based registration for non callable factory object with custom method name 'create'
308
            [
309
                [
310
                    'factories' => [
311
                        'FooService' => [
312
                            new class {
313
                                public function create(): \stdClass
314
                                {
315
                                    return new \stdClass();
316
                                }
317
                            },
318
                            'create',
319
                        ],
320
                    ],
321
                ],
322
            ],
323
324
            // String and array based registration
325
            [
326
                [
327
                    'factories' => [
328
                        'FooService' => 'StringFactoryName',
329
                        'BazService' => ['BazServiceFactory'],
330
                        'BarService' => ['StringBarServiceFactory', 'methodNameThatWillBeCalled'],
331
                        'ZapService' => ['ZapServiceFactory', '__invoke'],
332
                    ],
333
                ]
334
            ]
335
        ];
336
    }
337
338
    /**
339
     * Assert that a NotSupportedException is thrown when passing a string factory configuration to a adapter that
340
     * does not implement FactoryClassAwareInterface
341
     *
342
     * @throws NotSupportedException
343
     * @throws ServiceProviderException
344
     */
345
    public function testNotSupportExceptionIsThrownIsStringFactoryIsNotSupportedByTheAdapter(): void
346
    {
347
        $serviceName = 'Test123';
348
        $factoryName = \stdClass::class;
349
        $config = [
350
            'factories' => [
351
                $serviceName => $factoryName,
352
            ]
353
        ];
354
355
        $serviceProvider = new ConfigServiceProvider($config);
356
357
        $this->expectException(NotSupportedException::class);
358
        $this->expectExceptionMessage(
359
            sprintf(
360
                'Failed to register service \'%s\': The adapter class \'%s\' does not support factory \'%s\' '
361
                . 'which is of type \'string\'.'
362
                . 'The adapter must implement \'%s\' in order to support registration \'string\' factory types',
363
                get_class($this->adapter),
364
                $serviceName,
365
                $factoryName,
366
                FactoryClassAwareInterface::class
367
            )
368
        );
369
370
        // We provide an adapter mock that is doe NOT implement FactoryClassAwareInterface
371
        $serviceProvider->registerServices($this->adapter);
372
    }
373
374
    /**
375
     * Assert that a ServiceProviderException is thrown when the provider is unable to register a factory class
376
     *
377
     * @throws NotSupportedException
378
     * @throws ServiceProviderException
379
     */
380
    public function testStringFactoryRegistrationFailureWillThrowServiceProviderException(): void
381
    {
382
        $serviceName = 'FooService';
383
        $factoryName = 'FooServiceFactory';
384
        $factoryMethodName = null;
385
386
        $config = [
387
            'factories' => [
388
                $serviceName => $factoryName,
389
            ]
390
        ];
391
392
        /** @var FactoryClassAwareInterface|MockObject $adapter */
393
        $adapter = $this->getMockForAbstractClass(FactoryClassAwareInterface::class);
394
395
        $exceptionMessage = 'The exception message test string';
396
        $exceptionCode = 12345;
397
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
398
399
        $this->expectException(ServiceProviderException::class);
400
        $this->expectExceptionCode($exceptionCode);
401
        $this->expectExceptionMessage(
402
            sprintf(
403
                'Failed to register service \'%s\' with adapter \'%s\' using factory class \'%s\': %s',
404
                $serviceName,
405
                get_class($adapter),
406
                $factoryName,
407
                $exceptionMessage
408
            )
409
        );
410
411
        $adapter->expects($this->once())
412
            ->method('setFactoryClass')
413
            ->with($serviceName, $factoryName, $factoryMethodName)
414
            ->willThrowException($exception);
415
416
        (new ConfigServiceProvider($config))->registerServices($adapter);
417
    }
418
419
    /**
420
     * Assert that the ServiceProvider supports string factory registration
421
     *
422
     * @throws NotSupportedException
423
     * @throws ServiceProviderException
424
     */
425
    public function testRegistrationOfStringFactories(): void
426
    {
427
        $serviceName = 'Test123';
428
        $factoryName = \stdClass::class;
429
        $config = [
430
            'factories' => [
431
                $serviceName => $factoryName,
432
            ]
433
        ];
434
435
        /** @var FactoryClassAwareInterface|MockObject $adapter */
436
        $adapter = $this->getMockForAbstractClass(FactoryClassAwareInterface::class);
437
        $adapter->expects($this->once())
438
            ->method('setFactoryClass')
439
            ->with($serviceName, $factoryName, null);
440
441
        // We provide an adapter mock that is doe NOT implement FactoryClassAwareInterface
442
        (new ConfigServiceProvider($config))->registerServices($adapter);
443
    }
444
}
445