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

tempestphp / tempest-framework / 14322727335

07 Apr 2025 03:10PM UTC coverage: 81.259% (+0.4%) from 80.906%
14322727335

push

github

web-flow
fix(support): non-dev bun dependencies installation (#1124)

0 of 12 new or added lines in 1 file covered. (0.0%)

25 existing lines in 6 files now uncovered.

11434 of 14071 relevant lines covered (81.26%)

105.02 hits per line

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

92.74
/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(
798✔
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
    ) {}
798✔
39

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

44
        return $this;
×
45
    }
46

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

51
        return $this;
1✔
52
    }
53

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

58
        return $this;
×
59
    }
60

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

66
    public function getSingletons(): array
1✔
67
    {
68
        return $this->singletons->getArrayCopy();
1✔
69
    }
70

71
    public function getInitializers(): array
2✔
72
    {
73
        return $this->initializers->getArrayCopy();
2✔
74
    }
75

76
    public function getDynamicInitializers(): array
1✔
77
    {
78
        return $this->dynamicInitializers->getArrayCopy();
1✔
79
    }
80

81
    public function register(string $className, callable $definition): self
750✔
82
    {
83
        $this->definitions[$className] = $definition;
750✔
84

85
        return $this;
750✔
86
    }
87

88
    public function unregister(string $className): self
24✔
89
    {
90
        unset($this->definitions[$className], $this->singletons[$className]);
24✔
91

92
        return $this;
24✔
93
    }
94

95
    public function has(string $className, ?string $tag = null): bool
21✔
96
    {
97
        return isset($this->definitions[$className]) || isset($this->singletons[$this->resolveTaggedName($className, $tag)]);
21✔
98
    }
99

100
    public function singleton(string $className, mixed $definition, ?string $tag = null): self
763✔
101
    {
102
        $className = $this->resolveTaggedName($className, $tag);
763✔
103

104
        $this->singletons[$className] = $definition;
763✔
105

106
        return $this;
763✔
107
    }
108

109
    public function config(object $config): self
748✔
110
    {
111
        $this->singleton($config::class, $config);
748✔
112

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

117
        return $this;
748✔
118
    }
119

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

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

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

132
        return $dependency;
782✔
133
    }
134

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
205
        $this->stopChain();
×
206

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

210
    public function addInitializer(ClassReflector|string $initializerClass): Container
759✔
211
    {
212
        if (! ($initializerClass instanceof ClassReflector)) {
759✔
213
            $initializerClass = new ClassReflector($initializerClass);
759✔
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)) {
759✔
219
            $this->dynamicInitializers[] = $initializerClass->getName();
749✔
220

221
            return $this;
749✔
222
        }
223

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

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

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

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

237
        return $this;
757✔
238
    }
239

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

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

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

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

255
            return $instance;
758✔
256
        }
257

258
        // Check if a callable has been registered to resolve this class.
259
        if ($definition = $this->definitions[$dependencyName] ?? null) {
784✔
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) {
783✔
267
            $initializerClass = new ClassReflector($initializer);
758✔
268

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

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

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

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

282
            return $object;
758✔
283
        }
284

285
        // If we're requesting a tagged dependency and haven't resolved it at this point, something's wrong
286
        if ($tag) {
783✔
287
            throw new CannotResolveTaggedDependency($this->chain, new Dependency($className), $tag);
2✔
288
        }
289

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

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

UNCOV
300
        return null;
×
301
    }
302

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

311
        if ($initializerClass = $this->initializers[$this->resolveTaggedName($target->getName(), $tag)] ?? null) {
783✔
312
            return $this->resolve($initializerClass);
756✔
313
        }
314

315
        // Loop through the registered initializers to see if
316
        // we have something to handle this class.
317
        foreach ($this->dynamicInitializers as $initializerClass) {
775✔
318
            /** @var DynamicInitializer $initializer */
319
            $initializer = $this->resolve($initializerClass);
749✔
320

321
            if (! $initializer->canInitialize($target)) {
749✔
322
                continue;
748✔
323
            }
324

325
            return $initializer;
8✔
326
        }
327

328
        return null;
774✔
329
    }
330

331
    private function autowire(string $className, mixed ...$params): object
782✔
332
    {
333
        $classReflector = new ClassReflector($className);
782✔
334

335
        $constructor = $classReflector->getConstructor();
782✔
336

337
        if (! $classReflector->isInstantiable()) {
782✔
338
            throw new CannotInstantiateDependencyException($classReflector, $this->chain);
749✔
339
        }
340

341
        $instance = $constructor === null
780✔
342
            ? // If there isn't a constructor, don't waste time
780✔
343
            // trying to build it.
344
            $classReflector->newInstanceWithoutConstructor()
773✔
345
            : // Otherwise, use our autowireDependencies helper to automagically
780✔
346
            // build up each parameter.
347
            $classReflector->newInstanceArgs(
761✔
348
                $this->autowireDependencies($constructor, $params),
761✔
349
            );
761✔
350

351
        if (
352
            ! $classReflector->getType()->matches(Initializer::class) &&
779✔
353
                ! $classReflector->getType()->matches(DynamicInitializer::class) &&
779✔
354
                $classReflector->hasAttribute(Singleton::class)
779✔
355
        ) {
356
            $this->singleton($className, $instance);
748✔
357
        }
358

359
        foreach ($classReflector->getProperties() as $property) {
779✔
360
            if ($property->hasAttribute(Inject::class) && ! $property->isInitialized($instance)) {
763✔
361
                if ($property->hasAttribute(Lazy::class)) {
75✔
362
                    $property->set($instance, $property->getType()->asClass()->getReflection()->newLazyProxy(
1✔
363
                        fn () => $this->get($property->getType()->getName()),
1✔
364
                    ));
1✔
365
                } else {
366
                    $property->set($instance, $this->get($property->getType()->getName()));
74✔
367
                }
368
            }
369
        }
370

371
        return $instance;
779✔
372
    }
373

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

381
        $dependencies = [];
773✔
382

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

393
        return $dependencies;
768✔
394
    }
395

396
    private function autowireDependency(ParameterReflector $parameter, ?string $tag, mixed $providedValue = null): mixed
771✔
397
    {
398
        $parameterType = $parameter->getType();
771✔
399

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

406
        // Support lazy initialization
407
        $lazy = $parameter->hasAttribute(Lazy::class);
763✔
408
        // Loop through each type until we hit a match.
409
        foreach ($parameter->getType()->split() as $type) {
763✔
410
            try {
411
                return $this->autowireObjectDependency(
763✔
412
                    type: $type,
763✔
413
                    tag: $tag,
763✔
414
                    providedValue: $providedValue,
763✔
415
                    lazy: $lazy,
763✔
416
                );
763✔
417
            } catch (Throwable $throwable) {
752✔
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;
752✔
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()) {
751✔
428
            return $parameter->getDefaultValue();
747✔
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, ?string $tag, mixed $providedValue, bool $lazy): mixed
763✔
437
    {
438
        // If the provided value is of the right type,
439
        // don't waste time autowiring, return it!
440
        if ($type->accepts($providedValue)) {
763✔
441
            return $providedValue;
751✔
442
        }
443

444
        if ($lazy) {
761✔
445
            return $type
1✔
446
                ->asClass()
1✔
447
                ->getReflection()
1✔
448
                ->newLazyProxy(function () use ($type, $tag) {
1✔
449
                    return $this->resolve(className: $type->getName(), tag: $tag);
1✔
450
                });
1✔
451
        }
452

453
        // If we can successfully retrieve an instance
454
        // of the necessary dependency, return it.
455
        return $this->resolve(className: $type->getName(), tag: $tag);
761✔
456
    }
457

458
    private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed
183✔
459
    {
460
        $typeName = $parameter->getType()->getName();
183✔
461
        $tag = $parameter->getAttribute(Tag::class);
183✔
462

463
        if ($tag !== null && ($initializer = $this->initializerForBuiltin($parameter->getType(), $tag->name))) {
183✔
464
            $initializerClass = new ClassReflector($initializer);
1✔
465

466
            $object = $initializer->initialize($this->clone());
1✔
467

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

470
            if ($singleton !== null) {
1✔
471
                $this->singleton($typeName, $object, $tag->name);
1✔
472
            }
473

474
            return $object;
1✔
475
        }
476

477
        // Due to type coercion, the provided value may (or may not) work.
478
        // Here we give up trying to do type work for people. If they
479
        // didn't provide the right type, that's on them.
480
        if ($providedValue !== null) {
182✔
481
            return $providedValue;
16✔
482
        }
483

484
        // If the dependency has a default value, we might as well
485
        // use that at this point.
486
        if ($parameter->hasDefaultValue()) {
176✔
487
            return $parameter->getDefaultValue();
52✔
488
        }
489

490
        // If the dependency's type is an array or variadic variable, we'll
491
        // try to prevent an error by returning an empty array.
492
        if ($parameter->isVariadic() || $parameter->isIterable()) {
132✔
493
            return [];
1✔
494
        }
495

496
        // If the dependency's type allows null or is optional, we'll
497
        // try to prevent an error by returning null.
498
        if (! $parameter->isRequired()) {
131✔
499
            return null;
1✔
500
        }
501

502
        // At this point, there is nothing else we can do; we don't know
503
        // how to autowire this dependency.
504
        throw new CannotAutowireException($this->chain, new Dependency($parameter));
130✔
505
    }
506

507
    private function clone(): self
780✔
508
    {
509
        return clone $this;
780✔
510
    }
511

512
    private function resolveChain(): DependencyChain
792✔
513
    {
514
        if ($this->chain === null) {
792✔
515
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
792✔
516

517
            $this->chain = new DependencyChain($trace[1]['file'] . ':' . $trace[1]['line']);
792✔
518
        }
519

520
        return $this->chain;
792✔
521
    }
522

523
    private function stopChain(): void
786✔
524
    {
525
        $this->chain = null;
786✔
526
    }
527

528
    public function __clone(): void
780✔
529
    {
530
        $this->chain = $this->chain?->clone();
780✔
531
    }
532

533
    private function resolveTaggedName(string $className, ?string $tag): string
792✔
534
    {
535
        return $tag
792✔
536
            ? "{$className}#{$tag}"
754✔
537
            : $className;
792✔
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

© 2025 Coveralls, Inc