diff --git a/composer.json b/composer.json index 9b424b0f4a2f..638e5b726fdf 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^8.3", + "php": "^8.4", "ext-ctype": "*", "ext-filter": "*", "ext-hash": "*", diff --git a/src/Illuminate/Container/Attributes/Lazy.php b/src/Illuminate/Container/Attributes/Lazy.php new file mode 100644 index 000000000000..03bc6657dc90 --- /dev/null +++ b/src/Illuminate/Container/Attributes/Lazy.php @@ -0,0 +1,11 @@ + $abstract * @param array $parameters + * @param ReflectionAttribute[] $attributes * @return ($abstract is class-string ? TClass : mixed) * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function make($abstract, array $parameters = []) + public function make($abstract, array $parameters = [], array $attributes = []) { - return $this->resolve($abstract, $parameters); + return $this->resolve($abstract, $parameters, attributes: $attributes); } /** @@ -850,12 +852,14 @@ public function get(string $id) * @param string|class-string|callable $abstract * @param array $parameters * @param bool $raiseEvents + * @param ReflectionAttribute[] $attributes + * * @return ($abstract is class-string ? TClass : mixed) * * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Illuminate\Contracts\Container\CircularDependencyException */ - protected function resolve($abstract, $parameters = [], $raiseEvents = true) + protected function resolve($abstract, $parameters = [], $raiseEvents = true, array $attributes = []) { $abstract = $this->getAlias($abstract); @@ -887,7 +891,7 @@ protected function resolve($abstract, $parameters = [], $raiseEvents = true) // the binding. This will instantiate the types, as well as resolve any of // its "nested" dependencies recursively until all have gotten resolved. $object = $this->isBuildable($concrete, $abstract) - ? $this->build($concrete) + ? $this->build($concrete, $attributes) : $this->make($concrete); // If we defined any extenders for this type, we'll need to spin through them @@ -993,12 +997,13 @@ protected function isBuildable($concrete, $abstract) * @template TClass of object * * @param \Closure(static, array): TClass|class-string $concrete + * @param ReflectionAttribute[] $attributes * @return TClass * * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Illuminate\Contracts\Container\CircularDependencyException */ - public function build($concrete) + public function build($concrete, array $attributes = []) { // 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 @@ -1058,13 +1063,39 @@ public function build($concrete) array_pop($this->buildStack); + $isLazy = $this->isLazy(array_merge($attributes, $reflector->getAttributes(Lazy::class))); + + if ($isLazy) { + $instance = $reflector->newLazyProxy(function () use ($concrete, $instances) { + return new $concrete(...$instances); + }); + } else { + $instance = $reflector->newInstanceArgs($instances); + } + $this->fireAfterResolvingAttributeCallbacks( - $reflector->getAttributes(), $instance = $reflector->newInstanceArgs($instances) + $reflector->getAttributes(), $instance ); return $instance; } + /** + * @template TClass of object + * + * @param ReflectionAttribute[] $attributes + * + * @return bool + */ + protected function isLazy(array $attributes): bool + { + if (empty($attributes)) { + return false; + } + + return array_any($attributes, static fn($attribute) => $attribute->getName() === Lazy::class); + } + /** * Resolve all of the dependencies from the ReflectionParameters. * @@ -1199,7 +1230,7 @@ protected function resolveClass(ReflectionParameter $parameter) try { return $parameter->isVariadic() ? $this->resolveVariadicClass($parameter) - : $this->make($className); + : $this->make($className, $parameter->getAttributes(), $parameter->getAttributes()); } // If we can not resolve the class instance, we will check to see if the value diff --git a/tests/Container/ContainerLazyTest.php b/tests/Container/ContainerLazyTest.php new file mode 100644 index 000000000000..caa8ce03acc8 --- /dev/null +++ b/tests/Container/ContainerLazyTest.php @@ -0,0 +1,146 @@ +make(LazyWithAttributeStub::class); + + // No RuntimeException has occurred + // LazyWithAttributeStub behaves like a Lazy Object, but this is not obvious from its type + $this->assertInstanceOf(LazyWithAttributeStub::class, $lazy); + } + + public function testConcreteThrowsExceptionWithoutAttribute() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Lazy call'); + + $container = new Container; + $container->make(LazyWithoutAttributeStub::class); + } + + public function testConcreteDoesNotThrowsExceptionWithNoLogicConstructor() + { + $container = new Container; + $lazy = $container->make(LazyWithAttributeStub::class); + + $this->assertInstanceOf(LazyWithAttributeStub::class, $lazy); + + $this->assertSame('work', $lazy->work()); + } + + public function testConcreteDoesThrowsExceptionWithConstructorWithLogic() + { + $container = new Container; + $lazy = $container->make(LazyWithAttributeLogicStub::class); + + // No RuntimeException has occurred so far + $this->assertInstanceOf(LazyWithAttributeLogicStub::class, $lazy); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Lazy call'); + + // Only the call to number() causes a RuntimeException + $lazy->number(); + } + + public function testConcreteThrowsExceptionButNotLazyDependency() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Parent call'); + + $container = new Container; + $container->make(LazyDependencyWithAttributeStub::class); + } + + public function testConcreteNotLazyDependencyThrowsException() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Lazy call'); + + $container = new Container; + $container->make(LazyDependencyWithoutAttributeStub::class); + } +} + +#[Lazy] +class LazyWithAttributeStub +{ + public function __construct() + { + throw new \RuntimeException('Lazy call'); + } + + public function work() + { + return 'work'; + } +} + +class LazyWithTestRenameAttributeStub +{ + public function __construct() + { + throw new \RuntimeException('Lazy call'); + } + + public function work() + { + return 'work'; + } +} + +#[Lazy] +class LazyWithAttributeLogicStub +{ + public $number; + + public function __construct() + { + $this->number = 10; + + throw new \RuntimeException('Lazy call'); + } + + public function number() + { + $this->number += 10; + } +} + +class LazyWithoutAttributeStub +{ + public function __construct() + { + throw new \RuntimeException('Lazy call'); + } +} + +class LazyDependencyWithAttributeStub +{ + public function __construct(#[Lazy] LazyWithTestRenameAttributeStub $stub) + { + throw new \RuntimeException('Parent call'); + } +} + +class LazyDependencyWithoutAttributeStub +{ + public function __construct(LazyWithoutAttributeStub $stub) + { + throw new \RuntimeException('Parent call'); + } +}