Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/Illuminate/Container/Attributes/Lazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Illuminate\Container\Attributes;

use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;
use ReflectionNamedType;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PARAMETER)]
class Lazy implements ContextualAttribute
{
public static function resolve(self $attribute, Container $container, ReflectionNamedType $type)
{
return proxy($type->getName(), static fn () => $container->make($type->getName()));
}
}
16 changes: 12 additions & 4 deletions src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +20,7 @@
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionParameter;
use TypeError;

Expand Down Expand Up @@ -1108,12 +1110,13 @@ protected function isBuildable($concrete, $abstract)
* @template TClass of object
*
* @param \Closure(static, array): TClass|class-string<TClass> $concrete
* @param array<class-string> $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
Expand Down Expand Up @@ -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, [$concrete]));
}

$dependencies = $constructor->getParameters();

// Once we have all the constructor's parameters we can create each of the
Expand Down Expand Up @@ -1237,8 +1244,9 @@ 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
Expand Down Expand Up @@ -1389,7 +1397,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;

Expand All @@ -1403,7 +1411,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);
}

/**
Expand Down
186 changes: 182 additions & 4 deletions tests/Container/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,7 @@
use Illuminate\Contracts\Container\SelfBuilding;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use ReflectionClass;
use stdClass;
use TypeError;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -959,7 +1053,10 @@ interface IContainerContractStub

class ContainerImplementationStub implements IContainerContractStub
{
//
public function __construct()
{
ConstructionNotices::$constructed[self::class] = true;
}
}

class ContainerImplementationStubTwo implements IContainerContractStub
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -1109,9 +1213,7 @@ class WildcardConcrete implements WildcardOnlyInterface
{
}

/*
* The order of these attributes matters because we want to ensure we only fallback to '*' when there's no more specific environment.
*/
// The order of these attributes matters because we want to ensure we only fallback to '*' when there's no more specific environment.
#[Bind(FallbackConcrete::class)]
#[Bind(ProdConcrete::class, environments: 'prod')]
interface WildcardAndProdInterface
Expand Down Expand Up @@ -1217,3 +1319,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<class-string, true> */
public static array $constructed = [];

public static function reset(): void
{
self::$constructed = [];
}
}