diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4f95a7d..857c736 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,4 +12,4 @@ parameters: - message: '/Access to an undefined property Respect\\Config\\Container::\$/' path: tests/LazyLoadTest.php - count: 2 + count: 1 diff --git a/src/Container.php b/src/Container.php index 4d12029..13fd4b3 100644 --- a/src/Container.php +++ b/src/Container.php @@ -6,7 +6,6 @@ use ArrayObject; use Closure; -use InvalidArgumentException; use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; @@ -18,38 +17,24 @@ use function assert; use function call_user_func; use function class_exists; -use function constant; -use function count; -use function current; -use function defined; -use function explode; -use function file_exists; use function func_get_args; use function is_array; use function is_callable; -use function is_object; use function is_string; -use function parse_ini_file; -use function parse_ini_string; -use function preg_match; -use function preg_replace; -use function preg_replace_callback; -use function str_contains; -use function trim; /** @extends ArrayObject */ class Container extends ArrayObject implements ContainerInterface { - public function __construct(protected mixed $configurator = null) + /** @param array $definitions */ + public function __construct(array $definitions = []) { + foreach ($definitions as $key => $value) { + $this->offsetSet((string) $key, $value); + } } public function has(string $id): bool { - if ($this->configurator) { - $this->configure(); - } - if (!parent::offsetExists($id)) { return false; } @@ -64,10 +49,6 @@ public function has(string $id): bool public function getItem(string $name, bool $raw = false): mixed { - if ($this->configurator) { - $this->configure(); - } - if (!isset($this[$name])) { throw new NotFoundException('Item ' . $name . ' not found'); } @@ -88,44 +69,6 @@ public function get(string $id): mixed return $this->getItem($id); } - public function loadString(string $configurator): void - { - $iniData = parse_ini_string($configurator, true); - if ($iniData === false || count($iniData) === 0) { - throw new InvalidArgumentException('Invalid configuration string'); - } - - $this->loadArray($iniData); - } - - public function loadFile(string $configurator): void - { - $iniData = parse_ini_file($configurator, true); - if ($iniData === false) { - throw new InvalidArgumentException('Invalid configuration INI file'); - } - - $this->loadArray($iniData); - } - - /** @param array $configurator */ - 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; - } - - $this->parseItem($key, $value); - } - } - public function offsetSet(mixed $key, mixed $value): void { if ($value instanceof Autowire) { @@ -140,235 +83,6 @@ public function set(string $name, mixed $value): void $this[$name] = $value; } - protected function configure(): void - { - $configurator = $this->configurator; - $this->configurator = null; - - if ($configurator === null) { - return; - } - - if (is_array($configurator)) { - $this->loadArray($configurator); - - return; - } - - if (is_string($configurator) && file_exists($configurator)) { - $this->loadFile($configurator); - - return; - } - - if (is_string($configurator)) { - $this->loadString($configurator); - - return; - } - - throw new InvalidArgumentException('Invalid input. Must be a valid file or array'); - } - - /** @return array */ - protected function state(): array - { - return array_filter( - $this->getArrayCopy(), - static fn($v): bool => !is_object($v) || !$v instanceof Instantiator, - ); - } - - protected function keyHasStateInstance(string $key, mixed &$k): bool - { - return $this->offsetExists($k = current(explode(' ', $key))); - } - - protected function keyHasInstantiator(string $key): bool - { - return str_contains($key, ' '); - } - - protected function parseItem(string|int $key, mixed $value): void - { - $key = trim((string) $key); - if ($this->keyHasInstantiator($key)) { - if ($this->keyHasStateInstance($key, $k)) { - $this->offsetSet($key, $this[$k]); - } else { - $this->parseInstantiator($key, $value); - } - } else { - $this->parseStandardItem($key, $value); - } - } - - /** - * @param array $value - * - * @return array - */ - protected function parseSubValues(array &$value): array - { - foreach ($value as &$subValue) { - $subValue = $this->parseValue($subValue); - } - - return $value; - } - - protected function parseStandardItem(string $key, mixed &$value): void - { - if (is_array($value)) { - $this->parseSubValues($value); - } else { - $value = $this->parseValue($value); - } - - $this->offsetSet($key, $value); - } - - protected function removeDuplicatedSpaces(string $string): string - { - return (string) preg_replace('/\s+/', ' ', $string); - } - - protected function parseInstantiator(string $key, mixed $value): void - { - $key = $this->removeDuplicatedSpaces($key); - [$keyName, $keyClass] = explode(' ', $key, 2); - if ($keyName === 'instanceof') { - $keyName = $keyClass; - } - - /** @var class-string $keyClass */ - $instantiator = $this->createInstantiator($keyClass); - - if (is_array($value)) { - foreach ($value as $property => $pValue) { - $instantiator->setParam($property, $this->parseValue($pValue)); - } - } else { - $instantiator->setParam('__construct', $this->parseValue($value)); - } - - $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) { - return $value; - } - - if (is_array($value)) { - return $this->parseSubValues($value); - } - - if (empty($value)) { - return null; - } - - if (!is_string($value)) { - return $value; - } - - return $this->parseSingleValue($value); - } - - protected function hasCompleteBrackets(string $value): bool - { - return str_contains($value, '[') && str_contains($value, ']'); - } - - protected function parseSingleValue(string $value): mixed - { - $value = trim($value); - if ($this->hasCompleteBrackets($value)) { - return $this->parseBrackets($value); - } - - return $this->parseConstants($value); - } - - protected function parseConstants(string $value): mixed - { - if (preg_match('/^[\\\\a-zA-Z_]+([:]{2}[A-Z_]+)?$/', $value) && defined($value)) { - return constant($value); - } - - return $value; - } - - protected function matchSequence(string &$value): bool - { - if (preg_match('/^\[(.*?,.*?)\]$/', $value, $match)) { - $value = $match[1]; - - return true; - } - - return false; - } - - protected function matchReference(string &$value): bool - { - if (preg_match('/^\[([[:alnum:]_\\\\]+)\]$/', $value, $match)) { - $value = $match[1]; - - return true; - } - - return false; - } - - protected function parseBrackets(string $value): mixed - { - if ($this->matchSequence($value)) { - return $this->parseArgumentList($value); - } - - if ($this->matchReference($value)) { - return $this->getItem($value, true); - } - - return $this->parseVariables($value); - } - - protected function parseVariables(string $value): string - { - return (string) preg_replace_callback( - '/\[(\w+)\]/', - fn(array $match): string => $this[$match[1]] ?: '', - $value, - ); - } - - /** @return array */ - protected function parseArgumentList(string $value): array - { - $subValues = explode(',', $value); - - return $this->parseSubValues($subValues); - } - protected function lazyLoad(string $name): mixed { $callback = $this[$name]; @@ -431,10 +145,6 @@ static function ($param) use ($container) { parent::offsetSet($name, $item); } - if ($this->configurator) { - $this->configure(); - } - return $this; } diff --git a/src/IniLoader.php b/src/IniLoader.php new file mode 100644 index 0000000..facd7fe --- /dev/null +++ b/src/IniLoader.php @@ -0,0 +1,300 @@ +interpret($input); + } + + public function interpret(mixed $input): Container + { + if ($input === null) { + return $this->container; + } + + if (is_array($input)) { + return $this->fromArray($input); + } + + if (is_string($input) && file_exists($input)) { + return $this->fromFile($input); + } + + if (is_string($input)) { + return $this->fromString($input); + } + + throw new InvalidArgumentException('Invalid input. Must be a valid file or array'); + } + + public function fromString(string $configurator): Container + { + $iniData = parse_ini_string($configurator, true); + if ($iniData === false || count($iniData) === 0) { + throw new InvalidArgumentException('Invalid configuration string'); + } + + return $this->fromArray($iniData); + } + + public function fromFile(string $configurator): Container + { + $iniData = parse_ini_file($configurator, true); + if ($iniData === false) { + throw new InvalidArgumentException('Invalid configuration INI file'); + } + + return $this->fromArray($iniData); + } + + /** @param array $configurator */ + public function fromArray(array $configurator): Container + { + foreach ($this->state() + $configurator as $key => $value) { + if ($value instanceof Closure) { + $this->container->offsetSet((string) $key, $value); + continue; + } + + if ($value instanceof Instantiator) { + $this->container->offsetSet((string) $key, $value); + continue; + } + + $this->parseItem($key, $value); + } + + return $this->container; + } + + /** @return array */ + protected function state(): array + { + return array_filter( + $this->container->getArrayCopy(), + static fn($v): bool => !is_object($v) || !$v instanceof Instantiator, + ); + } + + protected function keyHasStateInstance(string $key, mixed &$k): bool + { + return $this->container->offsetExists($k = current(explode(' ', $key))); + } + + protected function keyHasInstantiator(string $key): bool + { + return str_contains($key, ' '); + } + + protected function parseItem(string|int $key, mixed $value): void + { + $key = trim((string) $key); + if ($this->keyHasInstantiator($key)) { + if ($this->keyHasStateInstance($key, $k)) { + $this->container->offsetSet($key, $this->container[$k]); + } else { + $this->parseInstantiator($key, $value); + } + } else { + $this->parseStandardItem($key, $value); + } + } + + /** + * @param array $value + * + * @return array + */ + protected function parseSubValues(array &$value): array + { + foreach ($value as &$subValue) { + $subValue = $this->parseValue($subValue); + } + + return $value; + } + + protected function parseStandardItem(string $key, mixed &$value): void + { + if (is_array($value)) { + $this->parseSubValues($value); + } else { + $value = $this->parseValue($value); + } + + $this->container->offsetSet($key, $value); + } + + protected function removeDuplicatedSpaces(string $string): string + { + return (string) preg_replace('/\s+/', ' ', $string); + } + + protected function parseInstantiator(string $key, mixed $value): void + { + $key = $this->removeDuplicatedSpaces($key); + [$keyName, $keyClass] = explode(' ', $key, 2); + if ($keyName === 'instanceof') { + $keyName = $keyClass; + } + + /** @var class-string $keyClass */ + $instantiator = $this->createInstantiator($keyClass); + + if (is_array($value)) { + foreach ($value as $property => $pValue) { + $instantiator->setParam($property, $this->parseValue($pValue)); + } + } else { + $instantiator->setParam('__construct', $this->parseValue($value)); + } + + $this->container->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) { + return $value; + } + + if (is_array($value)) { + return $this->parseSubValues($value); + } + + if (empty($value)) { + return null; + } + + if (!is_string($value)) { + return $value; + } + + return $this->parseSingleValue($value); + } + + protected function hasCompleteBrackets(string $value): bool + { + return str_contains($value, '[') && str_contains($value, ']'); + } + + protected function parseSingleValue(string $value): mixed + { + $value = trim($value); + if ($this->hasCompleteBrackets($value)) { + return $this->parseBrackets($value); + } + + return $this->parseConstants($value); + } + + protected function parseConstants(string $value): mixed + { + if (preg_match('/^[\\\\a-zA-Z_]+([:]{2}[A-Z_]+)?$/', $value) && defined($value)) { + return constant($value); + } + + return $value; + } + + protected function matchSequence(string &$value): bool + { + if (preg_match('/^\[(.*?,.*?)\]$/', $value, $match)) { + $value = $match[1]; + + return true; + } + + return false; + } + + protected function matchReference(string &$value): bool + { + if (preg_match('/^\[([[:alnum:]_\\\\]+)\]$/', $value, $match)) { + $value = $match[1]; + + return true; + } + + return false; + } + + protected function parseBrackets(string $value): mixed + { + if ($this->matchSequence($value)) { + return $this->parseArgumentList($value); + } + + if ($this->matchReference($value)) { + return $this->container->getItem($value, true); + } + + return $this->parseVariables($value); + } + + protected function parseVariables(string $value): string + { + return (string) preg_replace_callback( + '/\[(\w+)\]/', + fn(array $match): string => (string) ($this->container[$match[1]] ?? ''), + $value, + ); + } + + /** @return array */ + protected function parseArgumentList(string $value): array + { + $subValues = explode(',', $value); + + return $this->parseSubValues($subValues); + } +} diff --git a/tests/AutowireTest.php b/tests/AutowireTest.php index 6c11954..ebae6ff 100644 --- a/tests/AutowireTest.php +++ b/tests/AutowireTest.php @@ -120,7 +120,7 @@ public function testAutowireViaContainerIniSyntax(): void [consumer autowire Respect\Config\AutowireTypedConsumer] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $consumer = $c->getItem('consumer'); $this->assertInstanceOf(AutowireTypedConsumer::class, $consumer); @@ -141,7 +141,7 @@ public function testAutowireViaContainerWithExplicitOverride(): void dep = [dep2] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $consumer = $c->getItem('consumer'); $this->assertEquals('explicit', $consumer->dep->value); diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index bd127d2..ef875a8 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -16,7 +16,6 @@ use function chdir; use function class_alias; use function extension_loaded; -use function file_get_contents; use function get_class; use function getcwd; use function in_array; @@ -61,27 +60,35 @@ protected function setUp(): void $this->vfsRoot = vfsStream::url('root'); } - public function testLoadArray(): void + public function testConstructorWithArray(): void { - $ini = <<<'INI' -foo = bar -baz = bat -INI; - $c = new Container(self::parseIni($ini)); + $c = new Container(['foo' => 'bar', 'baz' => 'bat']); $this->assertTrue($c->has('foo')); $this->assertEquals('bar', $c->getItem('foo')); $this->assertEquals('bat', $c->getItem('baz')); } - public function testLoadFile(): void + public function testLoadViaIniLoader(): void { - $contents = file_get_contents($this->vfsRoot . '/exists.ini'); - $c = new Container($contents); + $c = IniLoader::load(self::parseIni("foo = bar\nbaz = bat")); $this->assertTrue($c->has('foo')); $this->assertEquals('bar', $c->getItem('foo')); $this->assertEquals('bat', $c->getItem('baz')); } + public function testLoadViaIniLoaderString(): void + { + $c = IniLoader::load("foo = bar\nbaz = bat"); + $this->assertEquals('bar', $c->getItem('foo')); + $this->assertEquals('bat', $c->getItem('baz')); + } + + public function testLoadViaIniLoaderFile(): void + { + $c = IniLoader::load($this->vfsRoot . '/exists.ini'); + $this->assertEquals('bar', $c->getItem('foo')); + } + public function testContainerInterop(): void { $ini = <<<'INI' @@ -89,7 +96,7 @@ public function testContainerInterop(): void baz = bat INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertTrue($c->has('foo')); $this->assertEquals('bar', $c->get('foo')); $this->assertEquals('bat', $c->get('baz')); @@ -103,23 +110,21 @@ public function testLoadInvalidName(): void foo = bar INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $c->get('baz'); } - public function testConfigure(): void + public function testLoadInvalidInput(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid input. Must be a valid file or array'); - $c = new Container(1); - $c->get('a'); + IniLoader::load(1); } - public function testLoadInvalid(): void + public function testLoadInvalidIniString(): void { $this->expectException(InvalidArgumentException::class); - $c = new Container('inexistent.ini'); - $c->get('foo'); + IniLoader::load('inexistent.ini'); } public function testLoadArraySections(): void @@ -130,7 +135,7 @@ public function testLoadArraySections(): void baz = bat INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $d = $c->getItem('sec'); $this->assertEquals('bar', $d['foo']); $this->assertEquals('bat', $d['baz']); @@ -147,7 +152,7 @@ public function testExpandVars(): void db_dsn = "[db_driver]:host=[db_host];dbname=[db_name]" INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertEquals( 'mysql:host=localhost;dbname=my_database', $c->getItem('db_dsn'), @@ -160,7 +165,7 @@ public function testInstantiator(): void [foo \stdClass] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo', true); $this->assertEquals('\stdClass', $instantiator->getClassName()); } @@ -171,7 +176,7 @@ public function testInstantiator2(): void foo \stdClass = INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo', true); $this->assertEquals('\stdClass', $instantiator->getClassName()); } @@ -187,7 +192,7 @@ public function testConstants(): void ipsum = [PATH_SEPARATOR, "foo"DIRECTORY_SEPARATOR"bar"] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertEquals(E_USER_ERROR, $c->getItem('foo')); $this->assertEquals(PDO::ATTR_ERRMODE, $c->getItem('bar')); $this->assertEquals([E_USER_ERROR, E_USER_WARNING], $c->getItem('faa')); @@ -204,7 +209,7 @@ public function testInstantiatorParams(): void baz = bat INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo', true); $this->assertEquals('bar', $instantiator->getParam('foo')); $this->assertEquals('bat', $instantiator->getParam('baz')); @@ -217,7 +222,7 @@ public function testInstantiatorMethodCalls(): void setTimestamp[] = 123 INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $dateTime = $c->getItem('date'); $this->assertEquals(123, $dateTime->getTimestamp()); } @@ -236,7 +241,7 @@ public function testInstantiatorNullMethodCalls(): void commit[] = INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $conn = $c->getItem('conn'); $this->assertNotEmpty($conn->query('SELECT * FROM sqlite_master')->fetch()); } @@ -249,7 +254,7 @@ public function testInstantiatorParamsArray(): void foo[def] = bat INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo', true); $expected = [ 'abc' => 'bar', @@ -267,7 +272,7 @@ public function testInstantiatorParamsBrackets(): void baz = [bat, blz] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo', true); $expectedFoo = [ 'abc' => ['bat', 'blz'], @@ -289,7 +294,7 @@ public function testInstantiatorParamsBracketsReferences(): void barr = [bat, [hi]] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo', true); $expectedFoo = [ 'abc' => ['bat', 'blz'], @@ -309,12 +314,12 @@ public function testGetItemLazyLoad(): void $this->assertEquals('ok', $c->getItem('foo', false)); } - public function testClosureWithLoadedFile(): void + public function testClosureWithIniLoad(): void { $ini = <<<'INI' respect_blah = "" INI; - $c = new Container($ini); + $c = IniLoader::load($ini); $c['panda'] = static function () { return 'ok'; }; @@ -333,7 +338,7 @@ public function testLazyLoadinessOnMultipleConfigLevels(): void child = [bar] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertFalse($GLOBALS['_SHIT_']); $GLOBALS['_SHIT_'] = false; } @@ -346,7 +351,7 @@ public function testSequencesConstructingLazy(): void hello[] = ["opa", [bar]] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $foo = $c->getItem('foo'); $this->assertInstanceOf(Bar::class, $foo->bar); } @@ -365,7 +370,7 @@ public function testPascutti(): void con = [pdo]; INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); // __set replaces the Instantiator's pending instance $c->pdo = new PDO('sqlite::memory:'); $this->assertSame($c->getItem('pdo'), $c->getItem('db')->c); @@ -381,32 +386,32 @@ public function testPascuttiTypeHintIssue40(): void date = [now]; INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertInstanceOf( TypeHintWowMuchType::class, $c->getItem('typed'), ); } - public function testLockedContainer(): void + public function testPrePopulatedContainer(): void { $ini = <<<'INI' foo = [undef] bar = [foo] INI; - $c = new Container(self::parseIni($ini)); - $result = $c(['undef' => 'Hello']); - $this->assertEquals('Hello', $result->getItem('bar')); + $c = new Container(['undef' => 'Hello']); + (new IniLoader($c))->fromArray(self::parseIni($ini)); + $this->assertEquals('Hello', $c->getItem('bar')); } - public function testLockedContainer2(): void + public function testPrePopulatedContainer2(): void { $ini = <<<'INI' foo = [undef] bar = [foo] INI; - $c = new Container(self::parseIni($ini)); - $c(['undef' => 'Hello']); + $c = new Container(['undef' => 'Hello']); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $result = $c->getItem('bar'); $this->assertEquals('Hello', $result); } @@ -417,7 +422,7 @@ public function testFactory(): void [now new DateTime] datetime = now INI; - $c = new Container(self::parseIni($ini)); + $c = IniLoader::load(self::parseIni($ini)); $result = $c->getItem('now'); $result2 = $c->getItem('now'); $this->assertNotSame($result, $result2); @@ -429,7 +434,7 @@ public function testDependenciesDoesNotAffectFactories(): void [now DateTime] datetime = now INI; - $c = new Container(self::parseIni($ini)); + $c = IniLoader::load(self::parseIni($ini)); $result = $c->getItem('now'); $result2 = $c->getItem('now'); $this->assertSame($result, $result2); @@ -441,7 +446,7 @@ public function testByInstanceCallback(): void [instanceof DateTime] datetime = now INI; - $c = new Container(self::parseIni($ini)); + $c = IniLoader::load(self::parseIni($ini)); $called = false; $result = $c(static function (DateTime $date) use (&$called) { $called = true; @@ -484,7 +489,7 @@ public function testClassConstants(): void foo = \Respect\Config\TestConstant::CONS_TEST INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertEquals(TestConstant::CONS_TEST, $c->getItem('foo')); } @@ -495,7 +500,7 @@ class_alias(TestConstant::class, 'Respect\Test\Another\Cons'); foo = \Respect\Test\Another\Cons::CONS_TEST INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); // The container resolves the aliased constant at runtime $this->assertEquals(TestConstant::CONS_TEST, $c->getItem('foo')); } @@ -506,7 +511,7 @@ public function testInstantiatorWithUnderline(): void [foo_bar \stdClass] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $instantiator = $c->getItem('foo_bar', true); $this->assertEquals('\stdClass', $instantiator->getClassName()); } @@ -520,16 +525,13 @@ public function testClassWithAnotherAndUnderline(): void test = [foo_bar] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $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)); + $c = new Container(['foo' => 'bar']); $this->assertTrue(isset($c->foo)); $this->assertFalse(isset($c->nonexistent)); } @@ -543,21 +545,14 @@ public function testSetMethod(): void public function testMagicGet(): void { - $ini = <<<'INI' -foo = bar -INI; - $c = new Container(self::parseIni($ini)); + $c = new Container(['foo' => 'bar']); $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']]); + $c = new Container(['bar' => 'Hello']); + $result = $c->__call('bar', [['extra' => 'val']]); $this->assertEquals('Hello', $result); } @@ -568,7 +563,7 @@ public function testLoadString(): void baz = bat INI; $c = new Container(); - $c->loadString($ini); + (new IniLoader($c))->fromString($ini); $this->assertEquals('bar', $c->getItem('foo')); $this->assertEquals('bat', $c->getItem('baz')); } @@ -578,26 +573,7 @@ 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')); + (new IniLoader($c))->fromString(''); } public function testHasReturnsFalseForNonExistentClass(): void @@ -606,7 +582,7 @@ public function testHasReturnsFalseForNonExistentClass(): void [foo Respect\Config\NonExistentClass12345] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertFalse($c->has('foo')); } @@ -616,7 +592,7 @@ public function testHasReturnsTrueForValidInstantiator(): void [foo DateTime] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $this->assertTrue($c->has('foo')); } @@ -626,7 +602,7 @@ public function testGetItemRawReturnsInstantiator(): void [foo DateTime] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $raw = $c->getItem('foo', true); $this->assertInstanceOf(Instantiator::class, $raw); } @@ -647,15 +623,16 @@ public function testInstanceofSyntax(): void [instanceof DateTime] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(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')); + $loader = new IniLoader($c); + $loader->fromArray(self::parseIni('foo = bar')); + $loader->fromArray(self::parseIni('baz = bat')); $this->assertEquals('bar', $c->getItem('foo')); $this->assertEquals('bat', $c->getItem('baz')); } @@ -667,7 +644,7 @@ public function testVariableExpansionInSequence(): void greetings = [hello, [name]] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $result = $c->getItem('greetings'); $this->assertEquals(['hello', 'world'], $result); } @@ -679,7 +656,7 @@ public function testLoadFileInvalidIni(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid configuration INI file'); $c = new Container(); - @$c->loadFile(vfsStream::url('bad') . '/unreadable.ini'); + @(new IniLoader($c))->fromFile(vfsStream::url('bad') . '/unreadable.ini'); } public function testLoadArrayWithInstantiatorValue(): void @@ -687,7 +664,7 @@ public function testLoadArrayWithInstantiatorValue(): void $i = new Instantiator('stdClass'); $i->setParam('foo', 'bar'); $c = new Container(); - $c->loadArray(['myobj' => $i]); + (new IniLoader($c))->fromArray(['myobj' => $i]); $result = $c->getItem('myobj'); $this->assertInstanceOf(stdClass::class, $result); $this->assertEquals('bar', $result->foo); @@ -696,7 +673,7 @@ public function testLoadArrayWithInstantiatorValue(): void public function testLoadArrayWithClosureValue(): void { $c = new Container(); - $c->loadArray(['fn' => static fn() => 'result']); + (new IniLoader($c))->fromArray(['fn' => static fn() => 'result']); $this->assertEquals('result', $c->getItem('fn')); } @@ -717,7 +694,7 @@ public function testUnknownInstantiatorModifier(): void [foo unknown stdClass] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $raw = $c->getItem('foo', true); $this->assertInstanceOf(Instantiator::class, $raw); $this->assertEquals('unknown stdClass', $raw->getClassName()); @@ -748,7 +725,7 @@ public function testParseValuePassesInstantiatorThrough(): void $inner = new Instantiator('stdClass'); $inner->setParam('x', 'y'); $c = new Container(); - $c->loadArray(['outer stdClass' => ['child' => $inner]]); + (new IniLoader($c))->fromArray(['outer stdClass' => ['child' => $inner]]); $result = $c->getItem('outer'); $this->assertInstanceOf(stdClass::class, $result); $this->assertInstanceOf(stdClass::class, $result->child); @@ -764,11 +741,19 @@ public function testAutowireModifierInContainerCreatesAutowire(): void [consumer autowire Respect\Config\WheneverWithAProperty] INI; $c = new Container(); - $c->loadArray(self::parseIni($ini)); + (new IniLoader($c))->fromArray(self::parseIni($ini)); $raw = $c->getItem('consumer', true); $this->assertInstanceOf(Autowire::class, $raw); } + public function testInvokeWithArrayInjectsValues(): void + { + $c = new Container(); + $result = $c(['foo' => 'bar']); + $this->assertSame($c, $result); + $this->assertEquals('bar', $c->getItem('foo')); + } + protected function tearDown(): void { if (!is_dir($this->originalCwd)) { diff --git a/tests/EnviromentConfigurationTest.php b/tests/EnviromentConfigurationTest.php index d4dde8a..06183bf 100644 --- a/tests/EnviromentConfigurationTest.php +++ b/tests/EnviromentConfigurationTest.php @@ -30,7 +30,7 @@ public function testEnviromentConfiguration30(): void $parsed = parse_ini_string($config, true); $this->assertIsArray($parsed); $config = array_merge($parsed[$environment], $parsed); - $container = new Container($config); + $container = IniLoader::load($config); $this->assertEquals($expected, $container->getItem('account')); } } diff --git a/tests/IniLoaderTest.php b/tests/IniLoaderTest.php new file mode 100644 index 0000000..65734b6 --- /dev/null +++ b/tests/IniLoaderTest.php @@ -0,0 +1,385 @@ +fromArray(self::parseIni('foo = bar')); + $this->assertEquals('bar', $container->getItem('foo')); + } + + public function testFromString(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromString("foo = bar\nbaz = bat"); + $this->assertEquals('bar', $container->getItem('foo')); + $this->assertEquals('bat', $container->getItem('baz')); + } + + public function testFromStringInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid configuration string'); + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromString(''); + } + + public function testFromFile(): void + { + $structure = ['test.ini' => "foo = bar\nbaz = bat"]; + vfsStream::setup('root', null, $structure); + + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromFile(vfsStream::url('root') . '/test.ini'); + $this->assertEquals('bar', $container->getItem('foo')); + $this->assertEquals('bat', $container->getItem('baz')); + } + + public function testFromFileInvalid(): 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'); + $container = new Container(); + $loader = new IniLoader($container); + @$loader->fromFile(vfsStream::url('bad') . '/unreadable.ini'); + } + + public function testInterpretWithArray(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->interpret(['foo' => 'bar']); + $this->assertEquals('bar', $container->getItem('foo')); + } + + public function testInterpretWithString(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->interpret('foo = bar' . "\n" . 'baz = bat'); + $this->assertEquals('bar', $container->getItem('foo')); + } + + public function testInterpretWithFile(): void + { + $structure = ['test.ini' => 'foo = bar']; + vfsStream::setup('root', null, $structure); + + $container = new Container(); + $loader = new IniLoader($container); + $loader->interpret(vfsStream::url('root') . '/test.ini'); + $this->assertEquals('bar', $container->getItem('foo')); + } + + public function testInterpretWithNull(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->interpret(null); + $this->assertFalse($container->has('anything')); + } + + public function testInterpretWithInvalidInput(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid input. Must be a valid file or array'); + $container = new Container(); + $loader = new IniLoader($container); + $loader->interpret(1); + } + + public function testExpandVars(): void + { + $ini = <<<'INI' +db_driver = "mysql" +db_host = "localhost" +db_name = "my_database" +db_dsn = "[db_driver]:host=[db_host];dbname=[db_name]" +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $this->assertEquals( + 'mysql:host=localhost;dbname=my_database', + $container->getItem('db_dsn'), + ); + } + + public function testInstantiator(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni('[foo \stdClass]')); + $instantiator = $container->getItem('foo', true); + $this->assertEquals('\stdClass', $instantiator->getClassName()); + } + + public function testConstants(): void + { + $ini = <<<'INI' +foo = E_USER_ERROR +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $this->assertEquals(E_USER_ERROR, $container->getItem('foo')); + } + + public function testFactoryModifier(): void + { + $ini = <<<'INI' +[now new DateTime] +datetime = now +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $raw = $container->getItem('now', true); + $this->assertInstanceOf(Factory::class, $raw); + } + + public function testAutowireModifier(): void + { + $ini = <<<'INI' +[consumer autowire \stdClass] +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $raw = $container->getItem('consumer', true); + $this->assertInstanceOf(Autowire::class, $raw); + } + + public function testInstanceofSyntax(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni('[instanceof DateTime]')); + $this->assertInstanceOf(DateTime::class, $container->getItem('DateTime')); + } + + public function testSequences(): void + { + $ini = <<<'INI' +greetings = [hello, world] +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $this->assertEquals(['hello', 'world'], $container->getItem('greetings')); + } + + public function testMultipleLoadsMergeState(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni('foo = bar')); + $loader->fromArray(self::parseIni('baz = bat')); + $this->assertEquals('bar', $container->getItem('foo')); + $this->assertEquals('bat', $container->getItem('baz')); + } + + public function testFromArrayPassesClosureThrough(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(['fn' => static fn() => 'result']); + $this->assertEquals('result', $container->getItem('fn')); + } + + public function testFromArrayPassesInstantiatorThrough(): void + { + $instantiator = new Instantiator('stdClass'); + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(['obj' => $instantiator]); + $raw = $container->getItem('obj', true); + $this->assertSame($instantiator, $raw); + } + + public function testSections(): void + { + $ini = <<<'INI' +[sec] +foo = bar +baz = bat +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $section = $container->getItem('sec'); + $this->assertEquals('bar', $section['foo']); + $this->assertEquals('bat', $section['baz']); + } + + public function testInstantiatorWithSingleConstructorParam(): void + { + $ini = <<<'INI' +foo \stdClass = bar +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $raw = $container->getItem('foo', true); + $this->assertInstanceOf(Instantiator::class, $raw); + $this->assertEquals('bar', $raw->getParam('__construct')); + } + + public function testKeyHasStateInstanceReusesExistingEntry(): void + { + $container = new Container(); + $container['foo'] = 'existing'; + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni('[foo \stdClass]')); + $this->assertEquals('existing', $container->getItem('foo \stdClass')); + } + + public function testParseValuePassesInstantiatorThrough(): void + { + $inner = new Instantiator('stdClass'); + $inner->setParam('x', 'y'); + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(['outer stdClass' => ['child' => $inner]]); + $result = $container->getItem('outer'); + $this->assertInstanceOf(stdClass::class, $result); + $this->assertInstanceOf(stdClass::class, $result->child); + } + + public function testEmptyValueBecomesNull(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(['key' => '']); + $this->assertNull($container['key']); + } + + public function testNonStringValuePassedThrough(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(['num' => 42]); + $this->assertSame(42, $container->getItem('num')); + } + + public function testReferenceResolution(): void + { + $ini = <<<'INI' +greeting = hello +ref = [greeting] +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $this->assertEquals('hello', $container->getItem('ref')); + } + + public function testParseValueHandlesNestedArray(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(['outer stdClass' => ['items' => ['a', 'b']]]); + $raw = $container->getItem('outer', true); + $this->assertEquals(['a', 'b'], $raw->getParam('items')); + } + + public function testLoadStaticFactory(): void + { + $container = IniLoader::load(self::parseIni('foo = bar')); + $this->assertInstanceOf(Container::class, $container); + $this->assertEquals('bar', $container->getItem('foo')); + } + + public function testLoadStaticFactoryWithExistingContainer(): void + { + $container = new Container(['existing' => 'value']); + $result = IniLoader::load(self::parseIni('foo = bar'), $container); + $this->assertSame($container, $result); + $this->assertEquals('value', $result->getItem('existing')); + $this->assertEquals('bar', $result->getItem('foo')); + } + + public function testFromArrayReturnsContainer(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $result = $loader->fromArray(self::parseIni('foo = bar')); + $this->assertSame($container, $result); + } + + public function testFromStringReturnsContainer(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $result = $loader->fromString("foo = bar\nbaz = bat"); + $this->assertSame($container, $result); + } + + public function testFromFileReturnsContainer(): void + { + $structure = ['test.ini' => 'foo = bar']; + vfsStream::setup('fluent', null, $structure); + + $container = new Container(); + $loader = new IniLoader($container); + $result = $loader->fromFile(vfsStream::url('fluent') . '/test.ini'); + $this->assertSame($container, $result); + } + + public function testInterpretReturnsContainer(): void + { + $container = new Container(); + $loader = new IniLoader($container); + $result = $loader->interpret(['foo' => 'bar']); + $this->assertSame($container, $result); + } + + public function testClassConstantResolution(): void + { + $ini = <<<'INI' +foo = \Respect\Config\IniLoaderTestConstant::VALUE +INI; + $container = new Container(); + $loader = new IniLoader($container); + $loader->fromArray(self::parseIni($ini)); + $this->assertEquals(IniLoaderTestConstant::VALUE, $container->getItem('foo')); + } + + /** @return array */ + private static function parseIni(string $ini): array + { + $result = parse_ini_string($ini, true); + self::assertIsArray($result); + + return $result; + } +} + +class IniLoaderTestConstant +{ + public const string VALUE = 'XPTO'; +} diff --git a/tests/LazyLoadTest.php b/tests/LazyLoadTest.php index 079554e..b2b19cc 100644 --- a/tests/LazyLoadTest.php +++ b/tests/LazyLoadTest.php @@ -21,8 +21,9 @@ public function testLazyLoadedParameters(): void string = [my_string] "; $expected = 'Hello World!'; - $container = new Container($config); + $container = new Container(); $container['my_string'] = $expected; + (new IniLoader($container))->fromString($config); $this->assertEquals($expected, (string) $container->getItem('hello')); } @@ -38,13 +39,16 @@ public function testLazyLoadedInstance(): void hello = [hello] "; $expected = 'Hello World!'; - $container = new Container($config); + $container = new Container(); $container['my_string'] = $expected; + (new IniLoader($container))->fromString($config); $this->assertEquals($expected, (string) $container->getItem('hello')); - $container = new Container($config); - $container->{'hello Respect\\Config\\MyLazyLoadedHelloWorld'} = ['string' => $expected]; + $container = new Container(); + $container['hello Respect\\Config\\MyLazyLoadedHelloWorld'] = ['string' => $expected]; + (new IniLoader($container))->fromString($config); $this->assertEquals($expected, (string) $container->getItem('hello')); - $container = new Container($config); + $container = new Container(); + (new IniLoader($container))->fromString($config); // __set detects existing Instantiator at 'hello' and calls setInstance() $container->hello = new MyLazyLoadedHelloWorld($expected); $this->assertEquals($expected, (string) $container->getItem('hello'));