Skip to content

Commit 78273cc

Browse files
SamMousabrendt
andauthored
feat(container): support lazy dependency initialization using lazy proxies (#1090)
Co-authored-by: Brent Roose <[email protected]>
1 parent da6665c commit 78273cc

File tree

8 files changed

+184
-2
lines changed

8 files changed

+184
-2
lines changed

src/Tempest/Container/src/GenericContainer.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,13 @@ private function autowire(string $className, mixed ...$params): object
353353

354354
foreach ($classReflector->getProperties() as $property) {
355355
if ($property->hasAttribute(Inject::class) && ! $property->isInitialized($instance)) {
356-
$property->set($instance, $this->get($property->getType()->getName()));
356+
if ($property->hasAttribute(Lazy::class)) {
357+
$property->set($instance, $property->getType()->asClass()->getReflection()->newLazyProxy(
358+
fn () => $this->get($property->getType()->getName()),
359+
));
360+
} else {
361+
$property->set($instance, $this->get($property->getType()->getName()));
362+
}
357363
}
358364
}
359365

@@ -392,13 +398,16 @@ private function autowireDependency(ParameterReflector $parameter, ?string $tag,
392398
return $this->autowireBuiltinDependency($parameter, $providedValue);
393399
}
394400

401+
// Support lazy initialization
402+
$lazy = $parameter->hasAttribute(Lazy::class);
395403
// Loop through each type until we hit a match.
396404
foreach ($parameter->getType()->split() as $type) {
397405
try {
398406
return $this->autowireObjectDependency(
399407
type: $type,
400408
tag: $tag,
401409
providedValue: $providedValue,
410+
lazy: $lazy,
402411
);
403412
} catch (Throwable $throwable) {
404413
// We were unable to resolve the dependency for the last union
@@ -419,14 +428,23 @@ private function autowireDependency(ParameterReflector $parameter, ?string $tag,
419428
throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter));
420429
}
421430

422-
private function autowireObjectDependency(TypeReflector $type, ?string $tag, mixed $providedValue): mixed
431+
private function autowireObjectDependency(TypeReflector $type, ?string $tag, mixed $providedValue, bool $lazy): mixed
423432
{
424433
// If the provided value is of the right type,
425434
// don't waste time autowiring, return it!
426435
if ($type->accepts($providedValue)) {
427436
return $providedValue;
428437
}
429438

439+
if ($lazy) {
440+
return $type
441+
->asClass()
442+
->getReflection()
443+
->newLazyProxy(function () use ($type, $tag) {
444+
return $this->resolve(className: $type->getName(), tag: $tag);
445+
});
446+
}
447+
430448
// If we can successfully retrieve an instance
431449
// of the necessary dependency, return it.
432450
return $this->resolve(className: $type->getName(), tag: $tag);

src/Tempest/Container/src/Lazy.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container;
6+
7+
use Attribute;
8+
9+
/**
10+
* Add this to an attribute that has #[Inject] or a constructor parameter to indicate
11+
* that your class might not always use this dependency.
12+
* The container may then decide to do lazy initialization
13+
*/
14+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
15+
final readonly class Lazy
16+
{
17+
}

src/Tempest/Container/tests/ContainerTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
use Tempest\Container\Tests\Fixtures\CallContainerObjectE;
2121
use Tempest\Container\Tests\Fixtures\CircularWithInitializerA;
2222
use Tempest\Container\Tests\Fixtures\CircularWithInitializerBInitializer;
23+
use Tempest\Container\Tests\Fixtures\ClassWithLazySlowDependency;
24+
use Tempest\Container\Tests\Fixtures\ClassWithLazySlowPropertyDependency;
2325
use Tempest\Container\Tests\Fixtures\ClassWithSingletonAttribute;
26+
use Tempest\Container\Tests\Fixtures\ClassWithSlowDependency;
2427
use Tempest\Container\Tests\Fixtures\ContainerObjectA;
2528
use Tempest\Container\Tests\Fixtures\ContainerObjectB;
2629
use Tempest\Container\Tests\Fixtures\ContainerObjectC;
@@ -41,6 +44,7 @@
4144
use Tempest\Container\Tests\Fixtures\OptionalTypesClass;
4245
use Tempest\Container\Tests\Fixtures\SingletonClass;
4346
use Tempest\Container\Tests\Fixtures\SingletonInitializer;
47+
use Tempest\Container\Tests\Fixtures\SlowDependency;
4448
use Tempest\Container\Tests\Fixtures\TaggedDependency;
4549
use Tempest\Container\Tests\Fixtures\TaggedDependencyCliInitializer;
4650
use Tempest\Container\Tests\Fixtures\TaggedDependencyWebInitializer;
@@ -474,4 +478,83 @@ public function test_has_tagged_singleton(): void
474478

475479
$this->assertTrue($container->has(TaggedDependency::class, 'web'));
476480
}
481+
482+
/**
483+
* @template T
484+
* @param (callable(): T) $callable
485+
* @return T
486+
*/
487+
private function assertFasterThan(callable $callable, float $seconds): mixed
488+
{
489+
$start = microtime(true);
490+
$result = $callable();
491+
$end = microtime(true);
492+
$this->assertLessThan($seconds, $end - $start);
493+
return $result;
494+
}
495+
496+
/**
497+
* @template T
498+
* @param (callable(): T) $callable
499+
* @return T
500+
*/
501+
private function assertSlowerThan(callable $callable, float $seconds): mixed
502+
{
503+
$start = microtime(true);
504+
$result = $callable();
505+
$end = microtime(true);
506+
$this->assertGreaterThan($seconds, $end - $start);
507+
return $result;
508+
}
509+
510+
public function test_lazy_dependency(): void
511+
{
512+
$container = new GenericContainer();
513+
514+
/**
515+
* Set the load time of the dependency, increasing this increases test time but makes the test more robust
516+
* At extremely low values other operations might have a bigger effect than the usleep inside the slow dependency
517+
*/
518+
$delay = 0.01;
519+
$counter = 1;
520+
521+
$container->register(SlowDependency::class, function () use ($delay, &$counter) {
522+
return new SlowDependency($delay, $counter++);
523+
});
524+
525+
// Normal example, this is slow during initialization, fast during use
526+
$instance1 = $this->assertSlowerThan(fn () => $container->get(ClassWithSlowDependency::class), $delay);
527+
$this->assertInstanceOf(ClassWithSlowDependency::class, $instance1);
528+
$this->assertInstanceOf(SlowDependency::class, $instance1->dependency);
529+
530+
$this->assertSame('value1', $this->assertFasterThan(fn () => $instance1->dependency->value, $delay));
531+
532+
// Lazy example, this is fast during initialization, slow during (first) use
533+
$instance2 = $this->assertFasterThan(fn () => $container->get(ClassWithLazySlowDependency::class), $delay);
534+
$this->assertInstanceOf(ClassWithLazySlowDependency::class, $instance2);
535+
$this->assertInstanceOf(SlowDependency::class, $instance2->dependency);
536+
537+
$this->assertSame('value2', $this->assertSlowerThan(fn () => $instance2->dependency->value, $delay));
538+
}
539+
540+
public function test_lazy_property_dependency(): void
541+
{
542+
$container = new GenericContainer();
543+
544+
/**
545+
* Set the load time of the dependency, increasing this increases test time but makes the test more robust
546+
* At extremely low values other operations might have a bigger effect than the usleep inside the slow dependency
547+
*/
548+
$delay = 0.01;
549+
$counter = 1;
550+
551+
$container->register(SlowDependency::class, function () use ($delay, &$counter) {
552+
return new SlowDependency($delay, $counter++);
553+
});
554+
555+
$instance = $this->assertFasterThan(fn () => $container->get(ClassWithLazySlowPropertyDependency::class), $delay);
556+
$this->assertInstanceOf(SlowDependency::class, $instance->dependency);
557+
558+
$this->assertSame('value1', $this->assertSlowerThan(fn () => $instance->dependency->value, $delay));
559+
}
477560
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Tempest\Container\Tests\Fixtures;
5+
6+
use Tempest\Container\Lazy;
7+
8+
final readonly class ClassWithLazySlowDependency
9+
{
10+
public function __construct(
11+
#[Lazy]
12+
public SlowDependency $dependency,
13+
) {}
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Tempest\Container\Tests\Fixtures;
5+
6+
use Tempest\Container\Inject;
7+
use Tempest\Container\Lazy;
8+
9+
final class ClassWithLazySlowPropertyDependency
10+
{
11+
#[Inject]
12+
#[Lazy]
13+
private(set) SlowDependency $dependency;
14+
15+
public function __construct() {}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Tempest\Container\Tests\Fixtures;
5+
6+
final readonly class ClassWithSlowDependency
7+
{
8+
public function __construct(
9+
public SlowDependency $dependency,
10+
) {}
11+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Tempest\Container\Tests\Fixtures;
5+
6+
final readonly class SlowDependency
7+
{
8+
public string $value;
9+
10+
public function __construct(float $delay = 0.1, $counter = 0)
11+
{
12+
// usleep apparently is buggy on windows...
13+
$start = microtime(true);
14+
while ((microtime(true) - $start) < $delay) {
15+
usleep(intval($delay * 1000000));
16+
}
17+
18+
$this->value = 'value' . $counter;
19+
}
20+
}

src/Tempest/Reflection/src/HasAttributes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ trait HasAttributes
1414
{
1515
abstract public function getReflection(): PHPReflectionClass|PHPReflectionMethod|PHPReflectionProperty|PHPReflectionParameter;
1616

17+
/**
18+
* @param class-string $name
19+
*/
1720
public function hasAttribute(string $name): bool
1821
{
1922
return $this->getReflection()->getAttributes($name) !== [];

0 commit comments

Comments
 (0)