Passed
Pull Request — master (#3)
by Alex
02:09
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\Factory\ServiceFactoryInterface;
12
use Arp\Container\Provider\ConfigServiceProvider;
13
use Arp\Container\Provider\Exception\NotSupportedException;
14
use Arp\Container\Provider\Exception\ServiceProviderException;
15
use Arp\Container\Provider\ServiceProviderInterface;
16
use PHPUnit\Framework\MockObject\MockObject;
17
use PHPUnit\Framework\TestCase;
18
use Psr\Container\ContainerInterface;
19
20
/**
21
 * @covers  \Arp\Container\Provider\ConfigServiceProvider
22
 *
23
 * @author  Alex Patterson <[email protected]>
24
 * @package ArpTest\Container\Provider
25
 */
26
final class ConfigServiceProviderTest extends TestCase
27
{
28
    /**
29
     * @var ContainerAdapterInterface|MockObject
30
     */
31
    private $adapter;
32
33
    /**
34
     * Prepare the test case dependencies
35
     */
36
    public function setUp(): void
37
    {
38
        $this->adapter = $this->createMock(ContainerAdapterInterface::class);
39
    }
40
41
    /**
42
     * Assert that the class implements ServiceProviderInterface.
43
     */
44
    public function testImplementsServiceProviderInterface(): void
45
    {
46
        $serviceProvider = new ConfigServiceProvider([]);
47
48
        $this->assertInstanceOf(ServiceProviderInterface::class, $serviceProvider);
49
    }
50
51
    /**
52
     * Assert that is the adapter raises an exception when executing setService() a ServiceProviderException
53
     * exception is thrown instead
54
     *
55
     * @throws NotSupportedException
56
     * @throws ServiceProviderException
57
     */
58
    public function testRegisterServicesWillThrowServiceProviderExceptionIfServiceCannotBeRegistered(): void
59
    {
60
        $name = 'FooService';
61
        $service = new \stdClass();
62
        $config = [
63
            ConfigServiceProvider::SERVICES => [
64
                $name => $service,
65
            ],
66
        ];
67
68
        $exceptionMessage = 'This is a test exception message from the adapter';
69
        $exceptionCode = 123454;
70
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
71
72
        $this->adapter->expects($this->once())
73
            ->method('setService')
74
            ->with($name, $service)
75
            ->willThrowException($exception);
76
77
        $this->expectException(ServiceProviderException::class);
78
        $this->expectExceptionCode($exceptionCode);
79
        $this->expectExceptionMessage(sprintf('Failed to register service \'%s\': %s', $name, $exceptionMessage));
80
81
        (new ConfigServiceProvider($config))->registerServices($this->adapter);
82
    }
83
84
    /**
85
     * Assert that invalid factories will raise a ServiceProviderException
86
     *
87
     * @throws ServiceProviderException
88
     */
89
    public function testRegisterServicesWillThrowServiceProviderExceptionIfProvidedFactoryIsInvalid(): void
90
    {
91
        $serviceName = 'FooService';
92
        $serviceFactory = false; // this is our invalid factory
93
94
        $serviceProvider = new ConfigServiceProvider(
95
            [
96
                'factories' => [
97
                    $serviceName => $serviceFactory,
98
                ],
99
            ]
100
        );
101
102
        $this->expectException(ServiceProviderException::class);
103
        $this->expectExceptionMessage(
104
            sprintf('Failed to register service \'%s\': The factory provided is not callable', $serviceName)
105
        );
106
107
        $serviceProvider->registerServices($this->adapter);
108
    }
109
110
    /**
111
     * Assert that AdapterException thrown from the adapter are caught and rethrown as ServiceProviderException.
112
     *
113
     * @throws ServiceProviderException
114
     */
115
    public function testRegisterServicesWillCatchAdapterExceptionAndRethrowAsServiceProviderException(): void
116
    {
117
        $serviceName = 'Foo';
118
        $serviceFactory = static function (): \stdClass {
119
            return new \stdClass();
120
        };
121
122
        $config = [
123
            'factories' => [
124
                $serviceName => $serviceFactory,
125
            ],
126
        ];
127
128
        $exceptionMessage = 'This is a test exception message';
129
        $exceptionCode = 3456;
130
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
131
132
        $this->adapter->expects($this->once())
133
            ->method('setFactory')
134
            ->with($serviceName, $serviceFactory)
135
            ->willThrowException($exception);
136
137
        $this->expectException(ServiceProviderException::class);
138
        $this->expectExceptionCode($exceptionCode);
139
        $this->expectExceptionMessage(
140
            sprintf('Failed to register service \'%s\': %s', $serviceName, $exceptionMessage),
141
        );
142
143
        (new ConfigServiceProvider($config))->registerServices($this->adapter);
144
    }
145
146
    /**
147
     * @throws NotSupportedException
148
     * @throws ServiceProviderException
149
     */
150
    public function testRegisterServicesWillThrowServiceProviderExceptionIfTheServiceAliasCannotBeSet(): void
151
    {
152
        $service = new \stdClass();
153
        $serviceName = 'FooService';
154
        $aliasName = 'FooAlias';
155
156
        $config = [
157
            'services' => [
158
                $serviceName => $service,
159
            ],
160
            'aliases'  => [
161
                $aliasName => $serviceName,
162
            ],
163
        ];
164
165
        $exceptionMessage = 'Test exception message';
166
        $exceptionCode = 12345;
167
        $exception = new AdapterException($exceptionMessage, $exceptionCode);
168
169
        /** @var AliasAwareInterface|MockObject $adapter */
170
        $adapter = $this->getMockForAbstractClass(AliasAwareInterface::class);
171
172
        $adapter->expects($this->once())
173
            ->method('setService')
174
            ->with($serviceName, $service);
175
176
        $adapter->expects($this->once())
177
            ->method('setAlias')
178
            ->with($aliasName, $serviceName)
179
            ->willThrowException($exception);
180
181
        $this->expectException(ServiceProviderException::class);
182
        $this->expectExceptionCode($exceptionCode);
183
        $this->expectExceptionMessage(
184
            sprintf(
185
                'Failed to register alias \'%s\' for service \'%s\': %s',
186
                $aliasName,
187
                $serviceName,
188
                $exceptionMessage
189
            )
190
        );
191
192
        (new ConfigServiceProvider($config))->registerServices($adapter);
193
    }
194
195
    /**
196
     * @throws NotSupportedException
197
     * @throws ServiceProviderException
198
     */
199
    public function testRegisterServicesWillThrowServiceProviderExceptionIfTheArrayServiceIsInvalid(): void
200
    {
201
        $serviceName = 'FooService';
202
203
        $config = [
204
            'factories' => [
205
                $serviceName => [],
206
            ],
207
        ];
208
209
        $this->expectException(ServiceProviderException::class);
210
        $this->expectExceptionMessage(
211
            sprintf('Failed to register service \'%s\': The provided array configuration is invalid', $serviceName)
212
        );
213
214
        (new ConfigServiceProvider($config))->registerServices($this->adapter);
215
    }
216
217
    /**
218
     * Assert that register services will correctly register the provided services and factories defined in $config.
219
     *
220
     * @param array $config The services that should be set
221
     *
222
     * @dataProvider getRegisterServicesWithFactoriesAndServicesData
223
     *
224
     * @throws ServiceProviderException
225
     */
226
    public function testRegisterServicesWithFactoriesAndServices(array $config): void
227
    {
228
        $serviceProvider = new ConfigServiceProvider($config);
229
230
        $factories = $config[ConfigServiceProvider::FACTORIES] ?? [];
231
        $services = $config[ConfigServiceProvider::SERVICES] ?? [];
232
233
        $setFactoryArgs = $setServiceArgs = $setFactoryClassArgs = [];
234
235
        /** @var FactoryClassAwareInterface|MockObject $adapter */
236
        $adapter = $this->getMockForAbstractClass(FactoryClassAwareInterface::class);
237
        foreach ($factories as $name => $factory) {
238
            $methodName = null;
239
240
            if (is_array($factory)) {
241
                $methodName = $factory[1] ?? null;
242
                $factory = $factory[0] ?? null;
243
244
                if (is_string($factory)) {
245
                    $setFactoryClassArgs[] = [$name, $factory, $methodName];
246
                    continue;
247
                }
248
                if (!is_callable($factory) && !$factory instanceof \Closure) {
249
                    $factory = [$factory, $methodName];
250
                }
251
            }
252
253
            if (is_string($factory)) {
254
                $setFactoryClassArgs[] = [$name, $factory, $methodName];
255
                continue;
256
            }
257
            $setFactoryArgs[] = [$name, $factory];
258
        }
259
260
        foreach ($services as $name => $service) {
261
            $setServiceArgs[] = [$name, $service];
262
        }
263
264
        $adapter->expects($this->exactly(count($setFactoryClassArgs)))
265
            ->method('setFactoryClass')
266
            ->withConsecutive(...$setFactoryClassArgs);
267
268
        $adapter->expects($this->exactly(count($setFactoryArgs)))
269
            ->method('setFactory')
270
            ->withConsecutive(...$setFactoryArgs);
271
272
        $adapter->expects($this->exactly(count($setServiceArgs)))
273
            ->method('setService')
274
            ->withConsecutive(...$setServiceArgs);
275
276
        $serviceProvider->registerServices($adapter);
277
    }
278
279
    /**
280
     * @return array
281
     */
282
    public function getRegisterServicesWithFactoriesAndServicesData(): array
283
    {
284
        return [
285
            [
286
                [], // empty config test
287
            ],
288
289
            [
290
                [
291
                    ConfigServiceProvider::FACTORIES => [
292
                        'FooService' => static function () {
293
                            return 'Hi';
294
                        },
295
                    ],
296
                ],
297
            ],
298
299
            [
300
                [
301
                    'services' => [
302
                        'FooService' => new \stdClass(),
303
                        'BarService' => new \stdClass(),
304
                        'Baz'        => 123,
305
                    ],
306
                ],
307
            ],
308
309
            [
310
                [
311
                    'factories' => [
312
                        'BazStringService' => $this->getMockForAbstractClass(ServiceFactoryInterface::class),
313
                        'Bar'              => static function () {
314
                            return 'Test';
315
                        },
316
                    ],
317
                ],
318
            ],
319
320
            // Array based registration for non callable factory object with custom method name 'create'
321
            [
322
                [
323
                    'factories' => [
324
                        'FooService' => [
325
                            new class {
326
                                public function create(ContainerInterface $container): \stdClass
0 ignored issues
show
Unused Code introduced by
The parameter $container is not used and could be removed. ( Ignorable by Annotation )

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

326
                                public function create(/** @scrutinizer ignore-unused */ ContainerInterface $container): \stdClass

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
327
                                {
328
                                    return new \stdClass();
329
                                }
330
                            },
331
                            'create',
332
                        ],
333
                    ],
334
                ],
335
            ],
336
337
            // String and array based registration
338
            [
339
                [
340
                    'factories' => [
341
                        'FooService' => 'StringFactoryName',
342
                        'BazService' => ['BazServiceFactory'],
343
                        'BarService' => ['StringBarServiceFactory', 'methodNameThatWillBeCalled'],
344
                        'ZapService' => ['ZapServiceFactory', '__invoke'],
345
                    ],
346
                ]
347
            ]
348
        ];
349
    }
350
351
    /**
352
     * Assert that a NotSupportedException is thrown when passing a string factory configuration to a adapter that
353
     * does not implement FactoryClassAwareInterface
354
     *
355
     * @throws NotSupportedException
356
     * @throws ServiceProviderException
357
     */
358
    public function testNotSupportExceptionIsThrownIsStringFactoryIsNotSupportedByTheAdapter(): void
359
    {
360
        $serviceName = 'Test123';
361
        $factoryName = \stdClass::class;
362
        $config = [
363
            'factories' => [
364
                $serviceName => $factoryName,
365
            ]
366
        ];
367
368
        $serviceProvider = new ConfigServiceProvider($config);
369
370
        $this->expectException(NotSupportedException::class);
371
        $this->expectExceptionMessage(
372
            sprintf(
373
                'Failed to register service \'%s\': The adapter class \'%s\' does not support factory \'%s\' '
374
                . 'which is of type \'string\'.'
375
                . 'The adapter must implement \'%s\' in order to support registration \'string\' factory types',
376
                get_class($this->adapter),
377
                $serviceName,
378
                $factoryName,
379
                FactoryClassAwareInterface::class
380
            )
381
        );
382
383
        // We provide an adapter mock that is doe NOT implement FactoryClassAwareInterface
384
        $serviceProvider->registerServices($this->adapter);
385
    }
386
387
    /**
388
     * Assert that the ServiceProvider supports string factory registration
389
     *
390
     * @throws NotSupportedException
391
     * @throws ServiceProviderException
392
     */
393
    public function testRegistrationOfStringFactories(): void
394
    {
395
        $serviceName = 'Test123';
396
        $factoryName = \stdClass::class;
397
        $config = [
398
            'factories' => [
399
                $serviceName => $factoryName,
400
            ]
401
        ];
402
403
        /** @var FactoryClassAwareInterface|MockObject $adapter */
404
        $adapter = $this->getMockForAbstractClass(FactoryClassAwareInterface::class);
405
        $adapter->expects($this->once())
406
            ->method('setFactoryClass')
407
            ->with($serviceName, $factoryName, null);
408
409
        // We provide an adapter mock that is doe NOT implement FactoryClassAwareInterface
410
        (new ConfigServiceProvider($config))->registerServices($adapter);
411
    }
412
}
413