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

elephox-dev / framework / 4877852653

pending completion
4877852653

push

github

Ricardo Boss
WIP

38 of 38 new or added lines in 6 files covered. (100.0%)

3863 of 5835 relevant lines covered (66.2%)

8.55 hits per line

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

48.39
/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\OffsetNotFoundException;
12
use Elephox\DI\Contract\RootServiceProvider;
13
use Elephox\DI\Contract\ServiceScope as ServiceScopeContract;
14
use Elephox\DI\Contract\ServiceScopeFactory;
15
use Generator;
16
use LogicException;
17
use ReflectionClass;
18
use ReflectionException;
19
use ReflectionFunction;
20
use ReflectionFunctionAbstract;
21
use ReflectionIntersectionType;
22
use ReflectionNamedType;
23
use ReflectionParameter;
24
use ReflectionType;
25
use ReflectionUnionType;
26

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

153
                return $service;
4✔
154
        }
155

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

287
                        return;
×
288
                }
289

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

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

297
                                break;
×
298
                        }
299

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

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

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

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

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

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

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

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

339
                if ($type instanceof ReflectionUnionType) {
1✔
340
                        $extractTypeNames = static function (ReflectionUnionType|ReflectionIntersectionType $refType, callable $self): GenericEnumerable {
341
                                return collect(...$refType->getTypes())
×
342
                                        ->select(static function (mixed $t) use ($self): array {
×
343
                                                assert($t instanceof ReflectionType, '$t must be an instance of ReflectionType');
×
344

345
                                                /** @var Closure(ReflectionUnionType|ReflectionIntersectionType, Closure): GenericEnumerable<class-string> $self */
346
                                                if ($t instanceof ReflectionUnionType) {
×
347
                                                        return $self($t, $self)->toList();
×
348
                                                }
349

350
                                                if ($t instanceof ReflectionIntersectionType) {
×
351
                                                        return [$self($t, $self)->toList()];
×
352
                                                }
353

354
                                                if ($t instanceof ReflectionNamedType) {
×
355
                                                        return [$t->getName()];
×
356
                                                }
357

358
                                                throw new ReflectionException('Unsupported ReflectionType: ' . get_debug_type($t));
×
359
                                        });
×
360
                        };
361

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

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

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

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

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

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

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

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

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

420
                $service = $this->get($name);
1✔
421

422
                $this->resolverStack->pop();
1✔
423

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