diff --git a/src/Illuminate/Container/Attributes/Lazy.php b/src/Illuminate/Container/Attributes/Lazy.php new file mode 100644 index 000000000000..0595ecb0778d --- /dev/null +++ b/src/Illuminate/Container/Attributes/Lazy.php @@ -0,0 +1,17 @@ +getName(), static fn () => $container->make($type->getName())); + } +} diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 45242594d823..c369ff51ebb2 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -6,6 +6,7 @@ use Closure; use Exception; use Illuminate\Container\Attributes\Bind; +use Illuminate\Container\Attributes\Lazy; use Illuminate\Container\Attributes\Scoped; use Illuminate\Container\Attributes\Singleton; use Illuminate\Contracts\Container\BindingResolutionException; @@ -19,6 +20,7 @@ use ReflectionClass; use ReflectionException; use ReflectionFunction; +use ReflectionNamedType; use ReflectionParameter; use TypeError; @@ -1108,12 +1110,13 @@ protected function isBuildable($concrete, $abstract) * @template TClass of object * * @param \Closure(static, array): TClass|class-string $concrete + * @param array $withoutLazyFor * @return TClass * * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Illuminate\Contracts\Container\CircularDependencyException */ - public function build($concrete) + public function build($concrete, $withoutLazyFor = []) { // If the concrete type is actually a Closure, we will just execute it and // hand back the results of the functions, which allows functions to be @@ -1163,6 +1166,10 @@ public function build($concrete) return $instance; } + if (! in_array($concrete, $withoutLazyFor) && ! empty($reflector->getAttributes(Lazy::class))) { + return proxy($concrete, fn () => $this->build($concrete, [...$withoutLazyFor, $concrete])); + } + $dependencies = $constructor->getParameters(); // Once we have all the constructor's parameters we can create each of the @@ -1238,7 +1245,7 @@ protected function resolveDependencies(array $dependencies) $result = null; if (! is_null($attribute = Util::getContextualAttributeFromDependency($dependency))) { - $result = $this->resolveFromAttribute($attribute); + $result = $this->resolveFromAttribute($attribute, $dependency->getType()); } // If the class is null, it means the dependency is a string or some other @@ -1389,7 +1396,7 @@ protected function resolveVariadicClass(ReflectionParameter $parameter) * @param \ReflectionAttribute $attribute * @return mixed */ - public function resolveFromAttribute(ReflectionAttribute $attribute) + public function resolveFromAttribute(ReflectionAttribute $attribute, ?ReflectionNamedType $type = null) { $handler = $this->contextualAttributes[$attribute->getName()] ?? null; @@ -1403,7 +1410,7 @@ public function resolveFromAttribute(ReflectionAttribute $attribute) throw new BindingResolutionException("Contextual binding attribute [{$attribute->getName()}] has no registered handler."); } - return $handler($instance, $this); + return $handler($instance, $this, $type); } /** diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index d5dad019ea28..b5fe4dab3265 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -4,6 +4,7 @@ use Attribute; use Illuminate\Container\Attributes\Bind; +use Illuminate\Container\Attributes\Lazy; use Illuminate\Container\Attributes\Scoped; use Illuminate\Container\Attributes\Singleton; use Illuminate\Container\Container; @@ -13,6 +14,7 @@ use Illuminate\Contracts\Container\SelfBuilding; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; +use ReflectionClass; use stdClass; use TypeError; @@ -914,6 +916,98 @@ public function testWithFactoryHasDependency() $this->assertEquals('taylor@laravel.com', $r->email); } + public function testLazyObjects() + { + if (version_compare(phpversion(), '8.4.0', '<')) { + $this->markTestSkipped('Lazy objects are only available in 8.4 and later'); + } + + $container = new Container; + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + $class = $container->make(ProxyDependenciesClass::class); + $this->assertTrue((new ReflectionClass($class))->isUninitializedLazyObject($class)); + $this->assertTrue($class->stubbyIsSet()); + $this->assertFalse((new ReflectionClass($class))->isUninitializedLazyObject($class)); + } + + public function testObjectWithLazyDependencies() + { + if (version_compare(phpversion(), '8.4.0', '<')) { + $this->markTestSkipped('Lazy objects are only available in 8.4 and later'); + } + + $container = new Container; + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + $class = $container->make(ClassWithLazyDependencies::class); + $this->assertFalse((new ReflectionClass($class))->isUninitializedLazyObject($class)); + $this->assertTrue((new ReflectionClass(ContainerDependentStub::class))->isUninitializedLazyObject($class->stubby)); + $this->assertTrue($class->stubbyIsSet()); + $this->assertFalse((new ReflectionClass($class))->isUninitializedLazyObject($class)); + } + + public function testLazyObjectWithLazyDependency() + { + if (version_compare(phpversion(), '8.4.0', '<')) { + $this->markTestSkipped('Lazy objects are only available in 8.4 and later'); + } + + ConstructionNotices::reset(); + + $container = new Container; + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $class = $container->make(LazyClassWithLazyDependency::class); + // The object and its dependency are both lazy + $this->assertCount(0, ConstructionNotices::$constructed); + $this->assertTrue((new ReflectionClass($class))->isUninitializedLazyObject($class)); + + // Now we call a function on the object to bring its direct dependencies to life + $class->setValue('hello'); + + // And the object overall is not lazy + $this->assertCount(2, ConstructionNotices::$constructed); + $this->assertTrue(ConstructionNotices::$constructed[LazyClassWithLazyDependency::class]); + $this->assertFalse((new ReflectionClass($class))->isUninitializedLazyObject($class)); + // Nor is its non-lazy dependency + $this->assertTrue(ConstructionNotices::$constructed[ClassWithLazyDependencies::class]); + + // now we bring to life `$wholeClassIsLazyDependency` + $class->bringDependencyToLife('child dependency'); + $this->assertCount(4, ConstructionNotices::$constructed); + $this->assertTrue(ConstructionNotices::$constructed[ProxyDependenciesClass::class]); + // which also constructs its Container IContainerImplementationStub dependency + $this->assertTrue(ConstructionNotices::$constructed[ContainerImplementationStub::class]); + + $class->lazyAttributeDependency->bringDependencyToLife('grandchild dependency'); + $this->assertCount(5, ConstructionNotices::$constructed); + $this->assertTrue(ConstructionNotices::$constructed[ContainerDependentStub::class]); + } + + public function testLazyObjectAsSingleton() + { + if (version_compare(phpversion(), '8.4.0', '<')) { + $this->markTestSkipped('Lazy objects are only available in 8.4 and later'); + } + + ConstructionNotices::reset(); + + $container = new Container; + $container->singleton(LazyClassWithLazyDependency::class); + $class = $container->make(LazyClassWithLazyDependency::class); + + $this->assertInstanceOf(LazyClassWithLazyDependency::class, $class); + $this->assertCount(0, ConstructionNotices::$constructed); + $class2 = $container->make(LazyClassWithLazyDependency::class); + $this->assertCount(0, ConstructionNotices::$constructed); + $this->assertSame($class, $class2); + + $class->setValue('hello'); + $this->assertCount(2, ConstructionNotices::$constructed); + $this->assertTrue(ConstructionNotices::$constructed[ClassWithLazyDependencies::class]); + $this->assertTrue(ConstructionNotices::$constructed[LazyClassWithLazyDependency::class]); + $this->assertEquals('hello', $class2->value); + } + // public function testContainerCanCatchCircularDependency() // { // $this->expectException(\Illuminate\Contracts\Container\CircularDependencyException::class); @@ -959,7 +1053,10 @@ interface IContainerContractStub class ContainerImplementationStub implements IContainerContractStub { - // + public function __construct() + { + ConstructionNotices::$constructed[self::class] = true; + } } class ContainerImplementationStubTwo implements IContainerContractStub @@ -969,11 +1066,18 @@ class ContainerImplementationStubTwo implements IContainerContractStub class ContainerDependentStub { + public $value; public $impl; public function __construct(IContainerContractStub $impl) { $this->impl = $impl; + ConstructionNotices::$constructed[self::class] = true; + } + + public function setValue($value) + { + $this->value = $value; } } @@ -1217,3 +1321,79 @@ public function __construct() $this->userId = $_SERVER['__withFactory.userId']; } } + +#[Lazy] +class ProxyDependenciesClass +{ + public string $value; + + public function __construct( + public IContainerContractStub $stubby + ) { + ConstructionNotices::$constructed[self::class] = true; + } + + public function stubbyIsSet(): bool + { + return isset($this->stubby); + } + + public function setValue(string $value): void + { + $this->value = $value; + } +} + +class ClassWithLazyDependencies +{ + public function __construct( + #[Lazy] + public ContainerDependentStub $stubby + ) { + ConstructionNotices::$constructed[self::class] = true; + } + + public function stubbyIsSet(): bool + { + return isset($this->stubby); + } + + public function bringDependencyToLife($value): void + { + $this->stubby->setValue($value); + } +} + +#[Lazy] +class LazyClassWithLazyDependency +{ + public string $value; + + public function __construct( + public ProxyDependenciesClass $wholeClassIsLazyDependency, + public ClassWithLazyDependencies $lazyAttributeDependency + ) { + ConstructionNotices::$constructed[self::class] = true; + } + + public function setValue(string $value): void + { + $this->value = $value; + } + + public function bringDependencyToLife(string $value): void + { + $this->wholeClassIsLazyDependency->setValue($value); + } +} + +class ConstructionNotices +{ + /** @var array */ + public static array $constructed = []; + + public static function reset(): void + { + self::$constructed = []; + } +}