From 34fdf6b50fa8f168fab54d9614a0a6b1cda3c982 Mon Sep 17 00:00:00 2001 From: Aleksander Kaim Date: Sun, 4 May 2025 15:28:46 +0200 Subject: [PATCH 1/2] Lazy services --- composer.json | 2 +- src/Illuminate/Container/Attributes/Lazy.php | 11 ++ src/Illuminate/Container/Container.php | 11 +- tests/Container/ContainerLazyTest.php | 133 +++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Container/Attributes/Lazy.php create mode 100644 tests/Container/ContainerLazyTest.php 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 @@ +buildStack); + if (!empty($reflector->getAttributes(Lazy::class))) { + $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; diff --git a/tests/Container/ContainerLazyTest.php b/tests/Container/ContainerLazyTest.php new file mode 100644 index 000000000000..f3b21a15077a --- /dev/null +++ b/tests/Container/ContainerLazyTest.php @@ -0,0 +1,133 @@ +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'; + } +} + +#[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(LazyWithAttributeStub $stub) + { + throw new \RuntimeException('Parent call'); + } +} + +class LazyDependencyWithoutAttributeStub +{ + public function __construct(LazyWithoutAttributeStub $stub) + { + throw new \RuntimeException('Parent call'); + } +} From 19bdded51732b0db844680ae44bf05244e150020 Mon Sep 17 00:00:00 2001 From: Aleksander Kaim Date: Mon, 26 May 2025 21:25:24 +0200 Subject: [PATCH 2/2] Lazy attribute as parameter - problem preview --- src/Illuminate/Container/Container.php | 36 +++++++++++++++++++++----- tests/Container/ContainerLazyTest.php | 15 ++++++++++- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index ba8a59d9348c..77c7ccc748e4 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -813,13 +813,14 @@ public function makeWith($abstract, array $parameters = []) * * @param string|class-string $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); } /** @@ -851,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); @@ -888,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 @@ -994,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 @@ -1059,7 +1063,9 @@ public function build($concrete) array_pop($this->buildStack); - if (!empty($reflector->getAttributes(Lazy::class))) { + $isLazy = $this->isLazy(array_merge($attributes, $reflector->getAttributes(Lazy::class))); + + if ($isLazy) { $instance = $reflector->newLazyProxy(function () use ($concrete, $instances) { return new $concrete(...$instances); }); @@ -1074,6 +1080,22 @@ public function build($concrete) 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. * @@ -1208,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 index f3b21a15077a..caa8ce03acc8 100644 --- a/tests/Container/ContainerLazyTest.php +++ b/tests/Container/ContainerLazyTest.php @@ -90,6 +90,19 @@ public function work() } } +class LazyWithTestRenameAttributeStub +{ + public function __construct() + { + throw new \RuntimeException('Lazy call'); + } + + public function work() + { + return 'work'; + } +} + #[Lazy] class LazyWithAttributeLogicStub { @@ -118,7 +131,7 @@ public function __construct() class LazyDependencyWithAttributeStub { - public function __construct(LazyWithAttributeStub $stub) + public function __construct(#[Lazy] LazyWithTestRenameAttributeStub $stub) { throw new \RuntimeException('Parent call'); }