From 48f13a6de522e838e4a0493292f90517d4064c42 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Fri, 20 Mar 2026 21:36:55 -0300 Subject: [PATCH] Add Ref for named container references in Autowire Ref is a value object that maps a constructor parameter to a container key, enabling Autowire for cases where type-based resolution is insufficient (same interface with multiple implementations, or builtin types like array/string). --- src/Autowire.php | 8 +++- src/Instantiator.php | 5 +++ src/Ref.php | 12 ++++++ tests/AutowireTest.php | 79 ++++++++++++++++++++++++++++++++++++++ tests/InstantiatorTest.php | 10 +++++ tests/RefTest.php | 26 +++++++++++++ 6 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/Ref.php create mode 100644 tests/RefTest.php diff --git a/src/Autowire.php b/src/Autowire.php index 732a240..079f82a 100644 --- a/src/Autowire.php +++ b/src/Autowire.php @@ -28,7 +28,13 @@ protected function cleanupParams(array $params): array foreach ($constructor->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $this->params)) { - $params[$name] = $this->lazyLoad($params[$name] ?? null); + $value = $params[$name] ?? null; + if ($value instanceof Ref) { + $params[$name] = $this->container->get($value->id); + } else { + $params[$name] = $this->lazyLoad($value); + } + continue; } diff --git a/src/Instantiator.php b/src/Instantiator.php index 3027fa9..9c29627 100644 --- a/src/Instantiator.php +++ b/src/Instantiator.php @@ -2,6 +2,7 @@ namespace Respect\Config; +use InvalidArgumentException; use ReflectionClass; use function array_key_exists; @@ -159,6 +160,10 @@ protected function cleanupParams(array $params): array protected function lazyLoad(mixed $value): mixed { + if ($value instanceof Ref) { + throw new InvalidArgumentException('Ref can only be used with Autowire, not ' . static::class); + } + return $value instanceof self ? $value->getInstance() : $value; } diff --git a/src/Ref.php b/src/Ref.php new file mode 100644 index 0000000..9634465 --- /dev/null +++ b/src/Ref.php @@ -0,0 +1,12 @@ +assertEquals('lazy_loaded', $instance->dep->value); } + public function testRefResolvesClassDependencyByStringKey(): void + { + $container = new Container(); + $dep = new AutowireDependency('via_ref'); + $container['my.custom.dep'] = $dep; + + $autowire = new Autowire(AutowireTypedConsumer::class); + $autowire->setContainer($container); + $autowire->setParam('dep', new Ref('my.custom.dep')); + + $instance = $autowire->getInstance(); + $this->assertSame($dep, $instance->dep); + } + + public function testRefResolvesNonClassDependency(): void + { + $container = new Container(); + $container['app.name'] = 'MyApp'; + + $autowire = new Autowire(AutowireWithBuiltin::class); + $autowire->setContainer($container); + $autowire->setParam('name', new Ref('app.name')); + + $instance = $autowire->getInstance(); + $this->assertEquals('MyApp', $instance->name); + } + + public function testRefCoexistsWithTypeBasedAutowiring(): void + { + $container = new Container(); + $container['DateTime'] = new DateTime('2024-01-15'); + $container['custom.dep'] = new AutowireDependency('from_ref'); + + $autowire = new Autowire(AutowireMultiParam::class); + $autowire->setContainer($container); + // Only bind 'dep' via Ref; 'date' should be autowired by type + $autowire->setParam('dep', new Ref('custom.dep')); + + $instance = $autowire->getInstance(); + $this->assertInstanceOf(DateTime::class, $instance->date); + $this->assertEquals('from_ref', $instance->dep->value); + } + + public function testRefTakesPrecedenceOverTypeBasedAutowiring(): void + { + $container = new Container(); + $container['DateTime'] = new DateTime('2024-01-15'); + $container[AutowireDependency::class] = new AutowireDependency('from_type'); + $container['override.dep'] = new AutowireDependency('from_ref'); + + $autowire = new Autowire(AutowireTypedConsumer::class); + $autowire->setContainer($container); + $autowire->setParam('dep', new Ref('override.dep')); + + $instance = $autowire->getInstance(); + $this->assertEquals('from_ref', $instance->dep->value); + } + + public function testRefResolvesArrayDependency(): void + { + $container = new Container(); + $container['app.paths'] = ['/path/one', '/path/two']; + + $autowire = new Autowire(AutowireWithArray::class); + $autowire->setContainer($container); + $autowire->setParam('paths', new Ref('app.paths')); + + $instance = $autowire->getInstance(); + $this->assertEquals(['/path/one', '/path/two'], $instance->paths); + } + /** @return array */ private static function parseIni(string $ini): array { @@ -284,3 +355,11 @@ public function __construct(public DateTime|null $a = null, public DateTime|null { } } + +class AutowireWithArray +{ + /** @param array $paths */ + public function __construct(public array $paths) + { + } +} diff --git a/tests/InstantiatorTest.php b/tests/InstantiatorTest.php index 6115b93..7e72e9c 100644 --- a/tests/InstantiatorTest.php +++ b/tests/InstantiatorTest.php @@ -5,6 +5,7 @@ namespace Respect\Config; use DateTimeZone; +use InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use stdClass; @@ -198,6 +199,15 @@ public function testMethodCallWithNullArg(): void $this->assertTrue($s->ok); } + public function testRefThrowsInInstantiator(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Ref can only be used with Autowire'); + $i = new Instantiator(stdClass::class); + $i->setParam('foo', new Ref('some.key')); + $i->getInstance(); + } + public function testStaticMethodReturningNonObject(): void { $i = new Instantiator(__NAMESPACE__ . '\\StaticNonObjectReturn'); diff --git a/tests/RefTest.php b/tests/RefTest.php new file mode 100644 index 0000000..165178c --- /dev/null +++ b/tests/RefTest.php @@ -0,0 +1,26 @@ +assertEquals('some.container.key', $ref->id); + } + + public function testIdIsReadonly(): void + { + $ref = new Ref('my.key'); + $reflection = new ReflectionProperty($ref, 'id'); + $this->assertTrue($reflection->isReadOnly()); + } +}