• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwiftlyPHP / dependency / 0ab2d434-1b16-4d51-8bb1-bfde8eda961c

11 Sep 2023 11:24AM UTC coverage: 64.972% (-29.3%) from 94.262%
0ab2d434-1b16-4d51-8bb1-bfde8eda961c

push

circleci

clvarley
Allowed invokable objects to be used as factories

2 of 2 new or added lines in 1 file covered. (100.0%)

230 of 354 relevant lines covered (64.97%)

4.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Container.php
1
<?php
2

3
namespace Swiftly\Dependency;
4

5
use Swiftly\Dependency\InspectorInterface;
6
use Swiftly\Dependency\Entry;
7
use Swiftly\Dependency\Type;
8
use Swiftly\Dependency\Parameter;
9
use Swiftly\Dependency\UndefinedStructureException;
10
use Swiftly\Dependency\ParameterException;
11
use Swiftly\Dependency\Exception\UndefinedServiceException;
12
use Swiftly\Dependency\Exception\ServiceInstantiationException;
13
use Swiftly\Dependency\Exception\UnexpectedTypeException;
14
use Swiftly\Dependency\Exception\InvalidArgumentException;
15
use Swiftly\Dependency\Exception\MissingArgumentException;
16
use Swiftly\Dependency\Exception\NestedServiceException;
17
use Exception;
18
use ReflectionClass;
19

20
use function array_key_exists;
21
use function call_user_func_array;
22

23
/**
24
 * Container responsible for storing and creating services
25
 *
26
 * @api
27
 */
28
final class Container
29
{
30
    private InspectorInterface $inspector;
31

32
    /** @var array<class-string,Entry> $entries */
33
    private array $entries;
34

35
    /** @var array<class-string,class-string> $aliases */
36
    private array $aliases;
37

38
    /**
39
     * Create a container that uses the given `$inspector` to resolve parameters
40
     *
41
     * @param InspectorInterface $inspector Parameter inspector
42
     */
43
    public function __construct(InspectorInterface $inspector)
44
    {
45
        $this->inspector = $inspector;
×
46
        $this->entries = [];
×
47
        $this->aliases = [];
×
48
    }
49

50
    /**
51
     * Register a new service with the container
52
     *
53
     * If provided, the `$factory` argument should either be a service object or
54
     * a callable that creates and returns a service object.
55
     *
56
     * @template T of object
57
     * @psalm-param null|T|callable():T $factory
58
     * @param class-string<T> $service Service type
59
     * @param null|T|callable $factory Service provider/factory
60
     * @return Entry<T>                Service entry definition
61
     */
62
    public function register(string $service, $factory = null): Entry
63
    {
64
        if ($factory && Type::isServiceInstance($factory)) {
×
65
            $entry = Entry::fromInstance($service, $factory);
×
66
        } else {
67
            $entry = new Entry($service, $factory);
×
68
        }
69

70
        return ($this->entries[$service] = $entry);
×
71
    }
72

73
    /**
74
     * Create an alias mapping between one service and another
75
     *
76
     * @throws UndefinedServiceException
77
     *          If trying to alias a service that doesn't exist
78
     *
79
     * @template T of object
80
     * @param class-string<T> $service Service name
81
     * @param class-string<T> $alias   Alias
82
     * @return self                    Chainable interface
83
     */
84
    public function alias(string $service, string $alias): self
85
    {
86
        if (!isset($this->entries[$service])) {
×
87
            throw new UndefinedServiceException($service);
×
88
        }
89

90
        $this->aliases[$alias] = $service;
×
91

92
        return $this;
×
93
    }
94

95
    /**
96
     * Determine if the given service has been registered
97
     *
98
     * @template T of object
99
     * @psalm-assert-if-true T $this->entries[$service]
100
     * @param class-string<T> $service Service type
101
     * @return bool                    Service is registered?
102
     */
103
    public function has(string $service): bool
104
    {
105
        return isset($this->entries[$this->aliases[$service] ?? $service]);
×
106
    }
107

108
    /**
109
     * Return a service of the given type
110
     *
111
     * @throws UndefinedServiceException
112
     *          If no definition is found for the given service
113
     * @throws ServiceInstantiationException
114
     *          If an error occured while resolving service requirements
115
     * @throws UnexpectedTypeException
116
     *          If a service was created but did not meet the type constraints
117
     *
118
     * @template T of object
119
     * @param class-string<T> $service Service type
120
     * @return T                       Service object
121
     */
122
    public function get(string $service): object
123
    {
124
        if (!$this->has($service)) {
×
125
            throw new UndefinedServiceException($service);
×
126
        }
127

128
        // Get the service definition
129
        $entry = $this->entries[$this->aliases[$service] ?? $service];
×
130

131
        // Get factory (or class constructor)
132
        $factory_or_class = self::factoryOrClass($entry);
×
133

134
        // Attempt to resolve arguments
135
        try {
136
            $parameters = $this->inspect($factory_or_class);
×
137
            $parameters = $this->prepare($parameters, $entry->arguments);
×
138
        } catch (Exception $e) {
×
139
            throw new ServiceInstantiationException($service, $e);
×
140
        }
141

142
        // Create the object!
143
        $instance = self::create($factory_or_class, $parameters);
×
144
        self::assertType($instance, $service);
×
145

146
        return $instance;
×
147
    }
148

149
    /**
150
     * Return all services with a given tag
151
     *
152
     * The optional `$type` argument can be used to pass a interface/class
153
     * constraint that all services must adhere to.
154
     *
155
     * @template T of object
156
     * @psalm-param null|class-string<T> $type
157
     * @psalm-return ($type is class-string ? list<T> : list<object>)
158
     * @param non-empty-string $tag   Service tag
159
     * @param null|class-string $type Interface or class constraint
160
     * @return object[]               Tagged services
161
     */
162
    public function tagged(string $tag, ?string $type = null): array
163
    {
164
        $resolved = [];
×
165

166
        foreach ($this->entries as $name => $entry) {
×
167
            if (!$entry->hasTag($tag)) {
×
168
                continue;
×
169
            }
170

171
            $service = $this->get($name);
×
172

173
            if ($type) {
×
174
                self::assertType($service, $type);
×
175
            }
176

177
            $resolved[] = $service;
×
178
        }
179

180
        return $resolved;
×
181
    }
182

183
    /**
184
     * Return the factory - or if not available the FQN - for this service
185
     *
186
     * @template T of object
187
     * @psalm-return class-string<T>|callable():T
188
     * @param Entry<T> $entry Service definition
189
     * @return class-string|callable
190
     */
191
    private static function factoryOrClass(Entry $entry)// : string|callable
192
    {
193
        return $entry->factory ?: $entry->type;
×
194
    }
195

196
    /**
197
     * Inspect the parameters of a class, method or function
198
     *
199
     * Accepts class names and all callable types apart from invokable objects.
200
     *
201
     * @throws ParameterException
202
     * @throws UndefinedStructureException
203
     *
204
     * @param class-string|callable $class_or_callable Class FQN or callable
205
     * @return list<Parameter>                         Parameters
206
     */
207
    private function inspect($class_or_callable): array
208
    {
209
        if (Type::isClassname($class_or_callable)) {
×
210
            return $this->inspector->inspectClass($class_or_callable);
×
211
        } else if (Type::isMethod($class_or_callable)) {
×
212
            return $this->inspector->inspectMethod($class_or_callable[0], $class_or_callable[1]);
×
213
        } else if (Type::isInvokable($class_or_callable)) {
×
214
            return $this->inspector->inspectMethod($class_or_callable, '__invoke');
×
215
        } else {
216
            /** @psalm-suppress PossiblyInvalidArgument */
217
            return $this->inspector->inspectFunction($class_or_callable);
×
218
        }
219
    }
220

221
    /**
222
     * Prepares arguments required for a function call
223
     *
224
     * @throws NestedServiceException
225
     * @throws InvalidArgumentException
226
     * @throws MissingArgumentException
227
     *
228
     * @template T
229
     * @param list<Parameter<T>> $parameters           Parameter information
230
     * @param array<non-empty-string,mixed> $arguments Provided arguments
231
     * @return list<T>                                 Resolved arguments
232
     */
233
    private function prepare(array $parameters, array $arguments): array
234
    {
235
        $prepared = [];
×
236

237
        foreach ($parameters as $parameter) {
×
238
            $name = $parameter->getName();
×
239

240
            // User manually provided args?
241
            if (array_key_exists($name, $arguments)) {
×
242
                $value = $arguments[$name];
×
243
            } else {
244
                $value = $this->findValue($parameter);
×
245
            }
246

247
            if (!$parameter->accepts($value)) {
×
248
                throw new InvalidArgumentException(
×
249
                    $name,
×
250
                    $parameter->getType(),
×
251
                    Type::getName($value)
×
252
                );
×
253
            }
254

255
            $prepared[] = $value;
×
256
        }
257

258
        return $prepared;
×
259
    }
260

261
    /**
262
     * Attempts to find a suitable value for the given parameter
263
     *
264
     * @throws NestedServiceException
265
     * @throws MissingArgumentException
266
     *
267
     * @php:8.0 Use mixed return type
268
     * @template T
269
     * @param Parameter<T> $parameter Parameter definition
270
     * @return null|T                 Resolved argument value
271
     */
272
    private function findValue(Parameter $parameter)// : mixed
273
    {
274
        if (!$parameter->isBuiltin()
×
275
            && $this->has(($type = $parameter->getType()))
×
276
        ) {
277
            try {
278
                return $this->get($type);
×
279
            } catch (Exception $e) {
×
280
                throw new NestedServiceException($type, $e);
×
281
            }
282
        }
283

284
        $value = self::defaultValue($parameter);
×
285

286
        if ($value === null && !$parameter->isNullable()) {
×
287
            throw new MissingArgumentException($parameter->getName());
×
288
        }
289

290
        return $value;
×
291
    }
292

293
    /**
294
     * Return the default argument of a parameter
295
     *
296
     * @php:8.0 Use mixed return type
297
     * @template T
298
     * @param Parameter<T> $parameter Parameter definition
299
     * @return null|T                 Default value
300
     */
301
    private static function defaultValue(Parameter $parameter)// : mixed
302
    {
303
        return ($parameter->hasDefault()
×
304
            ? ($parameter->getDefaultCallback())()
×
305
            : null
×
306
        );
×
307
    }
308

309
    /**
310
     * Create a service, either by calling the factory or creating an object
311
     *
312
     * @template T of object
313
     * @psalm-param class-string<T>|callable():T $factory_or_class
314
     * @param class-string|callable $factory_or_class Factory or class FQN
315
     * @param list<mixed> $arguments                  Arguments
316
     * @return T
317
     */
318
    private static function create($factory_or_class, array $arguments): object
319
    {
320
        if (Type::isClassname($factory_or_class)) {
×
321
            return self::initialise($factory_or_class, $arguments);
×
322
        } else {
323
            /** @psalm-suppress TooManyArguments */
324
            return call_user_func_array($factory_or_class, $arguments);
×
325
        }
326
    }
327

328
    /**
329
     * Initialise a new service instance with the given parameters
330
     *
331
     * @template T of object
332
     * @param class-string<T> $class Class FQN
333
     * @param list<mixed> $arguments Constructor arguments
334
     * @return T                     Initialised class
335
     */
336
    private static function initialise(string $class, array $arguments): object
337
    {
338
        return (new ReflectionClass($class))->newInstanceArgs($arguments);
×
339
    }
340

341
    /**
342
     * Validate that the given object meets a type constaint
343
     *
344
     * @throws UnexpectedTypeException
345
     *          If the `$service` is not of type `$constraint`
346
     *
347
     * @template T of object
348
     * @template K of object
349
     * @psalm-param class-string<K> $constraint
350
     * @psalm-assert T&K $service
351
     * @param T $service               Service instance
352
     * @param class-string $constraint Interface or class constaint
353
     * @return void
354
     */
355
    private static function assertType(object $service, string $constraint): void
356
    {
357
        if (!($service instanceof $constraint))
×
358
            throw new UnexpectedTypeException(
×
359
                $constraint,
×
360
                Type::getName($service)
×
361
            );
×
362
    }
363
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc