diff --git a/CHANGELOG.md b/CHANGELOG.md index c11cac6d..be05a545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Yii Dependency Injection Change Log -## 1.2.2 under development +## 2.0.0 under development -- no changes in this release. +- Chg #299: Improve types to use iterable objects (@xepozz) ## 1.2.1 December 23, 2022 diff --git a/src/Container.php b/src/Container.php index 43540927..367139a1 100644 --- a/src/Container.php +++ b/src/Container.php @@ -9,6 +9,7 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Throwable; +use Traversable; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\DefinitionStorage; use Yiisoft\Definitions\Exception\CircularReferenceException; @@ -59,7 +60,7 @@ final class Container implements ContainerInterface /** * @var array Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. - * @psalm-var array> + * @psalm-var array> */ private array $tags; @@ -223,13 +224,14 @@ private function addDefinition(string $id, mixed $definition): void /** * Sets multiple definitions at once. * - * @param array $config Definitions indexed by their IDs. + * @param iterable $config Definitions indexed by their IDs. * * @throws InvalidConfigException */ - private function addDefinitions(array $config): void + private function addDefinitions(iterable $config): void { /** @var mixed $definition */ + /** @psalm-suppress MixedAssignment */ foreach ($config as $id => $definition) { if ($this->validate && !is_string($id)) { throw new InvalidConfigException( @@ -239,8 +241,8 @@ private function addDefinitions(array $config): void ) ); } - /** @var string $id */ + $id = (string) $id; $this->addDefinition($id, $definition); } } @@ -251,11 +253,11 @@ private function addDefinitions(array $config): void * Each delegate must is a callable in format "function (ContainerInterface $container): ContainerInterface". * The container instance returned is used in case a service can not be found in primary container. * - * @param array $delegates + * @param iterable $delegates * * @throws InvalidConfigException */ - private function setDelegates(array $delegates): void + private function setDelegates(iterable $delegates): void { $this->delegates = new CompositeContainer(); $container = $this->get(ContainerInterface::class); @@ -323,10 +325,12 @@ private function validateDefinition(mixed $definition, ?string $id = null): void /** * @throws InvalidConfigException */ - private function validateMeta(array $meta): void + private function validateMeta(iterable $meta): void { /** @var mixed $value */ + /** @psalm-suppress MixedAssignment */ foreach ($meta as $key => $value) { + $key = (string)$key; if (!in_array($key, self::ALLOWED_META, true)) { throw new InvalidConfigException( sprintf( @@ -353,10 +357,10 @@ private function validateMeta(array $meta): void */ private function validateDefinitionTags(mixed $tags): void { - if (!is_array($tags)) { + if (!is_iterable($tags)) { throw new InvalidConfigException( sprintf( - 'Invalid definition: tags should be array of strings, %s given.', + 'Invalid definition: tags should be either iterable or array of strings, %s given.', get_debug_type($tags) ) ); @@ -387,7 +391,7 @@ private function validateDefinitionReset(mixed $reset): void /** * @throws InvalidConfigException */ - private function setTags(array $tags): void + private function setTags(iterable $tags): void { if ($this->validate) { foreach ($tags as $tag => $services) { @@ -395,14 +399,14 @@ private function setTags(array $tags): void throw new InvalidConfigException( sprintf( 'Invalid tags configuration: tag should be string, %s given.', - $tag + get_debug_type($services) ) ); } - if (!is_array($services)) { + if (!is_iterable($services)) { throw new InvalidConfigException( sprintf( - 'Invalid tags configuration: tag should contain array of service IDs, %s given.', + 'Invalid tags configuration: tag should be either iterable or array of service IDs, %s given.', get_debug_type($services) ) ); @@ -420,18 +424,26 @@ private function setTags(array $tags): void } } } - /** @psalm-var array> $tags */ + /** @psalm-var iterable> $tags */ - $this->tags = $tags; + $this->tags = $tags instanceof Traversable ? iterator_to_array($tags, true) : $tags ; } /** * @psalm-param string[] $tags */ - private function setDefinitionTags(string $id, array $tags): void + private function setDefinitionTags(string $id, iterable $tags): void { foreach ($tags as $tag) { - if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) { + if (!isset($this->tags[$tag])) { + $this->tags[$tag] = [$id]; + continue; + } + + $tags = $this->tags[$tag]; + $tags = $tags instanceof Traversable ? iterator_to_array($tags, true) : $tags; + if (!in_array($id, $tags, true)) { + /** @psalm-suppress PossiblyInvalidArrayAssignment */ $this->tags[$tag][] = $id; } } @@ -537,7 +549,7 @@ private function buildInternal(string $id) * @throws CircularReferenceException * @throws InvalidConfigException */ - private function addProviders(array $providers): void + private function addProviders(iterable $providers): void { $extensions = []; /** @var mixed $provider */ diff --git a/src/ContainerConfig.php b/src/ContainerConfig.php index 182ed2b3..2f8313f0 100644 --- a/src/ContainerConfig.php +++ b/src/ContainerConfig.php @@ -9,11 +9,11 @@ */ final class ContainerConfig implements ContainerConfigInterface { - private array $definitions = []; - private array $providers = []; - private array $tags = []; + private iterable $definitions = []; + private iterable $providers = []; + private iterable $tags = []; private bool $validate = true; - private array $delegates = []; + private iterable $delegates = []; private bool $useStrictMode = false; private function __construct() @@ -26,46 +26,46 @@ public static function create(): self } /** - * @param array $definitions Definitions to put into container. + * @param iterable $definitions Definitions to put into container. */ - public function withDefinitions(array $definitions): self + public function withDefinitions(iterable $definitions): self { $new = clone $this; $new->definitions = $definitions; return $new; } - public function getDefinitions(): array + public function getDefinitions(): iterable { return $this->definitions; } /** - * @param array $providers Service providers to get definitions from. + * @param iterable $providers Service providers to get definitions from. */ - public function withProviders(array $providers): self + public function withProviders(iterable $providers): self { $new = clone $this; $new->providers = $providers; return $new; } - public function getProviders(): array + public function getProviders(): iterable { return $this->providers; } /** - * @param array $tags Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. + * @param iterable $tags Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. */ - public function withTags(array $tags): self + public function withTags(iterable $tags): self { $new = clone $this; $new->tags = $tags; return $new; } - public function getTags(): array + public function getTags(): iterable { return $this->tags; } @@ -86,18 +86,18 @@ public function shouldValidate(): bool } /** - * @param array $delegates Container delegates. Each delegate is a callable in format + * @param iterable $delegates Container delegates. Each delegate is a callable in format * `function (ContainerInterface $container): ContainerInterface`. The container instance returned is used * in case a service can not be found in primary container. */ - public function withDelegates(array $delegates): self + public function withDelegates(iterable $delegates): self { $new = clone $this; $new->delegates = $delegates; return $new; } - public function getDelegates(): array + public function getDelegates(): iterable { return $this->delegates; } diff --git a/src/ContainerConfigInterface.php b/src/ContainerConfigInterface.php index 9e535e5b..8e3f0396 100644 --- a/src/ContainerConfigInterface.php +++ b/src/ContainerConfigInterface.php @@ -10,19 +10,19 @@ interface ContainerConfigInterface { /** - * @return array Definitions to put into container. + * @return iterable Definitions to put into container. */ - public function getDefinitions(): array; + public function getDefinitions(): iterable; /** - * @return array Service providers to get definitions from. + * @return iterable Service providers to get definitions from. */ - public function getProviders(): array; + public function getProviders(): iterable; /** - * @return array Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. + * @return iterable Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. */ - public function getTags(): array; + public function getTags(): iterable; /** * @return bool Whether definitions should be validated immediately. @@ -30,11 +30,11 @@ public function getTags(): array; public function shouldValidate(): bool; /** - * @return array Container delegates. Each delegate is a callable in format + * @return iterable Container delegates. Each delegate is a callable in format * `function (ContainerInterface $container): ContainerInterface`. The container instance returned is used * in case a service can not be found in primary container. */ - public function getDelegates(): array; + public function getDelegates(): iterable; /** * @return bool If the automatic addition of definition when class exists and can be resolved is disabled. diff --git a/src/ServiceProviderInterface.php b/src/ServiceProviderInterface.php index 275b7b0d..56dd295a 100644 --- a/src/ServiceProviderInterface.php +++ b/src/ServiceProviderInterface.php @@ -42,7 +42,7 @@ interface ServiceProviderInterface * @return array Definitions for the container. Each array key is the name of the service (usually it is * an interface name), and a corresponding value is a service definition. */ - public function getDefinitions(): array; + public function getDefinitions(): iterable; /** * Returns an array of service extensions. @@ -58,5 +58,5 @@ public function getDefinitions(): array; * @return array Extensions for the container services. Each array key is the name of the service to be modified * and a corresponding value is callable doing the job. */ - public function getExtensions(): array; + public function getExtensions(): iterable; } diff --git a/tests/Support/CarExtensionProvider.php b/tests/Support/CarExtensionProvider.php index 1a5f1d48..382b851c 100644 --- a/tests/Support/CarExtensionProvider.php +++ b/tests/Support/CarExtensionProvider.php @@ -9,12 +9,12 @@ final class CarExtensionProvider implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return []; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ Car::class => static function (ContainerInterface $container, Car $car) { diff --git a/tests/Support/CarProvider.php b/tests/Support/CarProvider.php index 6b4e7018..58efa270 100644 --- a/tests/Support/CarProvider.php +++ b/tests/Support/CarProvider.php @@ -9,7 +9,7 @@ final class CarProvider implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return [ 'car' => Car::class, @@ -17,7 +17,7 @@ public function getDefinitions(): array ]; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ Car::class => static function (ContainerInterface $container, Car $car) { diff --git a/tests/Support/ContainerInterfaceExtensionProvider.php b/tests/Support/ContainerInterfaceExtensionProvider.php index ab3ebe07..516c2c17 100644 --- a/tests/Support/ContainerInterfaceExtensionProvider.php +++ b/tests/Support/ContainerInterfaceExtensionProvider.php @@ -9,12 +9,12 @@ final class ContainerInterfaceExtensionProvider implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return []; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ ContainerInterface::class => static fn (ContainerInterface $container, ContainerInterface $extended) => $container, diff --git a/tests/Support/NullCarExtensionProvider.php b/tests/Support/NullCarExtensionProvider.php index 6080429d..53a45f0e 100644 --- a/tests/Support/NullCarExtensionProvider.php +++ b/tests/Support/NullCarExtensionProvider.php @@ -9,13 +9,13 @@ final class NullCarExtensionProvider implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return [ ]; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ Car::class => static fn (ContainerInterface $container, Car $car) => null, diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 7637fbfb..1722c302 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -10,14 +10,18 @@ use Psr\Container\NotFoundExceptionInterface; use RuntimeException; use stdClass; +use Yiisoft\Definitions\DynamicReference; +use Yiisoft\Definitions\Exception\CircularReferenceException; +use Yiisoft\Definitions\Exception\InvalidConfigException; +use Yiisoft\Definitions\Reference; use Yiisoft\Di\BuildingException; use Yiisoft\Di\CompositeContainer; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; use Yiisoft\Di\ExtensibleService; use Yiisoft\Di\NotFoundException; -use Yiisoft\Di\StateResetter; use Yiisoft\Di\ServiceProviderInterface; +use Yiisoft\Di\StateResetter; use Yiisoft\Di\Tests\Support\A; use Yiisoft\Di\Tests\Support\B; use Yiisoft\Di\Tests\Support\Car; @@ -41,15 +45,11 @@ use Yiisoft\Di\Tests\Support\PropertyTestClass; use Yiisoft\Di\Tests\Support\SportCar; use Yiisoft\Di\Tests\Support\TreeItem; -use Yiisoft\Di\Tests\Support\UnionTypeInConstructorSecondTypeInParamResolvable; -use Yiisoft\Di\Tests\Support\UnionTypeInConstructorSecondParamNotResolvable; -use Yiisoft\Di\Tests\Support\UnionTypeInConstructorParamNotResolvable; use Yiisoft\Di\Tests\Support\UnionTypeInConstructorFirstTypeInParamResolvable; +use Yiisoft\Di\Tests\Support\UnionTypeInConstructorParamNotResolvable; +use Yiisoft\Di\Tests\Support\UnionTypeInConstructorSecondParamNotResolvable; +use Yiisoft\Di\Tests\Support\UnionTypeInConstructorSecondTypeInParamResolvable; use Yiisoft\Di\Tests\Support\VariadicConstructor; -use Yiisoft\Definitions\DynamicReference; -use Yiisoft\Definitions\Exception\CircularReferenceException; -use Yiisoft\Definitions\Exception\InvalidConfigException; -use Yiisoft\Definitions\Reference; use Yiisoft\Injector\Injector; /** @@ -1087,6 +1087,30 @@ public function testTagsWithExternalDefinition(): void $this->assertSame(EngineMarkTwo::class, $engines[0]::class); } + public function testTagsIterable(): void + { + $config = ContainerConfig::create() + ->withDefinitions([ + EngineMarkOne::class => [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + ], + EngineMarkTwo::class => [ + 'class' => EngineMarkTwo::class, + ], + ]) + ->withTags(['engine' => new ArrayIterator([EngineMarkTwo::class])]) + ->withValidate(true); + $container = new Container($config); + + $engines = $container->get('tag@engine'); + + $this->assertIsArray($engines); + $this->assertCount(2, $engines); + $this->assertSame(EngineMarkOne::class, $engines[1]::class); + $this->assertSame(EngineMarkTwo::class, $engines[0]::class); + } + public function testTagsWithExternalDefinitionMerge(): void { $config = ContainerConfig::create() @@ -1373,7 +1397,7 @@ public function testResetterInProviderDefinitions(bool $strictMode): void ]) ->withProviders([ new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return [ StateResetter::class => static function (ContainerInterface $container) { @@ -1388,7 +1412,7 @@ public function getDefinitions(): array ]; } - public function getExtensions(): array + public function getExtensions(): iterable { return []; } @@ -1418,12 +1442,12 @@ public function testResetterInProviderExtensions(): void ]) ->withProviders([ new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return []; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ StateResetter::class => static function ( @@ -1598,7 +1622,7 @@ public function testResetterInCompositeContainer(): void public function testCircularReferenceExceptionWhileResolvingProviders(): void { $provider = new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return [ // E.g. wrapping container with proxy class @@ -1606,7 +1630,7 @@ public function getDefinitions(): array ]; } - public function getExtensions(): array + public function getExtensions(): iterable { return []; } @@ -1631,14 +1655,14 @@ public function getExtensions(): array public function testDifferentContainerWithProviders(): void { $provider = new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return [ ContainerInterface::class => static fn (ContainerInterface $container) => new Container(ContainerConfig::create()), ]; } - public function getExtensions(): array + public function getExtensions(): iterable { return []; } @@ -1846,12 +1870,12 @@ public function testIntegerKeyInExtensions(): void $config = ContainerConfig::create() ->withProviders([ new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return []; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ 23 => static fn (ContainerInterface $container, StateResetter $resetter) => $resetter, @@ -1870,12 +1894,12 @@ public function testNonCallableExtension(): void $config = ContainerConfig::create() ->withProviders([ new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return []; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ ColorPink::class => [], @@ -1920,7 +1944,7 @@ public function testNonArrayTags(): void $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( - 'Invalid definition: tags should be array of strings, string given.' + 'Invalid definition: tags should be either iterable or array of strings, string given.' ); new Container($config); } @@ -1939,22 +1963,22 @@ public function testNonArrayArguments(): void $this->expectExceptionMessage( 'Invalid definition: incorrect method "setNumber()" arguments. Expected array, got "int". Probably you should wrap them into square brackets.', ); - $container = new Container($config); + new Container($config); } public function dataInvalidTags(): array { return [ [ - '/^Invalid tags configuration: tag should be string, 42 given\.$/', + 'Invalid tags configuration: tag should be string, array given.', [42 => [EngineMarkTwo::class]], ], [ - '/^Invalid tags configuration: tag should contain array of service IDs, (integer|int) given\.$/', + 'Invalid tags configuration: tag should be either iterable or array of service IDs, int given.', ['engine' => 42], ], [ - '/^Invalid tags configuration: service should be defined as class string, (integer|int) given\.$/', + 'Invalid tags configuration: service should be defined as class string, int given.', ['engine' => [42]], ], ]; @@ -1969,7 +1993,28 @@ public function testInvalidTags(string $message, array $tags): void ->withTags($tags); $this->expectException(InvalidConfigException::class); - $this->expectExceptionMessageMatches($message); + $this->expectExceptionMessage($message); new Container($config); } + + public function testSupportIterableDefinitions(): void + { + $config = ContainerConfig::create() + ->withDefinitions( + (function () { + yield from [ + EngineMarkOne::class => [ + 'class' => EngineMarkOne::class, + 'setNumber()' => [42], + ], + ]; + })() + ); + + $container = new Container($config); + + $this->assertTrue($container->has(EngineMarkOne::class)); + $engine = $container->get(EngineMarkOne::class); + $this->assertEquals(42, $engine->getNumber()); + } } diff --git a/tests/Unit/ServiceProviderTest.php b/tests/Unit/ServiceProviderTest.php index 73007e5b..b1ced10b 100644 --- a/tests/Unit/ServiceProviderTest.php +++ b/tests/Unit/ServiceProviderTest.php @@ -6,21 +6,21 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; use Yiisoft\Di\ServiceProviderInterface; use Yiisoft\Di\Tests\Support\Car; -use Yiisoft\Di\Tests\Support\CarProvider; use Yiisoft\Di\Tests\Support\CarExtensionProvider; -use Yiisoft\Di\Tests\Support\ContainerInterfaceExtensionProvider; +use Yiisoft\Di\Tests\Support\CarProvider; use Yiisoft\Di\Tests\Support\ColorRed; +use Yiisoft\Di\Tests\Support\ContainerInterfaceExtensionProvider; use Yiisoft\Di\Tests\Support\EngineInterface; use Yiisoft\Di\Tests\Support\EngineMarkOne; use Yiisoft\Di\Tests\Support\EngineMarkTwo; use Yiisoft\Di\Tests\Support\MethodTestClass; use Yiisoft\Di\Tests\Support\NullCarExtensionProvider; use Yiisoft\Di\Tests\Support\SportCar; -use Yiisoft\Definitions\Exception\InvalidConfigException; final class ServiceProviderTest extends TestCase { @@ -174,12 +174,12 @@ public function testClassMethodsWithExtensible(): void ]) ->withProviders([ new class () implements ServiceProviderInterface { - public function getDefinitions(): array + public function getDefinitions(): iterable { return []; } - public function getExtensions(): array + public function getExtensions(): iterable { return [ 'method_test' => static fn (ContainerInterface $container, MethodTestClass $class) => $class,