From cc5e21b31a9b98d8da1f9e58a920bc8c1bc587eb Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Fri, 20 Mar 2026 17:49:51 -0300 Subject: [PATCH] Introduce Instantiator types (Autowire and Factory) Extract Factory (always-new instances) and Autowire (type-hint-based DI resolution) from Instantiator, replacing the mode string with polymorphism. Refactor Instantiator to lazily initialize reflection and constructor params, accept initial params in the constructor, and remove the embedded class-name parsing that now lives in Container's createInstantiator(). Container gains createInstantiator() with `new`/`autowire` modifier support, Autowire integration via offsetSet(), class_exists() check in has(), ReflectionException wrapping in getItem(), Closure-aware lazyLoad(), and set()/loadArray() handling for Instantiator/Closure values. Add AutowireTest, FactoryTest, NotFoundExceptionTest, and extend ContainerTest and InstantiatorTest to cover magic methods, deferred config, loadString/loadFile error paths, and edge cases (102 tests). --- composer.json | 2 +- src/Autowire.php | 55 ++++++ src/Container.php | 67 +++++++- src/Factory.php | 15 ++ src/Instantiator.php | 103 ++++-------- tests/AutowireTest.php | 286 ++++++++++++++++++++++++++++++++ tests/ContainerTest.php | 256 ++++++++++++++++++++++++++++ tests/FactoryTest.php | 49 ++++++ tests/InstantiatorTest.php | 86 ++++++++++ tests/NotFoundExceptionTest.php | 22 +++ 10 files changed, 867 insertions(+), 74 deletions(-) create mode 100644 src/Autowire.php create mode 100644 src/Factory.php create mode 100644 tests/AutowireTest.php create mode 100644 tests/FactoryTest.php create mode 100644 tests/NotFoundExceptionTest.php diff --git a/composer.json b/composer.json index 37833cf..6b3fbd9 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "3.0-dev" } } } diff --git a/src/Autowire.php b/src/Autowire.php new file mode 100644 index 0000000..732a240 --- /dev/null +++ b/src/Autowire.php @@ -0,0 +1,55 @@ +container = $container; + } + + /** @inheritDoc */ + protected function cleanupParams(array $params): array + { + $constructor = $this->reflection()->getConstructor(); + if ($constructor && $this->container) { + foreach ($constructor->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $this->params)) { + $params[$name] = $this->lazyLoad($params[$name] ?? null); + continue; + } + + $type = $param->getType(); + if ( + !($type instanceof ReflectionNamedType) || $type->isBuiltin() + || !$this->container->has($type->getName()) + ) { + continue; + } + + $params[$name] = $this->container->get($type->getName()); + } + + while (end($params) === null && ($key = key($params)) !== null) { + unset($params[$key]); + } + + return $params; + } + + return parent::cleanupParams($params); + } +} diff --git a/src/Container.php b/src/Container.php index b88b690..4d12029 100644 --- a/src/Container.php +++ b/src/Container.php @@ -9,12 +9,15 @@ use InvalidArgumentException; use Psr\Container\ContainerInterface; use ReflectionClass; +use ReflectionException; use ReflectionFunction; use ReflectionNamedType; use function array_filter; use function array_map; use function assert; +use function call_user_func; +use function class_exists; use function constant; use function count; use function current; @@ -47,7 +50,16 @@ public function has(string $id): bool $this->configure(); } - return parent::offsetExists($id); + if (!parent::offsetExists($id)) { + return false; + } + + $entry = $this[$id]; + if ($entry instanceof Instantiator) { + return class_exists($entry->getClassName()); + } + + return true; } public function getItem(string $name, bool $raw = false): mixed @@ -64,7 +76,11 @@ public function getItem(string $name, bool $raw = false): mixed return $this[$name]; } - return $this->lazyLoad($name); + try { + return $this->lazyLoad($name); + } catch (ReflectionException $e) { + throw new NotFoundException('Item ' . $name . ' not found: ' . $e->getMessage(), 0, $e); + } } public function get(string $id): mixed @@ -97,6 +113,12 @@ public function loadArray(array $configurator): void { foreach ($this->state() + $configurator as $key => $value) { if ($value instanceof Closure) { + $this->offsetSet((string) $key, $value); + continue; + } + + if ($value instanceof Instantiator) { + $this->offsetSet((string) $key, $value); continue; } @@ -104,6 +126,20 @@ public function loadArray(array $configurator): void } } + public function offsetSet(mixed $key, mixed $value): void + { + if ($value instanceof Autowire) { + $value->setContainer($this); + } + + parent::offsetSet($key, $value); + } + + public function set(string $name, mixed $value): void + { + $this[$name] = $value; + } + protected function configure(): void { $configurator = $this->configurator; @@ -206,7 +242,7 @@ protected function parseInstantiator(string $key, mixed $value): void } /** @var class-string $keyClass */ - $instantiator = new Instantiator($keyClass); + $instantiator = $this->createInstantiator($keyClass); if (is_array($value)) { foreach ($value as $property => $pValue) { @@ -219,6 +255,23 @@ protected function parseInstantiator(string $key, mixed $value): void $this->offsetSet($keyName, $instantiator); } + /** @param class-string $keyClass */ + protected function createInstantiator(string $keyClass): Instantiator + { + if (!str_contains($keyClass, ' ')) { + return new Instantiator($keyClass); + } + + [$modifier, $className] = explode(' ', $keyClass, 2); + + /** @var class-string $className */ + return match ($modifier) { + 'new' => new Factory($className), + 'autowire' => new Autowire($className), + default => new Instantiator($keyClass), + }; + } + protected function parseValue(mixed $value): mixed { if ($value instanceof Instantiator) { @@ -319,11 +372,15 @@ protected function parseArgumentList(string $value): array protected function lazyLoad(string $name): mixed { $callback = $this[$name]; - if ($callback instanceof Instantiator && $callback->getMode() !== Instantiator::MODE_FACTORY) { + if ($callback instanceof Instantiator && !$callback instanceof Factory) { return $this[$name] = $callback(); } - return $callback(); + if ($callback instanceof Closure) { + return $this[$name] = $callback($this); + } + + return call_user_func($callback); } public function __isset(string $name): bool diff --git a/src/Factory.php b/src/Factory.php new file mode 100644 index 0000000..e5561f3 --- /dev/null +++ b/src/Factory.php @@ -0,0 +1,15 @@ +instance = null; + + return parent::getInstance(true); + } +} diff --git a/src/Instantiator.php b/src/Instantiator.php index 847a723..3027fa9 100644 --- a/src/Instantiator.php +++ b/src/Instantiator.php @@ -10,28 +10,22 @@ use function call_user_func_array; use function count; use function end; -use function explode; use function func_get_args; use function is_array; use function is_callable; use function is_object; use function key; -use function str_contains; use function stripos; -use function strtolower; class Instantiator { - public const false MODE_DEPENDENCY = false; - public const string MODE_FACTORY = 'new'; - protected mixed $instance = null; - /** @var ReflectionClass */ - protected ReflectionClass $reflection; + /** @var ReflectionClass|null */ + protected ReflectionClass|null $reflection = null; - /** @var array */ - protected array $constructor = []; + /** @var array|null */ + protected array|null $constructor = null; /** @var array */ protected array $params = []; @@ -45,25 +39,15 @@ class Instantiator /** @var array */ protected array $propertySetters = []; - protected string|false $mode = self::MODE_DEPENDENCY; - - /** @param class-string $className */ - public function __construct(protected string $className) + /** + * @param class-string $className + * @param array $params Initial parameters (constructor, method, or property) + */ + public function __construct(protected string $className, array $params = []) { - if (str_contains(strtolower($className), ' ')) { - [$mode, $className] = explode(' ', $className, 2); - $this->mode = $mode; - /** @var class-string $className */ - $this->className = $className; + foreach ($params as $name => $value) { + $this->setParam($name, $value); } - - $this->reflection = new ReflectionClass($className); - $this->constructor = $this->findConstructorParams($this->reflection); - } - - public function getMode(): string|false - { - return $this->mode; } public function getClassName(): string @@ -73,10 +57,6 @@ public function getClassName(): string public function getInstance(bool $forceNew = false): mixed { - if ($this->mode === self::MODE_FACTORY) { - $this->instance = null; - } - if ($this->instance && !$forceNew) { return $this->instance; } @@ -98,15 +78,12 @@ static function (mixed $result) use ($className, &$instance, $staticMethods): vo ); } - $constructor = $this->reflection->getConstructor(); - $hasConstructor = $constructor ? $constructor->isPublic() : false; if (empty($instance)) { - if (empty($this->constructor) || !$hasConstructor) { - $instance = new $className(); + $constructorParams = $this->cleanupParams($this->constructor ?? []); + if (empty($constructorParams)) { + $instance = $this->reflection()->newInstance(); } else { - $instance = $this->reflection->newInstanceArgs( - $this->cleanupParams($this->constructor), - ); + $instance = $this->reflection()->newInstanceArgs($constructorParams); } } @@ -156,6 +133,12 @@ public function getParams(): array return $this->params; } + /** @return ReflectionClass */ + protected function reflection(): ReflectionClass + { + return $this->reflection ??= new ReflectionClass($this->className); + } + /** * @param array $params * @@ -163,12 +146,7 @@ public function getParams(): array */ protected function cleanupParams(array $params): array { - while (end($params) === null) { - $key = key($params); - if ($key === null) { - break; - } - + while (end($params) === null && ($key = key($params)) !== null) { unset($params[$key]); } @@ -184,27 +162,6 @@ protected function lazyLoad(mixed $value): mixed return $value instanceof self ? $value->getInstance() : $value; } - /** - * @param ReflectionClass $class - * - * @return array - */ - protected function findConstructorParams(ReflectionClass $class): array - { - $params = []; - $constructor = $class->getConstructor(); - - if (!$constructor) { - return []; - } - - foreach ($constructor->getParameters() as $param) { - $params[$param->getName()] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; - } - - return $params; - } - protected function processValue(mixed $value): mixed { if (is_array($value)) { @@ -218,6 +175,16 @@ protected function processValue(mixed $value): mixed protected function matchConstructorParam(string $name): bool { + if ($this->constructor === null) { + $this->constructor = []; + $ctor = $this->reflection()->getConstructor(); + if ($ctor) { + foreach ($ctor->getParameters() as $param) { + $this->constructor[$param->getName()] = null; + } + } + } + return array_key_exists($name, $this->constructor); } @@ -229,13 +196,13 @@ protected function matchFullConstructor(string $name): bool protected function matchMethod(string $name): bool { - return $this->reflection->hasMethod($name); + return $this->reflection()->hasMethod($name); } protected function matchStaticMethod(string $name): bool { - return $this->reflection->hasMethod($name) - && $this->reflection->getMethod($name)->isStatic(); + return $this->reflection()->hasMethod($name) + && $this->reflection()->getMethod($name)->isStatic(); } /** @param array{string, mixed} $methodCalls */ diff --git a/tests/AutowireTest.php b/tests/AutowireTest.php new file mode 100644 index 0000000..6c11954 --- /dev/null +++ b/tests/AutowireTest.php @@ -0,0 +1,286 @@ +setContainer($container); + $instance = $autowire->getInstance(); + + $this->assertInstanceOf(AutowireConsumer::class, $instance); + $this->assertSame($date, $instance->date); + } + + public function testExplicitParamTakesPrecedenceOverAutowired(): void + { + $container = new Container(); + $containerDate = new DateTime('2024-01-01'); + $explicitDate = new DateTime('2025-06-15'); + $container['DateTime'] = $containerDate; + + $autowire = new Autowire(AutowireConsumer::class); + $autowire->setContainer($container); + $autowire->setParam('date', $explicitDate); + $instance = $autowire->getInstance(); + + $this->assertSame($explicitDate, $instance->date); + } + + public function testAutowireWithoutContainerFallsBackToInstantiator(): void + { + $autowire = new Autowire(stdClass::class); + $autowire->setParam('foo', 'bar'); + $instance = $autowire->getInstance(); + + $this->assertInstanceOf(stdClass::class, $instance); + $this->assertEquals('bar', $instance->foo); + } + + public function testBuiltinTypeHintsAreNotAutowired(): void + { + $container = new Container(); + $container['string'] = 'hello'; + + $autowire = new Autowire(AutowireWithBuiltin::class); + $autowire->setContainer($container); + $autowire->setParam('name', 'world'); + $instance = $autowire->getInstance(); + + $this->assertEquals('world', $instance->name); + } + + public function testAutowireMultipleParams(): void + { + $container = new Container(); + $date = new DateTime('2024-01-15'); + $container['DateTime'] = $date; + $container[AutowireDependency::class] = new AutowireDependency('injected'); + + $autowire = new Autowire(AutowireMultiParam::class); + $autowire->setContainer($container); + $instance = $autowire->getInstance(); + + $this->assertSame($date, $instance->date); + $this->assertEquals('injected', $instance->dep->value); + } + + public function testAutowireMixedExplicitAndResolved(): void + { + $container = new Container(); + $container[AutowireDependency::class] = new AutowireDependency('auto'); + + $autowire = new Autowire(AutowireMultiParam::class); + $autowire->setContainer($container); + $autowire->setParam('date', new DateTime('2025-01-01')); + $instance = $autowire->getInstance(); + + $this->assertEquals('2025-01-01', $instance->date->format('Y-m-d')); + $this->assertEquals('auto', $instance->dep->value); + } + + public function testAutowireSkipsParamNotInContainer(): void + { + $container = new Container(); + // Container has DateTime but NOT AutowireDependency + + $date = new DateTime(); + $container['DateTime'] = $date; + + $autowire = new Autowire(AutowireOptionalDep::class); + $autowire->setContainer($container); + $instance = $autowire->getInstance(); + + $this->assertSame($date, $instance->date); + $this->assertNull($instance->dep); + } + + public function testAutowireViaContainerIniSyntax(): void + { + $ini = <<<'INI' +[Respect\Config\AutowireDependency Respect\Config\AutowireDependency] +value = from_config + +[consumer autowire Respect\Config\AutowireTypedConsumer] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + + $consumer = $c->getItem('consumer'); + $this->assertInstanceOf(AutowireTypedConsumer::class, $consumer); + $this->assertInstanceOf(AutowireDependency::class, $consumer->dep); + $this->assertEquals('from_config', $consumer->dep->value); + } + + public function testAutowireViaContainerWithExplicitOverride(): void + { + $ini = <<<'INI' +[Respect\Config\AutowireDependency Respect\Config\AutowireDependency] +value = default + +[dep2 Respect\Config\AutowireDependency] +value = explicit + +[consumer autowire Respect\Config\AutowireTypedConsumer] +dep = [dep2] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + + $consumer = $c->getItem('consumer'); + $this->assertEquals('explicit', $consumer->dep->value); + } + + public function testAutowireNestedInstantiatorAsParam(): void + { + $container = new Container(); + + $inner = new Instantiator(AutowireDependency::class); + $inner->setParam('value', 'lazy'); + + $autowire = new Autowire(AutowireTypedConsumer::class); + $autowire->setContainer($container); + $autowire->setParam('dep', $inner); + $instance = $autowire->getInstance(); + + $this->assertEquals('lazy', $instance->dep->value); + } + + public function testContainerSetsContainerOnAutowireViaOffsetSet(): void + { + $container = new Container(); + $dep = new AutowireDependency('hello'); + $container[AutowireDependency::class] = $dep; + + $autowire = new Autowire(AutowireTypedConsumer::class); + $container['consumer'] = $autowire; + + $consumer = $container->getItem('consumer'); + $this->assertSame($dep, $consumer->dep); + } + + public function testAutowireSingleton(): void + { + $container = new Container(); + $container['DateTime'] = new DateTime(); + + $autowire = new Autowire(AutowireConsumer::class); + $autowire->setContainer($container); + + $first = $autowire->getInstance(); + $second = $autowire->getInstance(); + $this->assertSame($first, $second); + } + + public function testAutowireStripsAllNullParams(): void + { + $container = new Container(); + $autowire = new Autowire(AutowireAllOptional::class); + $autowire->setContainer($container); + $instance = $autowire->getInstance(); + $this->assertInstanceOf(AutowireAllOptional::class, $instance); + $this->assertNull($instance->a); + $this->assertNull($instance->b); + } + + public function testExplicitNullIsNotOverriddenByAutowire(): void + { + $container = new Container(); + $container['DateTime'] = new DateTime(); + $container[AutowireDependency::class] = new AutowireDependency('from_container'); + + $autowire = new Autowire(AutowireOptionalDep::class); + $autowire->setContainer($container); + $autowire->setParam('dep', null); + + $instance = $autowire->getInstance(); + $this->assertInstanceOf(DateTime::class, $instance->date); + $this->assertNull($instance->dep, 'Explicit null must not be overridden by autowiring'); + } + + public function testAutowireWithoutContainerLazyLoadsParams(): void + { + $inner = new Instantiator(AutowireDependency::class); + $inner->setParam('value', 'lazy_loaded'); + + $autowire = new Autowire(AutowireTypedConsumer::class); + // No setContainer call — falls back to lazyLoad path + $autowire->setParam('dep', $inner); + $instance = $autowire->getInstance(); + $this->assertEquals('lazy_loaded', $instance->dep->value); + } + + /** @return array */ + private static function parseIni(string $ini): array + { + $result = parse_ini_string($ini, true); + self::assertIsArray($result); + + return $result; + } +} + +class AutowireConsumer +{ + public function __construct(public DateTime $date) + { + } +} + +class AutowireWithBuiltin +{ + public function __construct(public string $name) + { + } +} + +class AutowireDependency +{ + public function __construct(public string $value = 'default') + { + } +} + +class AutowireMultiParam +{ + public function __construct(public DateTime $date, public AutowireDependency $dep) + { + } +} + +class AutowireOptionalDep +{ + public function __construct(public DateTime $date, public AutowireDependency|null $dep = null) + { + } +} + +class AutowireTypedConsumer +{ + public function __construct(public AutowireDependency $dep) + { + } +} + +class AutowireAllOptional +{ + public function __construct(public DateTime|null $a = null, public DateTime|null $b = null) + { + } +} diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 481f51b..bd127d2 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Container\NotFoundExceptionInterface; +use stdClass; use function chdir; use function class_alias; @@ -523,6 +524,251 @@ public function testClassWithAnotherAndUnderline(): void $this->assertEquals(get_class($c->getItem('foo_bar')), get_class($c->getItem('bar_foo')->test)); } + public function testIsset(): void + { + $ini = <<<'INI' +foo = bar +INI; + $c = new Container(self::parseIni($ini)); + $this->assertTrue(isset($c->foo)); + $this->assertFalse(isset($c->nonexistent)); + } + + public function testSetMethod(): void + { + $c = new Container(); + $c->set('key', 'value'); + $this->assertEquals('value', $c->getItem('key')); + } + + public function testMagicGet(): void + { + $ini = <<<'INI' +foo = bar +INI; + $c = new Container(self::parseIni($ini)); + $this->assertEquals('bar', $c->__get('foo')); + } + + public function testMagicCall(): void + { + $ini = <<<'INI' +foo = [undef] +bar = [foo] +INI; + $c = new Container(self::parseIni($ini)); + $result = $c->__call('bar', [['undef' => 'Hello']]); + $this->assertEquals('Hello', $result); + } + + public function testLoadString(): void + { + $ini = <<<'INI' +foo = bar +baz = bat +INI; + $c = new Container(); + $c->loadString($ini); + $this->assertEquals('bar', $c->getItem('foo')); + $this->assertEquals('bat', $c->getItem('baz')); + } + + public function testLoadStringInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid configuration string'); + $c = new Container(); + $c->loadString(''); + } + + public function testDeferredConfigWithArray(): void + { + $c = new Container(['foo' => 'bar']); + $this->assertEquals('bar', $c->getItem('foo')); + } + + public function testDeferredConfigWithIniString(): void + { + $c = new Container("foo = bar\nbaz = bat"); + $this->assertEquals('bar', $c->getItem('foo')); + $this->assertEquals('bat', $c->getItem('baz')); + } + + public function testDeferredConfigWithFile(): void + { + $c = new Container($this->vfsRoot . '/exists.ini'); + $this->assertEquals('bar', $c->getItem('foo')); + } + + public function testHasReturnsFalseForNonExistentClass(): void + { + $ini = <<<'INI' +[foo Respect\Config\NonExistentClass12345] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $this->assertFalse($c->has('foo')); + } + + public function testHasReturnsTrueForValidInstantiator(): void + { + $ini = <<<'INI' +[foo DateTime] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $this->assertTrue($c->has('foo')); + } + + public function testGetItemRawReturnsInstantiator(): void + { + $ini = <<<'INI' +[foo DateTime] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $raw = $c->getItem('foo', true); + $this->assertInstanceOf(Instantiator::class, $raw); + } + + public function testClosureReceivesContainer(): void + { + $c = new Container(); + $c['greeting'] = 'hello'; + $c['result'] = static function (Container $container) { + return $container['greeting'] . ' world'; + }; + $this->assertEquals('hello world', $c->getItem('result')); + } + + public function testInstanceofSyntax(): void + { + $ini = <<<'INI' +[instanceof DateTime] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $this->assertInstanceOf(DateTime::class, $c->getItem('DateTime')); + } + + public function testLoadMultipleArraysMergesState(): void + { + $c = new Container(); + $c->loadArray(self::parseIni('foo = bar')); + $c->loadArray(self::parseIni('baz = bat')); + $this->assertEquals('bar', $c->getItem('foo')); + $this->assertEquals('bat', $c->getItem('baz')); + } + + public function testVariableExpansionInSequence(): void + { + $ini = <<<'INI' +name = world +greetings = [hello, [name]] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $result = $c->getItem('greetings'); + $this->assertEquals(['hello', 'world'], $result); + } + + public function testLoadFileInvalidIni(): void + { + $vfs = vfsStream::setup('bad'); + vfsStream::newFile('unreadable.ini', 0000)->at($vfs)->setContent('foo = bar'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid configuration INI file'); + $c = new Container(); + @$c->loadFile(vfsStream::url('bad') . '/unreadable.ini'); + } + + public function testLoadArrayWithInstantiatorValue(): void + { + $i = new Instantiator('stdClass'); + $i->setParam('foo', 'bar'); + $c = new Container(); + $c->loadArray(['myobj' => $i]); + $result = $c->getItem('myobj'); + $this->assertInstanceOf(stdClass::class, $result); + $this->assertEquals('bar', $result->foo); + } + + public function testLoadArrayWithClosureValue(): void + { + $c = new Container(); + $c->loadArray(['fn' => static fn() => 'result']); + $this->assertEquals('result', $c->getItem('fn')); + } + + public function testGetItemWrapsReflectionException(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('not found'); + $c = new Container(); + $i = new Instantiator(PrivateConstructorClass::class); + $i->setParam('__construct', ['x']); + $c['broken'] = $i; + $c->getItem('broken'); + } + + public function testUnknownInstantiatorModifier(): void + { + $ini = <<<'INI' +[foo unknown stdClass] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $raw = $c->getItem('foo', true); + $this->assertInstanceOf(Instantiator::class, $raw); + $this->assertEquals('unknown stdClass', $raw->getClassName()); + } + + public function testOffsetSetWithAutowire(): void + { + $c = new Container(); + $autowire = new Autowire(stdClass::class); + $c['myobj'] = $autowire; + $result = $c->getItem('myobj'); + $this->assertInstanceOf(stdClass::class, $result); + } + + public function testInvokeCallbackWithUntypedParam(): void + { + $c = new Container(); + $c(new DateTime()); + $result = $c(static function ($untyped, DateTime $date) { + return [$untyped, $date]; + }); + $this->assertNull($result[0]); + $this->assertInstanceOf(DateTime::class, $result[1]); + } + + public function testParseValuePassesInstantiatorThrough(): void + { + $inner = new Instantiator('stdClass'); + $inner->setParam('x', 'y'); + $c = new Container(); + $c->loadArray(['outer stdClass' => ['child' => $inner]]); + $result = $c->getItem('outer'); + $this->assertInstanceOf(stdClass::class, $result); + $this->assertInstanceOf(stdClass::class, $result->child); + $this->assertEquals('y', $result->child->x); + } + + public function testAutowireModifierInContainerCreatesAutowire(): void + { + $ini = <<<'INI' +[dep Respect\Config\WheneverWithAProperty] +test = hello + +[consumer autowire Respect\Config\WheneverWithAProperty] +INI; + $c = new Container(); + $c->loadArray(self::parseIni($ini)); + $raw = $c->getItem('consumer', true); + $this->assertInstanceOf(Autowire::class, $raw); + } + protected function tearDown(): void { if (!is_dir($this->originalCwd)) { @@ -598,3 +844,13 @@ class WheneverWithAProperty { public mixed $test = null; } + +class PrivateConstructorClass +{ + public string $value = ''; + + private function __construct(string $x) + { + $this->value = $x; + } +} diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php new file mode 100644 index 0000000..8eb6500 --- /dev/null +++ b/tests/FactoryTest.php @@ -0,0 +1,49 @@ +setParam('foo', 'bar'); + + $first = $factory->getInstance(); + $second = $factory->getInstance(); + + $this->assertInstanceOf(stdClass::class, $first); + $this->assertInstanceOf(stdClass::class, $second); + $this->assertNotSame($first, $second); + } + + public function testForceNewIsIgnoredAlwaysNew(): void + { + $factory = new Factory(stdClass::class); + + $first = $factory->getInstance(false); + $second = $factory->getInstance(false); + + $this->assertNotSame($first, $second); + } + + public function testFactoryWithConstructorParams(): void + { + $factory = new Factory('DateTime'); + $factory->setParam('datetime', '2024-06-15'); + + $first = $factory->getInstance(); + $second = $factory->getInstance(); + + $this->assertEquals('2024-06-15', $first->format('Y-m-d')); + $this->assertEquals('2024-06-15', $second->format('Y-m-d')); + $this->assertNotSame($first, $second); + } +} diff --git a/tests/InstantiatorTest.php b/tests/InstantiatorTest.php index e152307..6115b93 100644 --- a/tests/InstantiatorTest.php +++ b/tests/InstantiatorTest.php @@ -7,6 +7,7 @@ use DateTimeZone; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use stdClass; use function date_default_timezone_set; use function func_num_args; @@ -130,6 +131,91 @@ public function testMagickInvoke(): void $s = $i1(); $this->assertEquals('stdClass', get_class($s->foo)); } + + public function testGetClassName(): void + { + $i = new Instantiator('DateTime'); + $this->assertEquals('DateTime', $i->getClassName()); + } + + public function testGetParam(): void + { + $i = new Instantiator('stdClass'); + $i->setParam('foo', 'bar'); + $this->assertEquals('bar', $i->getParam('foo')); + } + + public function testGetParams(): void + { + $i = new Instantiator('stdClass'); + $i->setParam('foo', 'bar'); + $i->setParam('baz', 'bat'); + $this->assertEquals(['foo' => 'bar', 'baz' => 'bat'], $i->getParams()); + } + + public function testSetInstance(): void + { + $i = new Instantiator('stdClass'); + $obj = new stdClass(); + $obj->custom = true; + $i->setInstance($obj); + $this->assertSame($obj, $i->getInstance()); + } + + public function testConstructorWithInitialParams(): void + { + $i = new Instantiator('stdClass', ['foo' => 'bar', 'baz' => 'bat']); + $s = $i->getInstance(); + $this->assertEquals('bar', $s->foo); + $this->assertEquals('bat', $s->baz); + } + + public function testTrailingNullParamsAreStripped(): void + { + $i = new Instantiator(__NAMESPACE__ . '\\TestClass'); + $i->setParam('foo', true); + $i->setParam('bar', null); + $i->setParam('baz', null); + $s = $i->getInstance(); + $this->assertTrue($s->ok); + $this->assertNull($s->bar); + $this->assertNull($s->baz); + } + + public function testMethodCallWithSingleNonArrayArg(): void + { + $i = new Instantiator(__NAMESPACE__ . '\\TestClass'); + $i->setParam('oneParam', [true]); + $s = $i->getInstance(); + $this->assertTrue($s->ok); + } + + public function testMethodCallWithNullArg(): void + { + $i = new Instantiator(__NAMESPACE__ . '\\TestClass'); + $i->setParam('noParams', [null]); + $s = $i->getInstance(); + $this->assertTrue($s->ok); + } + + public function testStaticMethodReturningNonObject(): void + { + $i = new Instantiator(__NAMESPACE__ . '\\StaticNonObjectReturn'); + $i->setParam('init', [[]]); + $s = $i->getInstance(); + $this->assertInstanceOf(StaticNonObjectReturn::class, $s); + $this->assertTrue($s->ready); + } +} + +class StaticNonObjectReturn +{ + public bool $ready = true; + + public static function init(): string + { + return 'not_an_object'; + } } class TestClass diff --git a/tests/NotFoundExceptionTest.php b/tests/NotFoundExceptionTest.php new file mode 100644 index 0000000..a8e066f --- /dev/null +++ b/tests/NotFoundExceptionTest.php @@ -0,0 +1,22 @@ +assertInstanceOf(NotFoundExceptionInterface::class, $e); + $this->assertInstanceOf(Throwable::class, $e); + $this->assertEquals('not found', $e->getMessage()); + } +}