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

tempestphp / tempest-framework / 12021710761

25 Nov 2024 06:54PM UTC coverage: 79.441% (-2.6%) from 81.993%
12021710761

push

github

web-flow
ci: close stale issues and pull requests

7879 of 9918 relevant lines covered (79.44%)

61.32 hits per line

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

90.19
/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

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

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

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

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

35
        /** @var ArrayIterator<array-key, class-string> $dynamicInitializers */
36
        private ArrayIterator $dynamicInitializers = new ArrayIterator(),
37
        private ?DependencyChain $chain = null,
38
    ) {
39
    }
425✔
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
1✔
78
    {
79
        $this->definitions[$className] = $definition;
1✔
80

81
        return $this;
1✔
82
    }
83

84
    public function singleton(string $className, mixed $definition, ?string $tag = null): self
394✔
85
    {
86
        $className = $this->resolveTaggedName($className, $tag);
394✔
87

88
        $this->singletons[$className] = $definition;
394✔
89

90
        return $this;
394✔
91
    }
92

93
    public function config(object $config): self
383✔
94
    {
95
        $this->singleton($config::class, $config);
383✔
96

97
        return $this;
383✔
98
    }
99

100
    public function get(string $className, ?string $tag = null, mixed ...$params): object
417✔
101
    {
102
        $this->resolveChain();
417✔
103

104
        $dependency = $this->resolve(
417✔
105
            className: $className,
417✔
106
            tag: $tag,
417✔
107
            params: $params,
417✔
108
        );
417✔
109

110
        $this->stopChain();
412✔
111

112
        return $dependency;
412✔
113
    }
114

115
    public function invoke(MethodReflector|FunctionReflector|callable|string $callable, mixed ...$params): mixed
36✔
116
    {
117
        if ($callable instanceof MethodReflector) {
36✔
118
            return $this->invokeMethod($callable, ...$params);
25✔
119
        }
120

121
        if ($callable instanceof FunctionReflector) {
11✔
122
            return $this->invokeFunction($callable, ...$params);
×
123
        }
124

125
        if ($callable instanceof Closure) {
11✔
126
            return $this->invokeClosure($callable, ...$params);
8✔
127
        }
128

129
        if (is_array($callable) && count($callable) === 2) {
5✔
130
            return $this->invokeClosure(Closure::fromCallable($callable), ...$params);
1✔
131
        }
132

133
        if (method_exists($callable, '__invoke')) {
5✔
134
            return $this->invokeClosure(
4✔
135
                Closure::fromCallable([$this->get($callable), '__invoke']),
4✔
136
                ...$params
4✔
137
            );
4✔
138
        }
139

140
        throw new InvalidCallableException(new Dependency($callable));
1✔
141
    }
142

143
    private function invokeClosure(Closure $closure, mixed ...$params): mixed
10✔
144
    {
145
        $this->resolveChain();
10✔
146

147
        $parameters = $this->autowireDependencies(
10✔
148
            method: $reflector = new FunctionReflector($closure),
10✔
149
            parameters: $params
10✔
150
        );
10✔
151

152
        $this->stopChain();
9✔
153

154
        return $reflector->invokeArgs($parameters);
9✔
155
    }
156

157
    private function invokeMethod(MethodReflector $method, mixed ...$params): mixed
25✔
158
    {
159
        $this->resolveChain();
25✔
160

161
        $object = $this->get($method->getDeclaringClass()->getName());
25✔
162

163
        $parameters = $this->autowireDependencies($method, $params);
25✔
164

165
        $this->stopChain();
25✔
166

167
        return $method->invokeArgs($object, $parameters);
25✔
168
    }
169

170
    private function invokeFunction(FunctionReflector|Closure $callback, mixed ...$params): mixed
×
171
    {
172
        $this->resolveChain();
×
173

174
        $reflector = match(true) {
×
175
            $callback instanceof FunctionReflector => $callback,
×
176
            default => new ReflectionFunction($callback),
×
177
        };
×
178

179
        $parameters = $this->autowireDependencies($reflector, $params);
×
180

181
        $this->stopChain();
×
182

183
        return $reflector->invokeArgs($parameters);
×
184
    }
185

186
    public function addInitializer(ClassReflector|string $initializerClass): Container
394✔
187
    {
188
        if (! $initializerClass instanceof ClassReflector) {
394✔
189
            $initializerClass = new ClassReflector($initializerClass);
394✔
190
        }
191

192
        // First, we check whether this is a DynamicInitializer,
193
        // which don't have a one-to-one mapping
194
        if ($initializerClass->getType()->matches(DynamicInitializer::class)) {
394✔
195
            $this->dynamicInitializers[] = $initializerClass->getName();
384✔
196

197
            return $this;
384✔
198
        }
199

200
        $initializeMethod = $initializerClass->getMethod('initialize');
392✔
201

202
        // We resolve the optional Tag attribute from this initializer class
203
        $singleton = $initializeMethod->getAttribute(Singleton::class);
392✔
204

205
        // For normal Initializers, we'll use the return type
206
        // to determine which dependency they resolve
207
        $returnType = $initializeMethod->getReturnType();
392✔
208

209
        foreach ($returnType->split() as $type) {
392✔
210
            $this->initializers[$this->resolveTaggedName($type->getName(), $singleton?->tag)] = $initializerClass->getName();
392✔
211
        }
212

213
        return $this;
392✔
214
    }
215

216
    private function resolve(string $className, ?string $tag = null, mixed ...$params): object
419✔
217
    {
218
        $class = new ClassReflector($className);
419✔
219

220
        $dependencyName = $this->resolveTaggedName($className, $tag);
419✔
221

222
        // Check if the class has been registered as a singleton.
223
        if ($instance = $this->singletons[$dependencyName] ?? null) {
419✔
224
            if ($instance instanceof Closure) {
391✔
225
                $instance = $instance($this);
387✔
226
                $this->singletons[$className] = $instance;
387✔
227
            }
228

229
            $this->resolveChain()->add($class);
391✔
230

231
            return $instance;
391✔
232
        }
233

234
        // Check if a callable has been registered to resolve this class.
235
        if ($definition = $this->definitions[$dependencyName] ?? null) {
414✔
236
            $this->resolveChain()->add(new FunctionReflector($definition));
1✔
237

238
            return $definition($this);
1✔
239
        }
240

241
        // Next we check if any of our default initializers can initialize this class.
242
        if (($initializer = $this->initializerForClass($class, $tag)) !== null) {
413✔
243
            $initializerClass = new ClassReflector($initializer);
393✔
244

245
            $this->resolveChain()->add($initializerClass);
393✔
246

247
            $object = match (true) {
393✔
248
                $initializer instanceof Initializer => $initializer->initialize($this->clone()),
393✔
249
                $initializer instanceof DynamicInitializer => $initializer->initialize($class, $this->clone()),
6✔
250
            };
393✔
251

252
            $singleton = $initializerClass->getAttribute(Singleton::class)
393✔
253
                ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
393✔
254

255
            if ($singleton !== null) {
393✔
256
                $this->singleton($className, $object, $tag);
385✔
257
            }
258

259
            return $object;
393✔
260
        }
261

262
        // If we're requesting a tagged dependency and haven't resolved it at this point, something's wrong
263
        if ($tag) {
413✔
264
            throw new CannotResolveTaggedDependency($this->chain, new Dependency($className), $tag);
2✔
265
        }
266

267
        // Finally, autowire the class.
268
        return $this->autowire($className, ...$params);
412✔
269
    }
270

271
    private function initializerForBuiltin(TypeReflector $target, string $tag): null|Initializer
1✔
272
    {
273
        if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) {
1✔
274
            return $this->resolve($initializerClass);
1✔
275
        }
276

277
        return null;
×
278
    }
279

280
    private function initializerForClass(ClassReflector $target, ?string $tag = null): null|Initializer|DynamicInitializer
413✔
281
    {
282
        // Initializers themselves can't be initialized,
283
        // otherwise you'd end up with infinite loops
284
        if ($target->getType()->matches(Initializer::class) || $target->getType()->matches(DynamicInitializer::class)) {
413✔
285
            return null;
394✔
286
        }
287

288
        if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) {
413✔
289
            return $this->resolve($initializerClass);
391✔
290
        }
291

292
        // Loop through the registered initializers to see if
293
        // we have something to handle this class.
294
        foreach ($this->dynamicInitializers as $initializerClass) {
405✔
295
            /** @var DynamicInitializer $initializer */
296
            $initializer = $this->resolve($initializerClass);
384✔
297

298
            if (! $initializer->canInitialize($target)) {
384✔
299
                continue;
383✔
300
            }
301

302
            return $initializer;
6✔
303
        }
304

305
        return null;
404✔
306
    }
307

308
    private function autowire(string $className, mixed ...$params): object
412✔
309
    {
310
        $classReflector = new ClassReflector($className);
412✔
311

312
        $constructor = $classReflector->getConstructor();
412✔
313

314
        if (! $classReflector->isInstantiable()) {
412✔
315
            throw new CannotInstantiateDependencyException($classReflector, $this->chain);
382✔
316
        }
317

318
        $instance = $constructor === null
412✔
319
            // If there isn't a constructor, don't waste time
412✔
320
            // trying to build it.
412✔
321
            ? $classReflector->newInstanceWithoutConstructor()
407✔
322

323
            // Otherwise, use our autowireDependencies helper to automagically
412✔
324
            // build up each parameter.
412✔
325
            : $classReflector->newInstanceArgs(
394✔
326
                $this->autowireDependencies($constructor, $params),
394✔
327
            );
394✔
328

329
        if (
330
            ! $classReflector->getType()->matches(Initializer::class)
411✔
331
            && ! $classReflector->getType()->matches(DynamicInitializer::class)
411✔
332
            && $classReflector->hasAttribute(Singleton::class)
411✔
333
        ) {
334
            $this->singleton($className, $instance);
383✔
335
        }
336

337
        foreach ($classReflector->getProperties() as $property) {
411✔
338
            if ($property->hasAttribute(Inject::class) && ! $property->isInitialized($instance)) {
396✔
339
                $property->set($instance, $this->get($property->getType()->getName()));
41✔
340
            }
341
        }
342

343
        return $instance;
411✔
344
    }
345

346
    /**
347
     * @return ParameterReflector[]
348
     */
349
    private function autowireDependencies(MethodReflector|FunctionReflector $method, array $parameters = []): array
405✔
350
    {
351
        $this->resolveChain()->add($method);
405✔
352

353
        $dependencies = [];
405✔
354

355
        // Build the class by iterating through its
356
        // dependencies and resolving them.
357
        foreach ($method->getParameters() as $parameter) {
405✔
358
            $dependencies[] = $this->clone()->autowireDependency(
404✔
359
                parameter: $parameter,
404✔
360
                tag: $parameter->getAttribute(Tag::class)?->name,
404✔
361
                providedValue: $parameters[$parameter->getName()] ?? null,
404✔
362
            );
404✔
363
        }
364

365
        return $dependencies;
400✔
366
    }
367

368
    private function autowireDependency(ParameterReflector $parameter, ?string $tag, mixed $providedValue = null): mixed
404✔
369
    {
370
        $parameterType = $parameter->getType();
404✔
371

372
        // If the parameter is a built-in type, skip reflection and attempt to provide the value by
373
        // tagged initializer, a default value or null value.
374
        if ($parameterType->isBuiltin()) {
404✔
375
            return $this->autowireBuiltinDependency($parameter, $providedValue);
114✔
376
        }
377

378
        // Loop through each type until we hit a match.
379
        foreach ($parameter->getType()->split() as $type) {
396✔
380

381
            try {
382
                return $this->autowireObjectDependency(
396✔
383
                    type: $type,
396✔
384
                    tag: $tag,
396✔
385
                    providedValue: $providedValue
396✔
386
                );
396✔
387
            } catch (Throwable $throwable) {
387✔
388
                // We were unable to resolve the dependency for the last union
389
                // type, so we are moving on to the next one. We hang onto
390
                // the exception in case it is a circular reference.
391
                $lastThrowable = $throwable;
387✔
392
            }
393
        }
394

395
        // If the dependency has a default value, we do our best to prevent
396
        // an error by using that.
397
        if ($parameter->hasDefaultValue()) {
386✔
398
            return $parameter->getDefaultValue();
382✔
399
        }
400

401
        // At this point, there is nothing else we can do; we don't know
402
        // how to autowire this dependency.
403
        throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter));
5✔
404
    }
405

406
    private function autowireObjectDependency(TypeReflector $type, ?string $tag, mixed $providedValue): mixed
396✔
407
    {
408
        // If the provided value is of the right type,
409
        // don't waste time autowiring, return it!
410
        if ($type->accepts($providedValue)) {
396✔
411
            return $providedValue;
4✔
412
        }
413

414
        // If we can successfully retrieve an instance
415
        // of the necessary dependency, return it.
416
        return $this->resolve(className: $type->getName(), tag: $tag);
394✔
417
    }
418

419
    private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed
114✔
420
    {
421
        $typeName = $parameter->getType()->getName();
114✔
422
        $tag = $parameter->getAttribute(Tag::class);
114✔
423

424
        if ($tag !== null && $initializer = $this->initializerForBuiltin($parameter->getType(), $tag->name)) {
114✔
425
            $initializerClass = new ClassReflector($initializer);
1✔
426

427
            $object = $initializer->initialize($this->clone());
1✔
428

429
            $singleton = $initializerClass->getAttribute(Singleton::class)
1✔
430
                ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
1✔
431

432
            if ($singleton !== null) {
1✔
433
                $this->singleton($typeName, $object, $tag->name);
1✔
434
            }
435

436
            return $object;
1✔
437
        }
438

439
        // Due to type coercion, the provided value may (or may not) work.
440
        // Here we give up trying to do type work for people. If they
441
        // didn't provide the right type, that's on them.
442
        if ($providedValue !== null) {
113✔
443
            return $providedValue;
14✔
444
        }
445

446
        // If the dependency has a default value, we might as well
447
        // use that at this point.
448
        if ($parameter->hasDefaultValue()) {
102✔
449
            return $parameter->getDefaultValue();
97✔
450
        }
451

452
        // If the dependency's type is an array or variadic variable, we'll
453
        // try to prevent an error by returning an empty array.
454
        if ($parameter->isVariadic() || $parameter->isIterable()) {
5✔
455
            return [];
1✔
456
        }
457

458
        // If the dependency's type allows null or is optional, we'll
459
        // try to prevent an error by returning null.
460
        if (! $parameter->isRequired()) {
4✔
461
            return null;
1✔
462
        }
463

464
        // At this point, there is nothing else we can do; we don't know
465
        // how to autowire this dependency.
466
        throw new CannotAutowireException($this->chain, new Dependency($parameter));
3✔
467
    }
468

469
    private function clone(): self
413✔
470
    {
471
        return clone $this;
413✔
472
    }
473

474
    private function resolveChain(): DependencyChain
422✔
475
    {
476
        if ($this->chain === null) {
422✔
477
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
422✔
478

479
            $this->chain = new DependencyChain($trace[1]['file'] . ':' . $trace[1]['line']);
422✔
480
        }
481

482
        return $this->chain;
422✔
483
    }
484

485
    private function stopChain(): void
416✔
486
    {
487
        $this->chain = null;
416✔
488
    }
489

490
    public function __clone(): void
413✔
491
    {
492
        $this->chain = $this->chain?->clone();
413✔
493
    }
494

495
    private function resolveTaggedName(string $className, ?string $tag): string
419✔
496
    {
497
        return $tag
419✔
498
            ? "{$className}#{$tag}"
388✔
499
            : $className;
419✔
500
    }
501
}
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