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

tempestphp / tempest-framework / 14249273116

03 Apr 2025 05:37PM UTC coverage: 81.089% (+0.02%) from 81.07%
14249273116

Pull #1105

github

web-flow
Merge c64a10e0e into b96e68dbd
Pull Request #1105: feat(core): introduce tagged configurations

40 of 47 new or added lines in 7 files covered. (85.11%)

3 existing lines in 3 files now uncovered.

11196 of 13807 relevant lines covered (81.09%)

104.42 hits per line

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

90.65
/src/Tempest/Container/src/GenericContainer.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Container;
6

7
use ArrayIterator;
8
use Closure;
9
use ReflectionFunction;
10
use Tempest\Container\Exceptions\CannotAutowireException;
11
use Tempest\Container\Exceptions\CannotInstantiateDependencyException;
12
use Tempest\Container\Exceptions\CannotResolveTaggedDependency;
13
use Tempest\Container\Exceptions\InvalidCallableException;
14
use Tempest\Reflection\ClassReflector;
15
use Tempest\Reflection\FunctionReflector;
16
use Tempest\Reflection\MethodReflector;
17
use Tempest\Reflection\ParameterReflector;
18
use Tempest\Reflection\TypeReflector;
19
use Throwable;
20
use UnitEnum;
21

22
final class GenericContainer implements Container
23
{
24
    use HasInstance;
25

26
    public function __construct(
785✔
27
        /** @var ArrayIterator<array-key, mixed> $definitions */
28
        private ArrayIterator $definitions = new ArrayIterator(),
29

30
        /** @var ArrayIterator<array-key, mixed> $singletons */
31
        private ArrayIterator $singletons = new ArrayIterator(),
32

33
        /** @var ArrayIterator<array-key, class-string> $initializers */
34
        private ArrayIterator $initializers = new ArrayIterator(),
35

36
        /** @var ArrayIterator<array-key, class-string> $dynamicInitializers */
37
        private ArrayIterator $dynamicInitializers = new ArrayIterator(),
38
        private ?DependencyChain $chain = null,
39
    ) {}
785✔
40

41
    public function setDefinitions(array $definitions): self
×
42
    {
43
        $this->definitions = new ArrayIterator($definitions);
×
44

45
        return $this;
×
46
    }
47

48
    public function setInitializers(array $initializers): self
1✔
49
    {
50
        $this->initializers = new ArrayIterator($initializers);
1✔
51

52
        return $this;
1✔
53
    }
54

55
    public function setDynamicInitializers(array $dynamicInitializers): self
×
56
    {
57
        $this->dynamicInitializers = new ArrayIterator($dynamicInitializers);
×
58

59
        return $this;
×
60
    }
61

62
    public function getDefinitions(): array
×
63
    {
64
        return $this->definitions->getArrayCopy();
×
65
    }
66

67
    public function getInitializers(): array
1✔
68
    {
69
        return $this->initializers->getArrayCopy();
1✔
70
    }
71

72
    public function getDynamicInitializers(): array
×
73
    {
74
        return $this->dynamicInitializers->getArrayCopy();
×
75
    }
76

77
    public function register(string $className, callable $definition): self
737✔
78
    {
79
        $this->definitions[$className] = $definition;
737✔
80

81
        return $this;
737✔
82
    }
83

84
    public function unregister(string $className): self
24✔
85
    {
86
        unset($this->definitions[$className], $this->singletons[$className]);
24✔
87

88
        return $this;
24✔
89
    }
90

91
    public function has(string $className, null|string|UnitEnum $tag = null): bool
21✔
92
    {
93
        return isset($this->definitions[$className]) || isset($this->singletons[$this->resolveTaggedName($className, $tag)]);
21✔
94
    }
95

96
    public function singleton(string $className, mixed $definition, null|string|UnitEnum $tag = null): self
750✔
97
    {
98
        $className = $this->resolveTaggedName($className, $tag);
750✔
99

100
        $this->singletons[$className] = $definition;
750✔
101

102
        return $this;
750✔
103
    }
104

105
    public function config(object $config): self
735✔
106
    {
107
        $tag = ($config instanceof TaggedConfig)
735✔
108
            ? $config->tag
734✔
109
            : null;
735✔
110

111
        $this->singleton($config::class, $config, $tag);
735✔
112

113
        foreach (new ClassReflector($config)->getInterfaces() as $interface) {
735✔
114
            $this->singleton($interface->getName(), $config, $tag);
734✔
115
        }
116

117
        return $this;
735✔
118
    }
119

120
    public function get(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
774✔
121
    {
122
        $this->resolveChain();
774✔
123

124
        $dependency = $this->resolve(
774✔
125
            className: $className,
774✔
126
            tag: $tag,
774✔
127
            params: $params,
774✔
128
        );
774✔
129

130
        $this->stopChain();
769✔
131

132
        return $dependency;
769✔
133
    }
134

135
    public function invoke(ClassReflector|MethodReflector|FunctionReflector|callable|string $method, mixed ...$params): mixed
747✔
136
    {
137
        if ($method instanceof ClassReflector) {
747✔
138
            $method = $method->getMethod('__invoke');
738✔
139
        }
140

141
        if ($method instanceof MethodReflector) {
747✔
142
            return $this->invokeMethod($method, ...$params);
739✔
143
        }
144

145
        if ($method instanceof FunctionReflector) {
743✔
146
            return $this->invokeFunction($method, ...$params);
×
147
        }
148

149
        if ($method instanceof Closure) {
743✔
150
            return $this->invokeClosure($method, ...$params);
25✔
151
        }
152

153
        if (is_array($method) && count($method) === 2) {
736✔
154
            return $this->invokeClosure($method(...), ...$params);
1✔
155
        }
156

157
        if (method_exists($method, '__invoke')) {
736✔
158
            return $this->invokeClosure(
735✔
159
                $this->get($method)->__invoke(...),
735✔
160
                ...$params,
735✔
161
            );
735✔
162
        }
163

164
        throw new InvalidCallableException(new Dependency($method));
1✔
165
    }
166

167
    private function invokeClosure(Closure $closure, mixed ...$params): mixed
742✔
168
    {
169
        $this->resolveChain();
742✔
170

171
        $parameters = $this->autowireDependencies(
742✔
172
            method: $reflector = new FunctionReflector($closure),
742✔
173
            parameters: $params,
742✔
174
        );
742✔
175

176
        $this->stopChain();
741✔
177

178
        return $reflector->invokeArgs($parameters);
741✔
179
    }
180

181
    private function invokeMethod(MethodReflector $method, mixed ...$params): mixed
739✔
182
    {
183
        $this->resolveChain();
739✔
184

185
        $object = $this->get($method->getDeclaringClass()->getName());
739✔
186

187
        $parameters = $this->autowireDependencies($method, $params);
739✔
188

189
        $this->stopChain();
739✔
190

191
        return $method->invokeArgs($object, $parameters);
739✔
192
    }
193

194
    private function invokeFunction(FunctionReflector|Closure $callback, mixed ...$params): mixed
×
195
    {
196
        $this->resolveChain();
×
197

198
        $reflector = match (true) {
×
199
            $callback instanceof FunctionReflector => $callback,
×
200
            default => new ReflectionFunction($callback),
×
201
        };
×
202

203
        $parameters = $this->autowireDependencies($reflector, $params);
×
204

205
        $this->stopChain();
×
206

207
        return $reflector->invokeArgs($parameters);
×
208
    }
209

210
    public function addInitializer(ClassReflector|string $initializerClass): Container
746✔
211
    {
212
        if (! ($initializerClass instanceof ClassReflector)) {
746✔
213
            $initializerClass = new ClassReflector($initializerClass);
746✔
214
        }
215

216
        // First, we check whether this is a DynamicInitializer,
217
        // which don't have a one-to-one mapping
218
        if ($initializerClass->getType()->matches(DynamicInitializer::class)) {
746✔
219
            $this->dynamicInitializers[] = $initializerClass->getName();
736✔
220

221
            return $this;
736✔
222
        }
223

224
        $initializeMethod = $initializerClass->getMethod('initialize');
744✔
225

226
        // We resolve the optional Tag attribute from this initializer class
227
        $singleton = $initializeMethod->getAttribute(Singleton::class);
744✔
228

229
        // For normal Initializers, we'll use the return type
230
        // to determine which dependency they resolve
231
        $returnType = $initializeMethod->getReturnType();
744✔
232

233
        foreach ($returnType->split() as $type) {
744✔
234
            $this->initializers[$this->resolveTaggedName($type->getName(), $singleton?->tag)] = $initializerClass->getName();
744✔
235
        }
236

237
        return $this;
744✔
238
    }
239

240
    private function resolve(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
776✔
241
    {
242
        $class = new ClassReflector($className);
776✔
243

244
        $dependencyName = $this->resolveTaggedName($className, $tag);
776✔
245

246
        // Check if the class has been registered as a singleton.
247
        if ($instance = $this->singletons[$dependencyName] ?? null) {
776✔
248
            if ($instance instanceof Closure) {
745✔
249
                $instance = $instance($this);
740✔
250
                $this->singletons[$className] = $instance;
740✔
251
            }
252

253
            $this->resolveChain()->add($class);
745✔
254

255
            return $instance;
745✔
256
        }
257

258
        // Check if a callable has been registered to resolve this class.
259
        if ($definition = $this->definitions[$dependencyName] ?? null) {
771✔
260
            $this->resolveChain()->add(new FunctionReflector($definition));
32✔
261

262
            return $definition($this);
32✔
263
        }
264

265
        // Next we check if any of our default initializers can initialize this class.
266
        if (($initializer = $this->initializerForClass($class, $tag)) !== null) {
770✔
267
            $initializerClass = new ClassReflector($initializer);
745✔
268

269
            $this->resolveChain()->add($initializerClass);
745✔
270

271
            $object = match (true) {
745✔
272
                $initializer instanceof Initializer => $initializer->initialize($this->clone()),
745✔
273
                $initializer instanceof DynamicInitializer => $initializer->initialize($class, $this->clone()),
8✔
274
            };
745✔
275

276
            $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
745✔
277

278
            if ($singleton !== null) {
745✔
279
                $this->singleton($className, $object, $tag);
737✔
280
            }
281

282
            return $object;
745✔
283
        }
284

285
        // If we're requesting a non-dynamic tagged dependency and
286
        // haven't resolved it at this point, something's wrong
287
        if ($tag !== null && ! $class->getAttribute(AllowDynamicTags::class)) {
770✔
288
            throw new CannotResolveTaggedDependency($this->chain, new Dependency($className), $this->resolveTag($tag));
2✔
289
        }
290

291
        // Finally, autowire the class.
292
        return $this->autowire($className, $params, $tag);
769✔
293
    }
294

295
    private function initializerForBuiltin(TypeReflector $target, string $tag): ?Initializer
1✔
296
    {
297
        if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) {
1✔
298
            return $this->resolve($initializerClass);
1✔
299
        }
300

301
        return null;
×
302
    }
303

304
    private function initializerForClass(ClassReflector $target, null|string|UnitEnum $tag = null): null|Initializer|DynamicInitializer
770✔
305
    {
306
        // Initializers themselves can't be initialized,
307
        // otherwise you'd end up with infinite loops
308
        if ($target->getType()->matches(Initializer::class) || $target->getType()->matches(DynamicInitializer::class)) {
770✔
309
            return null;
746✔
310
        }
311

312
        // If an initializer is registered for the specified dependency and tag, we use it.
313
        if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) {
770✔
314
            return $this->resolve($initializerClass);
743✔
315
        }
316

317
        // If the dependency allows dynamic tags, we look for the original
318
        // initializer (without tag) and we resolve it, specifying the dynamic tag.
319
        if ($target->getAttribute(AllowDynamicTags::class) && ($initializerClass = $this->initializers[$target->getName()] ?? null)) {
762✔
NEW
320
            return $this->resolve($initializerClass, $tag);
×
321
        }
322

323
        // Loop through the registered initializers to see if
324
        // we have something to handle this class.
325
        foreach ($this->dynamicInitializers as $initializerClass) {
762✔
326
            /** @var DynamicInitializer $initializer */
327
            $initializer = $this->resolve($initializerClass);
736✔
328

329
            if (! $initializer->canInitialize($target)) {
736✔
330
                continue;
735✔
331
            }
332

333
            return $initializer;
8✔
334
        }
335

336
        return null;
761✔
337
    }
338

339
    private function autowire(string $className, array $params, null|string|UnitEnum $tag): object
769✔
340
    {
341
        $classReflector = new ClassReflector($className);
769✔
342

343
        $constructor = $classReflector->getConstructor();
769✔
344

345
        if (! $classReflector->isInstantiable()) {
769✔
346
            throw new CannotInstantiateDependencyException($classReflector, $this->chain);
736✔
347
        }
348

349
        $instance = $constructor === null
767✔
350
            ? // If there isn't a constructor, don't waste time
767✔
351
            // trying to build it.
352
            $classReflector->newInstanceWithoutConstructor()
760✔
353
            : // Otherwise, use our autowireDependencies helper to automagically
767✔
354
            // build up each parameter.
355
            $classReflector->newInstanceArgs(
748✔
356
                $this->autowireDependencies($constructor, $params, $tag),
748✔
357
            );
748✔
358

359
        if (
360
            ! $classReflector->getType()->matches(Initializer::class) &&
766✔
361
                ! $classReflector->getType()->matches(DynamicInitializer::class) &&
766✔
362
                $classReflector->hasAttribute(Singleton::class)
766✔
363
        ) {
364
            $this->singleton($className, $instance);
735✔
365
        }
366

367
        foreach ($classReflector->getProperties() as $property) {
766✔
368
            // Injects to the property the specified dependency
369
            if ($property->hasAttribute(Inject::class) && ! $property->isInitialized($instance)) {
750✔
370
                if ($property->hasAttribute(Lazy::class)) {
75✔
371
                    $property->set($instance, $property->getType()->asClass()->getReflection()->newLazyProxy(
1✔
372
                        fn () => $this->get($property->getType()->getName()),
1✔
373
                    ));
1✔
374
                } else {
375
                    $property->set($instance, $this->get($property->getType()->getName()));
74✔
376
                }
377
            }
378

379
            // Injects to the property the tag the class has been resolved with
380
            if ($property->hasAttribute(CurrentTag::class) && ! $property->isInitialized($instance)) {
750✔
381
                $property->set($instance, $property->accepts(UnitEnum::class) ? $tag : $this->resolveTag($tag));
732✔
382
            }
383
        }
384

385
        return $instance;
766✔
386
    }
387

388
    /**
389
     * @return ParameterReflector[]
390
     */
391
    private function autowireDependencies(MethodReflector|FunctionReflector $method, array $parameters = [], null|string|UnitEnum $tag = null): array
760✔
392
    {
393
        $this->resolveChain()->add($method);
760✔
394

395
        $dependencies = [];
760✔
396

397
        // Build the class by iterating through its
398
        // dependencies and resolving them.
399
        foreach ($method->getParameters() as $parameter) {
760✔
400
            // If the `ForwardTag` attribute is used on a constructor parameter, we
401
            // instantiate this parameter with the current tag. Otherwise we look
402
            // for a `Tag` attribute, and if specified, we use this one instead.
403
            $dependencyTag = $parameter->getAttribute(ForwardTag::class) && $tag
758✔
NEW
404
                ? $tag
×
405
                : $parameter->getAttribute(Tag::class)?->name;
758✔
406

407
            $dependencies[] = $this->clone()->autowireDependency(
758✔
408
                parameter: $parameter,
758✔
409
                tag: $dependencyTag,
758✔
410
                providedValue: $parameters[$parameter->getName()] ?? null,
758✔
411
            );
758✔
412
        }
413

414
        return $dependencies;
755✔
415
    }
416

417
    private function autowireDependency(ParameterReflector $parameter, null|string|UnitEnum $tag, mixed $providedValue = null): mixed
758✔
418
    {
419
        $parameterType = $parameter->getType();
758✔
420

421
        // If the parameter is a built-in type, skip reflection and attempt to provide the value by
422
        // tagged initializer, a default value or null value.
423
        if ($parameterType->isBuiltin()) {
758✔
424
            return $this->autowireBuiltinDependency($parameter, $providedValue);
172✔
425
        }
426

427
        // Support lazy initialization
428
        $lazy = $parameter->hasAttribute(Lazy::class);
750✔
429
        // Loop through each type until we hit a match.
430
        foreach ($parameter->getType()->split() as $type) {
750✔
431
            try {
432
                return $this->autowireObjectDependency(
750✔
433
                    type: $type,
750✔
434
                    tag: $tag,
750✔
435
                    providedValue: $providedValue,
750✔
436
                    lazy: $lazy,
750✔
437
                );
750✔
438
            } catch (Throwable $throwable) {
739✔
439
                // We were unable to resolve the dependency for the last union
440
                // type, so we are moving on to the next one. We hang onto
441
                // the exception in case it is a circular reference.
442
                $lastThrowable = $throwable;
739✔
443
            }
444
        }
445

446
        // If the dependency has a default value, we do our best to prevent
447
        // an error by using that.
448
        if ($parameter->hasDefaultValue()) {
738✔
449
            return $parameter->getDefaultValue();
734✔
450
        }
451

452
        // At this point, there is nothing else we can do; we don't know
453
        // how to autowire this dependency.
454
        throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter));
5✔
455
    }
456

457
    private function autowireObjectDependency(TypeReflector $type, null|string|UnitEnum $tag, mixed $providedValue, bool $lazy): mixed
750✔
458
    {
459
        // If the provided value is of the right type,
460
        // don't waste time autowiring, return it!
461
        if ($type->accepts($providedValue)) {
750✔
462
            return $providedValue;
738✔
463
        }
464

465
        if ($lazy) {
748✔
466
            return $type
1✔
467
                ->asClass()
1✔
468
                ->getReflection()
1✔
469
                ->newLazyProxy(function () use ($type, $tag) {
1✔
470
                    return $this->resolve(className: $type->getName(), tag: $tag);
1✔
471
                });
1✔
472
        }
473

474
        // If we can successfully retrieve an instance
475
        // of the necessary dependency, return it.
476
        return $this->resolve(className: $type->getName(), tag: $tag);
748✔
477
    }
478

479
    private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed
172✔
480
    {
481
        $typeName = $parameter->getType()->getName();
172✔
482
        $tag = $parameter->getAttribute(Tag::class);
172✔
483

484
        if ($tag !== null && ($initializer = $this->initializerForBuiltin($parameter->getType(), $tag->name))) {
172✔
485
            $initializerClass = new ClassReflector($initializer);
1✔
486

487
            $object = $initializer->initialize($this->clone());
1✔
488

489
            $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
1✔
490

491
            if ($singleton !== null) {
1✔
492
                $this->singleton($typeName, $object, $tag->name);
1✔
493
            }
494

495
            return $object;
1✔
496
        }
497

498
        // Due to type coercion, the provided value may (or may not) work.
499
        // Here we give up trying to do type work for people. If they
500
        // didn't provide the right type, that's on them.
501
        if ($providedValue !== null) {
171✔
502
            return $providedValue;
16✔
503
        }
504

505
        // If the dependency has a default value, we might as well
506
        // use that at this point.
507
        if ($parameter->hasDefaultValue()) {
165✔
508
            return $parameter->getDefaultValue();
49✔
509
        }
510

511
        // If the dependency's type is an array or variadic variable, we'll
512
        // try to prevent an error by returning an empty array.
513
        if ($parameter->isVariadic() || $parameter->isIterable()) {
124✔
514
            return [];
1✔
515
        }
516

517
        // If the dependency's type allows null or is optional, we'll
518
        // try to prevent an error by returning null.
519
        if (! $parameter->isRequired()) {
123✔
520
            return null;
1✔
521
        }
522

523
        // At this point, there is nothing else we can do; we don't know
524
        // how to autowire this dependency.
525
        throw new CannotAutowireException($this->chain, new Dependency($parameter));
122✔
526
    }
527

528
    private function clone(): self
767✔
529
    {
530
        return clone $this;
767✔
531
    }
532

533
    private function resolveChain(): DependencyChain
779✔
534
    {
535
        if ($this->chain === null) {
779✔
536
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
779✔
537

538
            $this->chain = new DependencyChain($trace[1]['file'] . ':' . $trace[1]['line']);
779✔
539
        }
540

541
        return $this->chain;
779✔
542
    }
543

544
    private function stopChain(): void
773✔
545
    {
546
        $this->chain = null;
773✔
547
    }
548

549
    public function __clone(): void
767✔
550
    {
551
        $this->chain = $this->chain?->clone();
767✔
552
    }
553

554
    private function resolveTag(null|string|UnitEnum $tag): ?string
741✔
555
    {
556
        if ($tag instanceof UnitEnum) {
741✔
557
            return $tag->name;
1✔
558
        }
559

560
        return $tag;
741✔
561
    }
562

563
    private function resolveTaggedName(string $className, null|string|UnitEnum $tag): string
779✔
564
    {
565
        return $tag
779✔
566
            ? "{$className}#{$this->resolveTag($tag)}"
741✔
567
            : $className;
779✔
568
    }
569
}
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