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

elephox-dev / framework / 5305335414

pending completion
5305335414

push

github

ricardoboss
Fix type errors

8 of 8 new or added lines in 2 files covered. (100.0%)

3877 of 5842 relevant lines covered (66.36%)

8.85 hits per line

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

46.01
/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
        /**
275
         * @template TResult
276
         *
277
         * @param ReflectionFunction|Closure|Closure(mixed): TResult $callback
278
         * @param argument-list $overrideArguments
279
         * @param null|Closure(ReflectionParameter $param, int $index): (null|TResult) $onUnresolved
280
         *
281
         * @return TResult
282
         *
283
         * @throws BadFunctionCallException
284
         */
285
        public function call(Closure|ReflectionFunction $callback, array $overrideArguments = [], ?Closure $onUnresolved = null): mixed
286
        {
287
                /** @noinspection PhpUnhandledExceptionInspection $callback is never a string */
288
                $reflectionFunction = $callback instanceof ReflectionFunction ? $callback : new ReflectionFunction($callback);
4✔
289
                $arguments = $this->resolveArguments($reflectionFunction, $overrideArguments, $onUnresolved);
4✔
290

291
                /** @var TResult */
292
                return $reflectionFunction->invokeArgs([...$arguments]);
4✔
293
        }
294

295
        public function resolveArguments(ReflectionFunctionAbstract $function, array $overrideArguments = [], ?Closure $onUnresolved = null): Generator
296
        {
297
                if ($overrideArguments !== [] && array_is_list($overrideArguments)) {
4✔
298
                        yield from $overrideArguments;
×
299

300
                        return;
×
301
                }
302

303
                $argumentCount = 0;
4✔
304
                $parameters = $function->getParameters();
4✔
305

306
                foreach ($parameters as $parameter) {
4✔
307
                        if ($parameter->isVariadic()) {
1✔
308
                                yield from $overrideArguments;
×
309

310
                                break;
×
311
                        }
312

313
                        $name = $parameter->getName();
1✔
314
                        if (array_key_exists($name, $overrideArguments)) {
1✔
315
                                yield $overrideArguments[$name];
×
316

317
                                unset($overrideArguments[$name]);
×
318
                        } else {
319
                                try {
320
                                        yield $this->resolveArgument($parameter);
1✔
321
                                } catch (UnresolvedParameterException $e) {
×
322
                                        if ($onUnresolved === null) {
×
323
                                                throw $e;
×
324
                                        }
325

326
                                        yield $onUnresolved($parameter, $argumentCount);
×
327
                                }
328
                        }
329

330
                        $argumentCount++;
1✔
331
                }
332
        }
333

334
        private function resolveArgument(ReflectionParameter $parameter): mixed
335
        {
336
                $name = $parameter->getName();
1✔
337
                $type = $parameter->getType();
1✔
338

339
                if ($type === null) {
1✔
340
                        if ($this->has($name)) {
×
341
                                /** @var class-string $name */
342
                                return $this->get($name);
×
343
                        }
344

345
                        if ($parameter->isDefaultValueAvailable()) {
×
346
                                return $parameter->getDefaultValue();
×
347
                        }
348

349
                        throw new MissingTypeHintException($parameter);
×
350
                }
351

352
                if ($type instanceof ReflectionUnionType) {
1✔
353
                        $extractTypeNames = static function (ReflectionUnionType|ReflectionIntersectionType $refType, callable $self): Enumerable {
354
                                /** @var Enumerable<string|list<string>> */
355
                                return new Enumerable(static function () use ($refType, $self) {
×
356
                                        /**
357
                                         * @var Closure(ReflectionUnionType|ReflectionIntersectionType, Closure): GenericEnumerable<class-string> $self
358
                                         * @var ReflectionType $t
359
                                         */
360
                                        foreach ($refType->getTypes() as $t) {
×
361
                                                assert($t instanceof ReflectionType, '$t must be an instance of ReflectionType');
×
362

363
                                                if ($t instanceof ReflectionUnionType) {
×
364
                                                        yield $self($t, $self)->toList();
×
365
                                                } elseif ($t instanceof ReflectionIntersectionType) {
×
366
                                                        yield [$self($t, $self)->toList()];
×
367
                                                } elseif ($t instanceof ReflectionNamedType) {
×
368
                                                        yield [$t->getName()];
×
369
                                                } else {
370
                                                        throw new ReflectionException('Unsupported ReflectionType: ' . get_debug_type($t));
×
371
                                                }
372
                                        }
373
                                });
×
374
                        };
375

376
                        $allowedTypes = $extractTypeNames($type, $extractTypeNames)->select(static function (string|array $t): string|array {
×
377
                                if (!is_array($t)) {
×
378
                                        return $t;
×
379
                                }
380

381
                                if (count($t) === 1) {
×
382
                                        return $t[0];
×
383
                                }
384

385
                                return collect(...$t)->flatten()->toList();
×
386
                        });
×
387
                } else {
388
                        /** @var ReflectionNamedType $type */
389
                        $allowedTypes = [$type->getName()];
1✔
390
                }
391

392
                /** @var list<class-string|list<class-string>> $allowedTypes */
393
                foreach ($allowedTypes as $typeName) {
1✔
394
                        if (is_string($typeName)) {
1✔
395
                                if (!$this->has($typeName)) {
1✔
396
                                        continue;
×
397
                                }
398

399
                                return $this->resolveService($typeName, $parameter->getDeclaringFunction()->getName());
1✔
400
                        }
401

402
                        if (is_array($typeName)) {
×
403
                                /** @var class-string $combinedTypeName */
404
                                $combinedTypeName = implode('&', $typeName);
×
405

406
                                if (!$this->has($combinedTypeName)) {
×
407
                                        continue;
×
408
                                }
409

410
                                return $this->resolveService($combinedTypeName, $parameter->getDeclaringFunction()->getName());
×
411
                        }
412
                }
413

414
                if ($parameter->isDefaultValueAvailable()) {
×
415
                        return $parameter->getDefaultValue();
×
416
                }
417

418
                if ($parameter->allowsNull()) {
×
419
                        return null;
×
420
                }
421

422
                throw new UnresolvedParameterException(
×
423
                        $parameter->getDeclaringClass()?->getShortName() ??
×
424
                        $parameter->getDeclaringFunction()->getClosureScopeClass()?->getShortName() ??
×
425
                        '<unknown class>',
×
426
                        $parameter->getDeclaringFunction()->getShortName(),
×
427
                        (string) $type,
×
428
                        $parameter->name,
×
429
                        $parameter->getDeclaringFunction()->getFileName(),
×
430
                        $parameter->getDeclaringFunction()->getStartLine(),
×
431
                        $parameter->getDeclaringFunction()->getEndLine(),
×
432
                );
×
433
        }
434

435
        /**
436
         * @template TService of object
437
         *
438
         * @param class-string<TService> $name
439
         * @param string $forMethod
440
         *
441
         * @return TService
442
         */
443
        private function resolveService(string $name, string $forMethod): object
444
        {
445
                $this->resolverStack->push("$name::$forMethod");
1✔
446

447
                $service = $this->get($name);
1✔
448

449
                $this->resolverStack->pop();
1✔
450

451
                return $service;
1✔
452
        }
453
}
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