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

tempestphp / tempest-framework / 14205916051

01 Apr 2025 09:00PM UTC coverage: 81.054% (+0.09%) from 80.964%
14205916051

Pull #1105

github

web-flow
Merge 50c7146c9 into 9c84c680a
Pull Request #1105: feat(core): introduce tagged configurations

50 of 57 new or added lines in 12 files covered. (87.72%)

3 existing lines in 3 files now uncovered.

11132 of 13734 relevant lines covered (81.05%)

103.18 hits per line

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

90.0
/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(
775✔
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
    ) {}
775✔
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
728✔
78
    {
79
        $this->definitions[$className] = $definition;
728✔
80

81
        return $this;
728✔
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
742✔
97
    {
98
        $className = $this->resolveTaggedName($className, $tag);
742✔
99

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

102
        return $this;
742✔
103
    }
104

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

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

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

117
        return $this;
728✔
118
    }
119

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

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

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

132
        return $dependency;
759✔
133
    }
134

135
    public function invoke(MethodReflector|FunctionReflector|callable|string $method, mixed ...$params): mixed
739✔
136
    {
137
        if ($method instanceof MethodReflector) {
739✔
138
            return $this->invokeMethod($method, ...$params);
43✔
139
        }
140

141
        if ($method instanceof FunctionReflector) {
738✔
142
            return $this->invokeFunction($method, ...$params);
×
143
        }
144

145
        if ($method instanceof Closure) {
738✔
146
            return $this->invokeClosure($method, ...$params);
25✔
147
        }
148

149
        if (is_array($method) && count($method) === 2) {
732✔
150
            return $this->invokeClosure($method(...), ...$params);
1✔
151
        }
152

153
        if (method_exists($method, '__invoke')) {
732✔
154
            return $this->invokeClosure(
731✔
155
                $this->get($method)->__invoke(...),
731✔
156
                ...$params,
731✔
157
            );
731✔
158
        }
159

160
        throw new InvalidCallableException(new Dependency($method));
1✔
161
    }
162

163
    private function invokeClosure(Closure $closure, mixed ...$params): mixed
737✔
164
    {
165
        $this->resolveChain();
737✔
166

167
        $parameters = $this->autowireDependencies(
737✔
168
            method: $reflector = new FunctionReflector($closure),
737✔
169
            parameters: $params,
737✔
170
        );
737✔
171

172
        $this->stopChain();
736✔
173

174
        return $reflector->invokeArgs($parameters);
736✔
175
    }
176

177
    private function invokeMethod(MethodReflector $method, mixed ...$params): mixed
43✔
178
    {
179
        $this->resolveChain();
43✔
180

181
        $object = $this->get($method->getDeclaringClass()->getName());
43✔
182

183
        $parameters = $this->autowireDependencies($method, $params);
43✔
184

185
        $this->stopChain();
43✔
186

187
        return $method->invokeArgs($object, $parameters);
43✔
188
    }
189

190
    private function invokeFunction(FunctionReflector|Closure $callback, mixed ...$params): mixed
×
191
    {
192
        $this->resolveChain();
×
193

194
        $reflector = match (true) {
×
195
            $callback instanceof FunctionReflector => $callback,
×
196
            default => new ReflectionFunction($callback),
×
197
        };
×
198

199
        $parameters = $this->autowireDependencies($reflector, $params);
×
200

201
        $this->stopChain();
×
202

203
        return $reflector->invokeArgs($parameters);
×
204
    }
205

206
    public function addInitializer(ClassReflector|string $initializerClass): Container
739✔
207
    {
208
        if (! ($initializerClass instanceof ClassReflector)) {
739✔
209
            $initializerClass = new ClassReflector($initializerClass);
739✔
210
        }
211

212
        // First, we check whether this is a DynamicInitializer,
213
        // which don't have a one-to-one mapping
214
        if ($initializerClass->getType()->matches(DynamicInitializer::class)) {
739✔
215
            $this->dynamicInitializers[] = $initializerClass->getName();
729✔
216

217
            return $this;
729✔
218
        }
219

220
        $initializeMethod = $initializerClass->getMethod('initialize');
737✔
221

222
        // We resolve the optional Tag attribute from this initializer class
223
        $singleton = $initializeMethod->getAttribute(Singleton::class);
737✔
224

225
        // For normal Initializers, we'll use the return type
226
        // to determine which dependency they resolve
227
        $returnType = $initializeMethod->getReturnType();
737✔
228

229
        foreach ($returnType->split() as $type) {
737✔
230
            $this->initializers[$this->resolveTaggedName($type->getName(), $singleton?->tag)] = $initializerClass->getName();
737✔
231
        }
232

233
        return $this;
737✔
234
    }
235

236
    private function resolve(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
766✔
237
    {
238
        $class = new ClassReflector($className);
766✔
239

240
        $dependencyName = $this->resolveTaggedName($className, $tag);
766✔
241

242
        // Check if the class has been registered as a singleton.
243
        if ($instance = $this->singletons[$dependencyName] ?? null) {
766✔
244
            if ($instance instanceof Closure) {
737✔
245
                $instance = $instance($this);
732✔
246
                $this->singletons[$className] = $instance;
732✔
247
            }
248

249
            $this->resolveChain()->add($class);
737✔
250

251
            return $instance;
737✔
252
        }
253

254
        // Check if a callable has been registered to resolve this class.
255
        if ($definition = $this->definitions[$dependencyName] ?? null) {
761✔
256
            $this->resolveChain()->add(new FunctionReflector($definition));
30✔
257

258
            return $definition($this);
30✔
259
        }
260

261
        // Next we check if any of our default initializers can initialize this class.
262
        if (($initializer = $this->initializerForClass($class, $tag)) !== null) {
760✔
263
            $initializerClass = new ClassReflector($initializer);
738✔
264

265
            $this->resolveChain()->add($initializerClass);
738✔
266

267
            $object = match (true) {
738✔
268
                $initializer instanceof Initializer => $initializer->initialize($this->clone()),
738✔
269
                $initializer instanceof DynamicInitializer => $initializer->initialize($class, $this->clone(), $this->resolveTag($tag)),
8✔
270
            };
738✔
271

272
            $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
738✔
273

274
            if ($singleton !== null) {
738✔
275
                $this->singleton($className, $object, $tag);
730✔
276
            }
277

278
            return $object;
738✔
279
        }
280

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

287
        // Finally, autowire the class.
288
        return $this->autowire($className, $params, $tag);
759✔
289
    }
290

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

297
        return null;
×
298
    }
299

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

308
        // If an initializer is registered for the specified dependency and tag, we use it.
309
        if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) {
760✔
310
            return $this->resolve($initializerClass);
736✔
311
        }
312

313
        // If the dependency allows dynamic tags, we look for the original initializer, without tags.
314
        if ($target->getAttribute(AllowDynamicTags::class) && ($initializerClass = $this->initializers[$target->getName()] ?? null)) {
752✔
NEW
315
            return $this->resolve($initializerClass, $tag);
×
316
        }
317

318
        // Loop through the registered initializers to see if
319
        // we have something to handle this class.
320
        foreach ($this->dynamicInitializers as $initializerClass) {
752✔
321
            /** @var DynamicInitializer $initializer */
322
            $initializer = $this->resolve($initializerClass);
729✔
323

324
            if (! $initializer->canInitialize($target, $this->resolveTag($tag))) {
729✔
325
                continue;
728✔
326
            }
327

328
            return $initializer;
8✔
329
        }
330

331
        return null;
751✔
332
    }
333

334
    private function autowire(string $className, array $params, null|string|UnitEnum $tag): object
759✔
335
    {
336
        $classReflector = new ClassReflector($className);
759✔
337

338
        $constructor = $classReflector->getConstructor();
759✔
339

340
        if (! $classReflector->isInstantiable()) {
759✔
341
            throw new CannotInstantiateDependencyException($classReflector, $this->chain);
729✔
342
        }
343

344
        $instance = $constructor === null
757✔
345
            ? // If there isn't a constructor, don't waste time
757✔
346
            // trying to build it.
347
            $classReflector->newInstanceWithoutConstructor()
752✔
348
            : // Otherwise, use our autowireDependencies helper to automagically
757✔
349
            // build up each parameter.
350
            $classReflector->newInstanceArgs(
739✔
351
                $this->autowireDependencies($constructor, $params, $tag),
739✔
352
            );
739✔
353

354
        if (
355
            ! $classReflector->getType()->matches(Initializer::class) &&
756✔
356
                ! $classReflector->getType()->matches(DynamicInitializer::class) &&
756✔
357
                $classReflector->hasAttribute(Singleton::class)
756✔
358
        ) {
359
            $this->singleton($className, $instance);
728✔
360
        }
361

362
        foreach ($classReflector->getProperties() as $property) {
756✔
363
            if ($property->hasAttribute(Inject::class) && ! $property->isInitialized($instance)) {
741✔
364
                $property->set($instance, $this->get($property->getType()->getName()));
74✔
365
            }
366

367
            if ($property->hasAttribute(TagName::class) && ! $property->isInitialized($instance)) {
741✔
368
                $property->set($instance, $property->accepts(UnitEnum::class) ? $tag : $this->resolveTag($tag));
725✔
369
            }
370
        }
371

372
        return $instance;
756✔
373
    }
374

375
    /**
376
     * @return ParameterReflector[]
377
     */
378
    private function autowireDependencies(MethodReflector|FunctionReflector $method, array $parameters = [], null|string|UnitEnum $tag = null): array
750✔
379
    {
380
        $this->resolveChain()->add($method);
750✔
381

382
        $dependencies = [];
750✔
383

384
        // Build the class by iterating through its
385
        // dependencies and resolving them.
386
        foreach ($method->getParameters() as $parameter) {
750✔
387
            $dependencies[] = $this->clone()->autowireDependency(
749✔
388
                parameter: $parameter,
749✔
389
                tag: $parameter->getAttribute(ForwardTag::class) && $tag
749✔
NEW
390
                    ? $tag
×
391
                    : $parameter->getAttribute(Tag::class)?->name,
749✔
392
                providedValue: $parameters[$parameter->getName()] ?? null,
749✔
393
            );
749✔
394
        }
395

396
        return $dependencies;
745✔
397
    }
398

399
    private function autowireDependency(ParameterReflector $parameter, null|string|UnitEnum $tag, mixed $providedValue = null): mixed
749✔
400
    {
401
        $parameterType = $parameter->getType();
749✔
402

403
        // If the parameter is a built-in type, skip reflection and attempt to provide the value by
404
        // tagged initializer, a default value or null value.
405
        if ($parameterType->isBuiltin()) {
749✔
406
            return $this->autowireBuiltinDependency($parameter, $providedValue);
171✔
407
        }
408

409
        // Loop through each type until we hit a match.
410
        foreach ($parameter->getType()->split() as $type) {
741✔
411
            try {
412
                return $this->autowireObjectDependency(
741✔
413
                    type: $type,
741✔
414
                    tag: $tag,
741✔
415
                    providedValue: $providedValue,
741✔
416
                );
741✔
417
            } catch (Throwable $throwable) {
732✔
418
                // We were unable to resolve the dependency for the last union
419
                // type, so we are moving on to the next one. We hang onto
420
                // the exception in case it is a circular reference.
421
                $lastThrowable = $throwable;
732✔
422
            }
423
        }
424

425
        // If the dependency has a default value, we do our best to prevent
426
        // an error by using that.
427
        if ($parameter->hasDefaultValue()) {
731✔
428
            return $parameter->getDefaultValue();
727✔
429
        }
430

431
        // At this point, there is nothing else we can do; we don't know
432
        // how to autowire this dependency.
433
        throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter));
5✔
434
    }
435

436
    private function autowireObjectDependency(TypeReflector $type, null|string|UnitEnum $tag, mixed $providedValue): mixed
741✔
437
    {
438
        // If the provided value is of the right type,
439
        // don't waste time autowiring, return it!
440
        if ($type->accepts($providedValue)) {
741✔
441
            return $providedValue;
135✔
442
        }
443

444
        // If we can successfully retrieve an instance
445
        // of the necessary dependency, return it.
446
        return $this->resolve(className: $type->getName(), tag: $tag);
739✔
447
    }
448

449
    private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed
171✔
450
    {
451
        $typeName = $parameter->getType()->getName();
171✔
452
        $tag = $parameter->getAttribute(Tag::class);
171✔
453

454
        if ($tag !== null && ($initializer = $this->initializerForBuiltin($parameter->getType(), $tag->name))) {
171✔
455
            $initializerClass = new ClassReflector($initializer);
1✔
456

457
            $object = $initializer->initialize($this->clone());
1✔
458

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

461
            if ($singleton !== null) {
1✔
462
                $this->singleton($typeName, $object, $tag->name);
1✔
463
            }
464

465
            return $object;
1✔
466
        }
467

468
        // Due to type coercion, the provided value may (or may not) work.
469
        // Here we give up trying to do type work for people. If they
470
        // didn't provide the right type, that's on them.
471
        if ($providedValue !== null) {
170✔
472
            return $providedValue;
18✔
473
        }
474

475
        // If the dependency has a default value, we might as well
476
        // use that at this point.
477
        if ($parameter->hasDefaultValue()) {
159✔
478
            return $parameter->getDefaultValue();
43✔
479
        }
480

481
        // If the dependency's type is an array or variadic variable, we'll
482
        // try to prevent an error by returning an empty array.
483
        if ($parameter->isVariadic() || $parameter->isIterable()) {
124✔
484
            return [];
1✔
485
        }
486

487
        // If the dependency's type allows null or is optional, we'll
488
        // try to prevent an error by returning null.
489
        if (! $parameter->isRequired()) {
123✔
490
            return null;
1✔
491
        }
492

493
        // At this point, there is nothing else we can do; we don't know
494
        // how to autowire this dependency.
495
        throw new CannotAutowireException($this->chain, new Dependency($parameter));
122✔
496
    }
497

498
    private function clone(): self
758✔
499
    {
500
        return clone $this;
758✔
501
    }
502

503
    private function resolveChain(): DependencyChain
769✔
504
    {
505
        if ($this->chain === null) {
769✔
506
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
769✔
507

508
            $this->chain = new DependencyChain($trace[1]['file'] . ':' . $trace[1]['line']);
769✔
509
        }
510

511
        return $this->chain;
769✔
512
    }
513

514
    private function stopChain(): void
763✔
515
    {
516
        $this->chain = null;
763✔
517
    }
518

519
    public function __clone(): void
758✔
520
    {
521
        $this->chain = $this->chain?->clone();
758✔
522
    }
523

524
    private function resolveTag(null|string|UnitEnum $tag): ?string
736✔
525
    {
526
        if ($tag instanceof UnitEnum) {
736✔
527
            return $tag->name;
1✔
528
        }
529

530
        return $tag;
736✔
531
    }
532

533
    private function resolveTaggedName(string $className, null|string|UnitEnum $tag): string
769✔
534
    {
535
        return $tag
769✔
536
            ? "{$className}#{$this->resolveTag($tag)}"
734✔
537
            : $className;
769✔
538
    }
539
}
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