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()); + } +}