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

elephox-dev / framework / 5002016857

pending completion
5002016857

push

github

Ricardo Boss
Skip resolving if provider doesnt have combined type and overhaul union types

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

3876 of 5835 relevant lines covered (66.43%)

8.56 hits per line

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

47.77
/modules/DI/src/ServiceProvider.php
1
<?php
2
declare(strict_types=1);
3

4
namespace Elephox\DI;
5

6
use BadFunctionCallException;
7
use BadMethodCallException;
8
use Closure;
9
use Elephox\Collection\ArrayMap;
10
use Elephox\Collection\Contract\GenericEnumerable;
11
use Elephox\Collection\Enumerable;
12
use Elephox\Collection\OffsetNotFoundException;
13
use Elephox\DI\Contract\RootServiceProvider;
14
use Elephox\DI\Contract\ServiceScope as ServiceScopeContract;
15
use Elephox\DI\Contract\ServiceScopeFactory;
16
use Generator;
17
use LogicException;
18
use ReflectionClass;
19
use ReflectionException;
20
use ReflectionFunction;
21
use ReflectionFunctionAbstract;
22
use ReflectionIntersectionType;
23
use ReflectionNamedType;
24
use ReflectionParameter;
25
use ReflectionType;
26
use ReflectionUnionType;
27

28
/**
29
 * @psalm-type argument-list = array<non-empty-string, mixed>
30
 */
31
readonly class ServiceProvider implements RootServiceProvider, ServiceScopeFactory
32
{
33
        /**
34
         * @var ArrayMap<string, ServiceDescriptor<object, object>>
35
         */
36
        protected ArrayMap $descriptors;
37

38
        /** @var ArrayMap<class-string, object>
39
         */
40
        protected ArrayMap $instances;
41

42
        private array $selfIds;
43
        private ResolverStack $resolverStack;
44

45
        /**
46
         * @param iterable<ServiceDescriptor> $descriptors
47
         * @param iterable<class-string, object> $instances
48
         */
49
        public function __construct(iterable $descriptors = [], iterable $instances = [])
50
        {
51
                $this->descriptors = new ArrayMap();
11✔
52
                $this->instances = new ArrayMap();
11✔
53

54
                /** @var ServiceDescriptor $description */
55
                foreach ($descriptors as $descriptor) {
11✔
56
                        $this->descriptors->put($descriptor->serviceType, $descriptor);
5✔
57
                }
58

59
                foreach ($instances as $className => $instance) {
11✔
60
                        $this->instances->put($className, $instance);
×
61
                }
62

63
                $interfaces = class_implements($this);
11✔
64

65
                assert($interfaces !== false);
11✔
66

67
                $this->selfIds = [
11✔
68
                        self::class,
11✔
69
                        ...$interfaces,
11✔
70
                ];
11✔
71

72
                $this->resolverStack = new ResolverStack();
11✔
73
        }
74

75
        protected function isSelf(string $id): bool
76
        {
77
                return in_array($id, $this->selfIds, true);
6✔
78
        }
79

80
        public function has(string $id): bool
81
        {
82
                return $this->isSelf($id) || $this->descriptors->has($id);
6✔
83
        }
84

85
        protected function getDescriptor(string $id): ServiceDescriptor
86
        {
87
                try {
88
                        return $this->descriptors->get($id);
4✔
89
                } catch (OffsetNotFoundException $e) {
×
90
                        throw new ServiceNotFoundException($id, previous: $e);
×
91
                }
92
        }
93

94
        /**
95
         * @template TService of object
96
         *
97
         * @param string|class-string<TService> $id
98
         *
99
         * @return TService
100
         */
101
        public function get(string $id): object
102
        {
103
                if ($this->isSelf($id)) {
4✔
104
                        /** @var TService */
105
                        return $this;
2✔
106
                }
107

108
                $descriptor = $this->getDescriptor($id);
4✔
109

110
                /** @var TService */
111
                return match ($descriptor->lifetime) {
4✔
112
                        ServiceLifetime::Transient => $this->requireTransient($descriptor),
4✔
113
                        ServiceLifetime::Singleton => $this->requireSingleton($descriptor),
4✔
114
                        ServiceLifetime::Scoped => $this->requireScoped($descriptor),
4✔
115
                        default => throw new LogicException("Invalid descriptor lifetime: {$descriptor->lifetime->name}"),
4✔
116
                };
4✔
117
        }
118

119
        protected function requireTransient(ServiceDescriptor $descriptor): object
120
        {
121
                assert($descriptor->lifetime === ServiceLifetime::Transient, sprintf('Expected %s lifetime, got: %s', ServiceLifetime::Transient->name, $descriptor->lifetime->name));
1✔
122

123
                return $this->createInstance($descriptor);
1✔
124
        }
125

126
        protected function requireSingleton(ServiceDescriptor $descriptor): object
127
        {
128
                assert($descriptor->lifetime === ServiceLifetime::Singleton, sprintf('Expected %s lifetime, got: %s', ServiceLifetime::Singleton->name, $descriptor->lifetime->name));
2✔
129

130
                return $this->getOrCreateInstance($descriptor);
2✔
131
        }
132

133
        protected function requireScoped(ServiceDescriptor $descriptor): object
134
        {
135
                assert($descriptor->lifetime === ServiceLifetime::Scoped, sprintf('Expected %s lifetime, got: %s', ServiceLifetime::Scoped->name, $descriptor->lifetime->name));
1✔
136

137
                throw new ServiceException(sprintf(
1✔
138
                        "Cannot resolve service '%s' from %s, as it requires a scope.",
1✔
139
                        $descriptor->serviceType,
1✔
140
                        get_debug_type($this),
1✔
141
                ));
1✔
142
        }
143

144
        protected function getOrCreateInstance(ServiceDescriptor $descriptor): object
145
        {
146
                if ($this->instances->has($descriptor->serviceType)) {
4✔
147
                        $service = $this->instances->get($descriptor->serviceType);
3✔
148
                } else {
149
                        $service = $this->createInstance($descriptor);
4✔
150

151
                        $this->instances->put($descriptor->serviceType, $service);
4✔
152
                }
153

154
                return $service;
4✔
155
        }
156

157
        protected function createInstance(ServiceDescriptor $descriptor): object
158
        {
159
                try {
160
                        return $descriptor->createInstance($this);
4✔
161
                } catch (BadFunctionCallException $e) {
×
162
                        throw new ServiceInstantiationException($descriptor->serviceType, previous: $e);
×
163
                }
164
        }
165

166
        public function createScope(): ServiceScopeContract
167
        {
168
                $scopedProvider = new ScopedServiceProvider(
3✔
169
                        $this,
3✔
170
                        $this->descriptors->where(static fn (ServiceDescriptor $d) => $d->lifetime === ServiceLifetime::Scoped),
3✔
171
                );
3✔
172

173
                return new ServiceScope($scopedProvider);
3✔
174
        }
175

176
        public function dispose(): void
177
        {
178
                $this->instances->clear();
×
179
        }
180

181
        public function instantiate(string $className, array $overrideArguments = [], ?Closure $onUnresolved = null): object
182
        {
183
                if (!class_exists($className)) {
1✔
184
                        assert(is_string($className), sprintf('Expected string, got: %s', get_debug_type($className)));
×
185

186
                        throw new ClassNotFoundException($className);
×
187
                }
188

189
                $reflectionClass = new ReflectionClass($className);
1✔
190
                $constructor = $reflectionClass->getConstructor();
1✔
191

192
                try {
193
                        if ($constructor === null) {
1✔
194
                                return $reflectionClass->newInstance();
1✔
195
                        }
196

197
                        $serviceName = $constructor->getDeclaringClass()->getName();
×
198

199
                        $this->resolverStack->push("$serviceName::__construct");
×
200

201
                        $arguments = $this->resolveArguments($constructor, $overrideArguments, $onUnresolved);
×
202
                        $instance = $reflectionClass->newInstanceArgs([...$arguments]);
×
203

204
                        $this->resolverStack->pop();
×
205

206
                        return $instance;
×
207
                } catch (ReflectionException $e) {
×
208
                        throw new BadMethodCallException("Failed to instantiate class '$className'", previous: $e);
×
209
                }
210
        }
211

212
        /**
213
         * @param class-string $className
214
         * @param non-empty-string $method
215
         * @param argument-list $overrideArguments
216
         * @param null|Closure(ReflectionParameter $param, int $index): mixed $onUnresolved
217
         *
218
         * @return mixed
219
         *
220
         * @throws BadMethodCallException
221
         */
222
        public function callMethod(string $className, string $method, array $overrideArguments = [], ?Closure $onUnresolved = null): mixed
223
        {
224
                $instance = $this->instantiate($className);
×
225

226
                return $this->callMethodOn($instance, $method, $overrideArguments, $onUnresolved);
×
227
        }
228

229
        /**
230
         * @param non-empty-string $method
231
         * @param argument-list $overrideArguments
232
         * @param null|Closure(ReflectionParameter $param, int $index): mixed $onUnresolved
233
         *
234
         * @throws BadMethodCallException
235
         */
236
        public function callMethodOn(object $instance, string $method, array $overrideArguments = [], ?Closure $onUnresolved = null): mixed
237
        {
238
                try {
239
                        $reflectionClass = new ReflectionClass($instance);
×
240
                        $reflectionMethod = $reflectionClass->getMethod($method);
×
241
                        $arguments = $this->resolveArguments($reflectionMethod, $overrideArguments, $onUnresolved);
×
242

243
                        return $reflectionMethod->invokeArgs($instance, [...$arguments]);
×
244
                } catch (ReflectionException $e) {
×
245
                        throw new BadMethodCallException(sprintf(
×
246
                                "Failed to call method '%s' on class '%s'",
×
247
                                $method,
×
248
                                $instance::class,
×
249
                        ), previous: $e);
×
250
                }
251
        }
252

253
        /**
254
         * @param class-string $className
255
         * @param non-empty-string $method
256
         * @param argument-list $overrideArguments
257
         * @param null|Closure(ReflectionParameter $param, int $index): mixed $onUnresolved
258
         *
259
         * @throws BadMethodCallException
260
         */
261
        public function callStaticMethod(string $className, string $method, array $overrideArguments = [], ?Closure $onUnresolved = null): mixed
262
        {
263
                try {
264
                        $reflectionClass = new ReflectionClass($className);
×
265
                        $reflectionMethod = $reflectionClass->getMethod($method);
×
266
                        $arguments = $this->resolveArguments($reflectionMethod, $overrideArguments, $onUnresolved);
×
267

268
                        return $reflectionMethod->invokeArgs(null, [...$arguments]);
×
269
                } catch (ReflectionException $e) {
×
270
                        throw new BadMethodCallException("Failed to call method '$method' on class '$className'", previous: $e);
×
271
                }
272
        }
273

274
        public function call(Closure|ReflectionFunction $callback, array $overrideArguments = [], ?Closure $onUnresolved = null): mixed
275
        {
276
                /** @noinspection PhpUnhandledExceptionInspection $callback is never a string */
277
                $reflectionFunction = $callback instanceof ReflectionFunction ? $callback : new ReflectionFunction($callback);
4✔
278
                $arguments = $this->resolveArguments($reflectionFunction, $overrideArguments, $onUnresolved);
4✔
279

280
                return $reflectionFunction->invokeArgs([...$arguments]);
4✔
281
        }
282

283
        public function resolveArguments(ReflectionFunctionAbstract $function, array $overrideArguments = [], ?Closure $onUnresolved = null): Generator
284
        {
285
                if ($overrideArguments !== [] && array_is_list($overrideArguments)) {
4✔
286
                        yield from $overrideArguments;
×
287

288
                        return;
×
289
                }
290

291
                $argumentCount = 0;
4✔
292
                $parameters = $function->getParameters();
4✔
293

294
                foreach ($parameters as $parameter) {
4✔
295
                        if ($parameter->isVariadic()) {
1✔
296
                                yield from $overrideArguments;
×
297

298
                                break;
×
299
                        }
300

301
                        $name = $parameter->getName();
1✔
302
                        if (array_key_exists($name, $overrideArguments)) {
1✔
303
                                yield $overrideArguments[$name];
×
304

305
                                unset($overrideArguments[$name]);
×
306
                        } else {
307
                                try {
308
                                        yield $this->resolveArgument($parameter);
1✔
309
                                } catch (UnresolvedParameterException $e) {
×
310
                                        if ($onUnresolved === null) {
×
311
                                                throw $e;
×
312
                                        }
313

314
                                        yield $onUnresolved($parameter, $argumentCount);
×
315
                                }
316
                        }
317

318
                        $argumentCount++;
1✔
319
                }
320
        }
321

322
        private function resolveArgument(ReflectionParameter $parameter): mixed
323
        {
324
                $name = $parameter->getName();
1✔
325
                $type = $parameter->getType();
1✔
326

327
                if ($type === null) {
1✔
328
                        if ($this->has($name)) {
×
329
                                /** @var class-string $name */
330
                                return $this->get($name);
×
331
                        }
332

333
                        if ($parameter->isDefaultValueAvailable()) {
×
334
                                return $parameter->getDefaultValue();
×
335
                        }
336

337
                        throw new MissingTypeHintException($parameter);
×
338
                }
339

340
                if ($type instanceof ReflectionUnionType) {
1✔
341
                        $extractTypeNames = static function (ReflectionUnionType|ReflectionIntersectionType $refType, callable $self): Enumerable {
342
                                return new Enumerable(function () use ($refType, $self) {
×
343
                                        foreach ($refType->getTypes() as $t) {
×
344
                                                assert($t instanceof ReflectionType, '$t must be an instance of ReflectionType');
×
345

346
                                                /** @var Closure(ReflectionUnionType|ReflectionIntersectionType, Closure): GenericEnumerable<class-string> $self */
347
                                                if ($t instanceof ReflectionUnionType) {
×
348
                                                        yield $self($t, $self)->toList();
×
349
                                                } else if ($t instanceof ReflectionIntersectionType) {
×
350
                                                        yield [$self($t, $self)->toList()];
×
351
                                                } else if ($t instanceof ReflectionNamedType) {
×
352
                                                        yield [$t->getName()];
×
353
                                                } else {
354
                                                        throw new ReflectionException('Unsupported ReflectionType: ' . get_debug_type($t));
×
355
                                                }
356
                                        }
357
                                });
×
358
                        };
359

360
                        /** @psalm-suppress DocblockTypeContradiction */
361
                        $allowedTypes = $extractTypeNames($type, $extractTypeNames)->select(static fn (string|array $t): string|array => is_array($t) ? (count($t) === 1 ? $t[0] : collect(...$t)->flatten()->toList()) : $t);
×
362
                } else {
363
                        /** @var ReflectionNamedType $type */
364
                        $allowedTypes = [$type->getName()];
1✔
365
                }
366

367
                /** @var list<class-string|list<class-string>> $allowedTypes */
368
                foreach ($allowedTypes as $typeName) {
1✔
369
                        if (is_string($typeName)) {
1✔
370
                                if (!$this->has($typeName)) {
1✔
371
                                        continue;
×
372
                                }
373

374
                                return $this->resolveService($typeName, $parameter->getDeclaringFunction()->getName());
1✔
375
                        }
376

377
                        if (is_array($typeName)) {
×
378
                                /** @var class-string $combinedTypeName */
379
                                $combinedTypeName = implode('&', $typeName);
×
380

381
                                if (!$this->has($combinedTypeName)) {
×
382
                                        continue;
×
383
                                }
384

385
                                return $this->resolveService($combinedTypeName, $parameter->getDeclaringFunction()->getName());
×
386
                        }
387
                }
388

389
                if ($parameter->isDefaultValueAvailable()) {
×
390
                        return $parameter->getDefaultValue();
×
391
                }
392

393
                if ($parameter->allowsNull()) {
×
394
                        return null;
×
395
                }
396

397
                throw new UnresolvedParameterException(
×
398
                        $parameter->getDeclaringClass()?->getShortName() ??
×
399
                        $parameter->getDeclaringFunction()->getClosureScopeClass()?->getShortName() ??
×
400
                        '<unknown class>',
×
401
                        $parameter->getDeclaringFunction()->getShortName(),
×
402
                        (string) $type,
×
403
                        $parameter->name,
×
404
                        $parameter->getDeclaringFunction()->getFileName(),
×
405
                        $parameter->getDeclaringFunction()->getStartLine(),
×
406
                        $parameter->getDeclaringFunction()->getEndLine(),
×
407
                );
×
408
        }
409

410
        /**
411
         * @template TService of object
412
         *
413
         * @param class-string<TService> $name
414
         * @param string $forMethod
415
         *
416
         * @return TService
417
         */
418
        private function resolveService(string $name, string $forMethod): object
419
        {
420
                $this->resolverStack->push("$name::$forMethod");
1✔
421

422
                $service = $this->get($name);
1✔
423

424
                $this->resolverStack->pop();
1✔
425

426
                return $service;
1✔
427
        }
428
}
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

© 2025 Coveralls, Inc