From 41c6a3aefb06c8bdc26b373c7e0ada418aac57d2 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 10:58:13 +0300 Subject: [PATCH 01/16] Add yiisoft/di container dev package, add container factory and state classes for Yii container support, add test --- composer.json | 3 +- composer.lock | 210 +++++++++++++++++- .../YIiDiContainerFactory.php | 60 +++++ .../YiiDiContainerFactoryState.php | 5 + .../YiiDiContainerFactoryTest.php | 23 ++ 5 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 src/ContainerFactory/YIiDiContainerFactory.php create mode 100644 src/ContainerFactory/YiiDiContainerFactoryState.php create mode 100644 test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php diff --git a/composer.json b/composer.json index b3da22ac..b13c0ca0 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "roave/security-advisories": "dev-latest", "vimeo/psalm": "^4.24", "illuminate/container": "^10.11", - "jetbrains/phpstorm-attributes": "^1.1" + "jetbrains/phpstorm-attributes": "^1.1", + "yiisoft/di": "^1.4" }, "bin": ["bin/annotated-container"], "autoload": { diff --git a/composer.lock b/composer.lock index 8934f031..881ac725 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d089e50e5bff4f71ec9d42bdec16aa60", + "content-hash": "e403ca99ff327a6cda4b7c3c2e26edbb", "packages": [ { "name": "brick/varexporter", @@ -5380,6 +5380,210 @@ }, "abandoned": "symfony/filesystem", "time": "2015-12-17T08:42:14+00:00" + }, + { + "name": "yiisoft/definitions", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/definitions.git", + "reference": "313dc892dfe1ad03be20ea7181d1c0a845023d98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/definitions/zipball/313dc892dfe1ad03be20ea7181d1c0a845023d98", + "reference": "313dc892dfe1ad03be20ea7181d1c0a845023d98", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/container": "^1.0 || ^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.69", + "maglnet/composer-require-checker": "^4.7.1", + "phpunit/phpunit": "^10.5.45", + "rector/rector": "^2.0.9", + "roave/infection-static-analysis-plugin": "^1.35", + "spatie/phpunit-watcher": "^1.24", + "vimeo/psalm": "^5.26.1 || ^6.7.1", + "yiisoft/test-support": "^3.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Yiisoft\\Definitions\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "The package provides definition syntax", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "definitions" + ], + "support": { + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/definitions/issues?state=open", + "source": "https://github.com/yiisoft/definitions", + "wiki": "https://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/sponsors/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" + } + ], + "time": "2025-03-02T16:55:21+00:00" + }, + { + "name": "yiisoft/di", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/di.git", + "reference": "95be35f7db8869efe55515e7c500c7337c70f8d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/di/zipball/95be35f7db8869efe55515e7c500c7337c70f8d0", + "reference": "95be35f7db8869efe55515e7c500c7337c70f8d0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "8.1 - 8.4", + "psr/container": "^1.1 || ^2.0", + "yiisoft/definitions": "^3.0", + "yiisoft/friendly-exception": "^1.1.0" + }, + "provide": { + "psr/container-implementation": "1.0.0" + }, + "require-dev": { + "league/container": "^5.1.0", + "maglnet/composer-require-checker": "^4.7.1", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^10.5.46", + "rector/rector": "^2.0.17", + "roave/infection-static-analysis-plugin": "^1.35", + "spatie/phpunit-watcher": "^1.24", + "vimeo/psalm": "^5.26.1 || ^6.12", + "yiisoft/injector": "^1.2", + "yiisoft/test-support": "^3.0.2" + }, + "suggest": { + "phpbench/phpbench": "To run benchmarks.", + "yiisoft/injector": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Yiisoft\\Di\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Yii DI container", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "Autowiring", + "PSR-11", + "container", + "dependency", + "di", + "injection", + "injector" + ], + "support": { + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/di/issues?state=open", + "source": "https://github.com/yiisoft/di", + "wiki": "https://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/sponsors/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" + } + ], + "time": "2025-05-30T11:38:08+00:00" + }, + { + "name": "yiisoft/friendly-exception", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/friendly-exception.git", + "reference": "4b4a19edff251791e3c92d4d83435d2716351ff4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/friendly-exception/zipball/4b4a19edff251791e3c92d4d83435d2716351ff4", + "reference": "4b4a19edff251791e3c92d4d83435d2716351ff4", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.4", + "roave/infection-static-analysis-plugin": "^1.5", + "spatie/phpunit-watcher": "^1.23", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Yiisoft\\FriendlyException\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "An interface for friendlier exception", + "homepage": "http://www.yiiframework.com/", + "keywords": [ + "error handling", + "exception", + "exceptions", + "friendly" + ], + "support": { + "forum": "http://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/friendly-exception/issues?state=open", + "source": "https://github.com/yiisoft/friendly-exception", + "wiki": "http://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + } + ], + "time": "2021-10-26T21:43:25+00:00" } ], "aliases": [], @@ -5395,6 +5599,6 @@ "ext-libxml": "*", "composer-runtime-api": "^2" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php new file mode 100644 index 00000000..af89b871 --- /dev/null +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -0,0 +1,60 @@ + Date: Tue, 3 Jun 2025 14:12:36 +0300 Subject: [PATCH 02/16] First implementation steps: single concrete service, named service, support constructor and property injection --- .../YIiDiContainerFactory.php | 103 ++++++++++++++++-- .../YiiDiContainerFactoryState.php | 87 ++++++++++++++- .../YiiDiContainerFactoryTest.php | 3 +- 3 files changed, 180 insertions(+), 13 deletions(-) diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php index af89b871..4018c95d 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -3,58 +3,141 @@ namespace Cspray\AnnotatedContainer\ContainerFactory; use Cspray\AnnotatedContainer\AnnotatedContainer; +use Cspray\AnnotatedContainer\Autowire\AutowireableParameterSet; use Cspray\AnnotatedContainer\ContainerFactory\AliasResolution\AliasDefinitionResolution; use Cspray\AnnotatedContainer\Definition\ConfigurationDefinition; use Cspray\AnnotatedContainer\Definition\InjectDefinition; use Cspray\AnnotatedContainer\Definition\ServiceDefinition; use Cspray\AnnotatedContainer\Definition\ServiceDelegateDefinition; use Cspray\AnnotatedContainer\Definition\ServicePrepareDefinition; +use Cspray\AnnotatedContainer\Exception\ServiceNotFound; +use Cspray\AnnotatedContainer\Exception\UnsupportedOperation; use Cspray\AnnotatedContainer\Profiles\ActiveProfiles; use Cspray\Typiphy\ObjectType; +use Yiisoft\Di\Container; +use Yiisoft\Di\ContainerConfig; +use function Cspray\Typiphy\objectType; -final class YIiDiContainerFactory extends AbstractContainerFactory implements ContainerFactory { +// @codeCoverageIgnoreStart +if (!class_exists(Container::class)) { + throw new \RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/di 1.4+!"); +} + +// @codeCoverageIgnoreEnd + + +final class YIiDiContainerFactory extends AbstractContainerFactory implements ContainerFactory +{ protected function getBackingContainerType(): ObjectType { - // TODO: Implement getBackingContainerType() method. + return objectType(Container::class); } protected function getContainerFactoryState(): ContainerFactoryState { - // TODO: Implement getContainerFactoryState() method. + return new YiiDiContainerFactoryState(); } protected function handleServiceDefinition(ContainerFactoryState $state, ServiceDefinition $definition): void { - // TODO: Implement handleServiceDefinition() method. + \assert($state instanceof YiiDiContainerFactoryState); + if ($definition->isAbstract()) { + $state->addAbstractService($definition->getType()->getName()); + } else { + $state->addConcreteService($definition->getType()->getName()); + } + $alias = $definition->getName(); + if ($alias !== null) { + $state->addNamedService($definition->getType()->getName(), $alias); + } } protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefinitionResolution $resolution): void { - // TODO: Implement handleAliasDefinition() method. + \assert($state instanceof YiiDiContainerFactoryState); + $definition = $resolution->getAliasDefinition(); + if ($definition !== null) { + $state->addAlias($definition->getAbstractService()->getName(), $definition->getConcreteService()->getName()); + } } protected function handleServiceDelegateDefinition(ContainerFactoryState $state, ServiceDelegateDefinition $definition): void { - // TODO: Implement handleServiceDelegateDefinition() method. + // TODO: implement me } protected function handleServicePrepareDefinition(ContainerFactoryState $state, ServicePrepareDefinition $definition): void { - // TODO: Implement handleServicePrepareDefinition() method. + // TODO: implement me } protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void { - // TODO: Implement handleInjectDefinition() method. + \assert($state instanceof YiiDiContainerFactoryState); + if ($definition->getTargetIdentifier()->isMethodParameter()) { + $state->addMethodInject( + $definition->getTargetIdentifier()->getClass()->getName(), + $definition->getTargetIdentifier()->getMethodName(), + $definition->getTargetIdentifier()->getName(), + $this->getInjectDefinitionValue($definition), + ); + } else { + $state->addPropertyInject( + $definition->getTargetIdentifier()->getClass()->getName(), + $definition->getTargetIdentifier()->getName(), + $this->getInjectDefinitionValue($definition), + ); + } } protected function handleConfigurationDefinition(ContainerFactoryState $state, ConfigurationDefinition $definition): void { - // TODO: Implement handleConfigurationDefinition() method. + // TODO: Configuration attribute is deprecated. Should this method be implemented? } protected function createAnnotatedContainer(ContainerFactoryState $state, ActiveProfiles $activeProfiles): AnnotatedContainer { - // TODO: Implement createAnnotatedContainer() method. + \assert($state instanceof YiiDiContainerFactoryState); + + $config = ContainerConfig::create() + ->withDefinitions($state->createDefinitions()) + ->withStrictMode(false); // TODO: review and check strict mode + + $container = new Container($config); + return new readonly class ($container) implements AnnotatedContainer { + public function __construct(private Container $container) + { + } + + public function getBackingContainer(): object + { + return $this->container; + } + + public function make(string $classType, ?AutowireableParameterSet $parameters = null): object + { + // TODO: implement me + throw UnsupportedOperation::fromMethodNotSupported(__METHOD__); + } + + public function invoke(callable $callable, ?AutowireableParameterSet $parameters = null): mixed + { + // TODO: implement me + throw UnsupportedOperation::fromMethodNotSupported(__METHOD__); + } + + public function get(string $id) + { + if (!$this->has($id)) { + throw ServiceNotFound::fromServiceNotInContainer($id); + } + return $this->container->get($id); + } + + public function has(string $id): bool + { + return $this->container->has($id); + } + }; } } diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index e9245dae..2b2525ed 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -2,4 +2,89 @@ namespace Cspray\AnnotatedContainer\ContainerFactory; -final class YiiDiContainerFactoryState implements ContainerFactoryState {} +use Yiisoft\Definitions\ArrayDefinition; +use Yiisoft\Definitions\Exception\InvalidConfigException; +use Yiisoft\Definitions\Reference; + +final class YiiDiContainerFactoryState implements ContainerFactoryState +{ + use HasMethodInjectState; + use HasPropertyInjectState; + + private array $abstractServices = []; + private array $concreteServices = []; + private array $namedServices = []; + private array $aliases = []; + + public function __construct() {} + + public function addConcreteService(string $name): void + { + $this->concreteServices[$name] = $name; + } + + public function addAbstractService(string $name): void + { + $this->abstractServices[$name] = $name; + } + + public function addNamedService(string $service, string $name): void + { + $this->namedServices[$name] = $service; + } + + public function addAlias(string $abstract, string $concrete): void + { + $this->aliases[$abstract] = $concrete; + } + + /** + * @throws InvalidConfigException + */ + public function createDefinitions(): array + { + $definitions = \array_map(fn($concrete) => $concrete, $this->aliases); + + foreach ($this->namedServices as $name => $service) { + if (\array_key_exists($name, $definitions) && $definitions[$name] !== $service) { + // TODO: should this exception be removed? + throw new \Exception("duplicate alias '{$name}' detected, please check"); + } + $definitions[$name] = $service; + } + + $methodInject = $this->getMethodInject(); + + foreach ($methodInject as $class => $path) { + $def = $definitions[$class] ?? ['class' => $class]; + + foreach ($path as $method => $val) { + $constructor = "$method()"; + if ($constructor === ArrayDefinition::CONSTRUCTOR) { + $def[$constructor] ??= []; + foreach ($val as $param => $value) { + $def[$constructor][$param] = Reference::to($value->name); + } + } + } + $definitions[$class] = $def; + } + + $propertiesInject = $this->getPropertyInject(); + + foreach ($propertiesInject as $class => $path) { + $def = $definitions[$class] ?? ['class' => $class]; + if (\is_string($def)) { + $def = [ + 'class' => $class, + ]; + } + foreach ($path as $property => $value) { + $def["\$$property"] = Reference::to($value->type->getName()); + } + $definitions[$class] = $def; + } + + return $definitions; + } +} diff --git a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php index 57d92b8a..979a2e40 100644 --- a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php +++ b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php @@ -3,12 +3,11 @@ namespace Cspray\AnnotatedContainer\Unit\ContainerFactoryTests; use Cspray\AnnotatedContainer\ContainerFactory\ContainerFactory; -use Cspray\AnnotatedContainer\ContainerFactory\PhpDiContainerFactory; use Cspray\AnnotatedContainer\ContainerFactory\YIiDiContainerFactory; use Cspray\AnnotatedContainer\Profiles\ActiveProfiles; use Cspray\AnnotatedContainer\Unit\ContainerFactoryTestCase; use Cspray\Typiphy\ObjectType; -use DI\Container; +use Yiisoft\Di\Container; use function Cspray\Typiphy\objectType; class YiiDiContainerFactoryTest extends ContainerFactoryTestCase { From 2dbc806f3e1e36d4618fea7f2b1ddef6942ee0bc Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 14:20:16 +0300 Subject: [PATCH 03/16] Temporarily skip tests for Configuration that is not implemented yet --- .../YiiDiContainerFactoryTest.php | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php index 979a2e40..9e8ecaad 100644 --- a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php +++ b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php @@ -10,13 +10,76 @@ use Yiisoft\Di\Container; use function Cspray\Typiphy\objectType; -class YiiDiContainerFactoryTest extends ContainerFactoryTestCase { +class YiiDiContainerFactoryTest extends ContainerFactoryTestCase +{ - protected function getContainerFactory(ActiveProfiles $activeProfiles) : ContainerFactory { + protected function getContainerFactory(ActiveProfiles $activeProfiles): ContainerFactory + { return new YIiDiContainerFactory(); } - protected function getBackingContainerInstanceOf() : ObjectType { + protected function getBackingContainerInstanceOf(): ObjectType + { return objectType(Container::class); } + + public function testCreateArbitraryStoreOnConfigurationNotPresent() + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testConfigurationSharedInstance() + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testConfigurationValues() + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testNamedConfigurationInstanceOf() + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testLoggingConfiguration(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testLoggingNamedConfiguration(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testLoggingInjectNonServiceNotFromStoreConfigurationProperty(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testLoggingInjectEnumFromConfigurationProperty(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testLoggingInjectValueFromStoreForConfigurationProperty(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testLoggingInjectServiceForConfigurationProperty(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testCreatingConstructorPromotedConfiguration(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + public function testCreatingAliasedConfiguration(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } } From 8899473ef4d0b54ef66ded148933f945b21ccd03 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 16:51:04 +0300 Subject: [PATCH 04/16] Create Yii DI container in strict mode (all services must be explicitly defined), implement definition of AutowireableFactory, AutowireableInvoker and ActiveProfiles --- .../YIiDiContainerFactory.php | 35 +++++++++----- .../YiiDiContainerFactoryState.php | 46 ++++++++++++++++--- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php index 4018c95d..d0fda439 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -3,6 +3,8 @@ namespace Cspray\AnnotatedContainer\ContainerFactory; use Cspray\AnnotatedContainer\AnnotatedContainer; +use Cspray\AnnotatedContainer\Autowire\AutowireableFactory; +use Cspray\AnnotatedContainer\Autowire\AutowireableInvoker; use Cspray\AnnotatedContainer\Autowire\AutowireableParameterSet; use Cspray\AnnotatedContainer\ContainerFactory\AliasResolution\AliasDefinitionResolution; use Cspray\AnnotatedContainer\Definition\ConfigurationDefinition; @@ -14,13 +16,15 @@ use Cspray\AnnotatedContainer\Exception\UnsupportedOperation; use Cspray\AnnotatedContainer\Profiles\ActiveProfiles; use Cspray\Typiphy\ObjectType; +use RuntimeException; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; +use function assert; use function Cspray\Typiphy\objectType; // @codeCoverageIgnoreStart if (!class_exists(Container::class)) { - throw new \RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/di 1.4+!"); + throw new RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/di 1.4+!"); } // @codeCoverageIgnoreEnd @@ -40,7 +44,7 @@ protected function getContainerFactoryState(): ContainerFactoryState protected function handleServiceDefinition(ContainerFactoryState $state, ServiceDefinition $definition): void { - \assert($state instanceof YiiDiContainerFactoryState); + assert($state instanceof YiiDiContainerFactoryState); if ($definition->isAbstract()) { $state->addAbstractService($definition->getType()->getName()); } else { @@ -48,13 +52,13 @@ protected function handleServiceDefinition(ContainerFactoryState $state, Service } $alias = $definition->getName(); if ($alias !== null) { - $state->addNamedService($definition->getType()->getName(), $alias); + $state->addNamedService($alias, $definition->getType()->getName()); } } protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefinitionResolution $resolution): void { - \assert($state instanceof YiiDiContainerFactoryState); + assert($state instanceof YiiDiContainerFactoryState); $definition = $resolution->getAliasDefinition(); if ($definition !== null) { $state->addAlias($definition->getAbstractService()->getName(), $definition->getConcreteService()->getName()); @@ -73,7 +77,7 @@ protected function handleServicePrepareDefinition(ContainerFactoryState $state, protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void { - \assert($state instanceof YiiDiContainerFactoryState); + assert($state instanceof YiiDiContainerFactoryState); if ($definition->getTargetIdentifier()->isMethodParameter()) { $state->addMethodInject( $definition->getTargetIdentifier()->getClass()->getName(), @@ -97,16 +101,23 @@ protected function handleConfigurationDefinition(ContainerFactoryState $state, C protected function createAnnotatedContainer(ContainerFactoryState $state, ActiveProfiles $activeProfiles): AnnotatedContainer { - \assert($state instanceof YiiDiContainerFactoryState); + assert($state instanceof YiiDiContainerFactoryState); - $config = ContainerConfig::create() - ->withDefinitions($state->createDefinitions()) - ->withStrictMode(false); // TODO: review and check strict mode + $state->addInstance(ActiveProfiles::class, $activeProfiles); - $container = new Container($config); - return new readonly class ($container) implements AnnotatedContainer { - public function __construct(private Container $container) + return new readonly class ($state) implements AnnotatedContainer { + private Container $container; + + public function __construct(YiiDiContainerFactoryState $state) { + $state->addInstance(AutowireableFactory::class, $this); + $state->addInstance(AutowireableInvoker::class, $this); + + $config = ContainerConfig::create() + ->withDefinitions($state->createDefinitions()) + ->withStrictMode(); + + $this->container = new Container($config); } public function getBackingContainer(): object diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index 2b2525ed..ad66e0de 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -2,9 +2,13 @@ namespace Cspray\AnnotatedContainer\ContainerFactory; +use Exception; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Reference; +use function array_key_exists; +use function array_map; +use function is_string; final class YiiDiContainerFactoryState implements ContainerFactoryState { @@ -13,10 +17,15 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState private array $abstractServices = []; private array $concreteServices = []; + /** @var array */ private array $namedServices = []; + /** @var array */ private array $aliases = []; + private array $instances = []; - public function __construct() {} + public function __construct() + { + } public function addConcreteService(string $name): void { @@ -28,7 +37,7 @@ public function addAbstractService(string $name): void $this->abstractServices[$name] = $name; } - public function addNamedService(string $service, string $name): void + public function addNamedService(string $name, string $service): void { $this->namedServices[$name] = $service; } @@ -38,17 +47,28 @@ public function addAlias(string $abstract, string $concrete): void $this->aliases[$abstract] = $concrete; } + public function getAliases(): array + { + return $this->aliases; + } + + public function addInstance(string $name, object $instance): void + { + $this->instances[$name] = $instance; + } + /** * @throws InvalidConfigException + * @throws Exception */ public function createDefinitions(): array { - $definitions = \array_map(fn($concrete) => $concrete, $this->aliases); + $definitions = array_map(fn($concrete): string => $concrete, $this->aliases); foreach ($this->namedServices as $name => $service) { - if (\array_key_exists($name, $definitions) && $definitions[$name] !== $service) { + if (array_key_exists($name, $definitions) && $definitions[$name] !== $service) { // TODO: should this exception be removed? - throw new \Exception("duplicate alias '{$name}' detected, please check"); + throw new Exception("duplicate alias '$name' while trying to define named service"); } $definitions[$name] = $service; } @@ -74,7 +94,7 @@ public function createDefinitions(): array foreach ($propertiesInject as $class => $path) { $def = $definitions[$class] ?? ['class' => $class]; - if (\is_string($def)) { + if (is_string($def)) { $def = [ 'class' => $class, ]; @@ -85,6 +105,20 @@ public function createDefinitions(): array $definitions[$class] = $def; } + foreach ($this->instances as $key => $value) { + if (array_key_exists($key, $definitions) && $definitions[$key] !== $value) { + // TODO: remove me + throw new Exception("Duplicate alias '$key' while trying to define value"); + } + $definitions[$key] = $value; + +// $definitions[$key] = $definitions[$key] ?? $value; + } + + foreach ($this->concreteServices as $concrete) { + $definitions[$concrete] = $definitions[$concrete] ?? $concrete; + } + return $definitions; } } From 6a99cd4e68a58684e27ef2fd3eb5c7f069bce693 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 17:41:09 +0300 Subject: [PATCH 05/16] Fix constructor and property injection definitions --- src/ContainerFactory/YiiDiContainerFactoryState.php | 5 +++-- .../ContainerFactoryTests/YiiDiContainerFactoryTest.php | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index ad66e0de..4ebf4d23 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -83,7 +83,7 @@ public function createDefinitions(): array if ($constructor === ArrayDefinition::CONSTRUCTOR) { $def[$constructor] ??= []; foreach ($val as $param => $value) { - $def[$constructor][$param] = Reference::to($value->name); + $def[$constructor][$param] = $value instanceof ContainerReference ? Reference::to($value->name) : $value; } } } @@ -100,7 +100,8 @@ public function createDefinitions(): array ]; } foreach ($path as $property => $value) { - $def["\$$property"] = Reference::to($value->type->getName()); + // TODO: check case with container reference one more carefully ($value->name or $value->type->getName()) + $def["\$$property"] = $value instanceof ContainerReference ? Reference::to($value->name) : $value; } $definitions[$class] = $def; } diff --git a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php index 9e8ecaad..3a3229ab 100644 --- a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php +++ b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php @@ -82,4 +82,11 @@ public function testCreatingAliasedConfiguration(): void { $this->markTestSkipped('Configuration is not supported yet'); } + + public function testLoggingInjectPropertyArrayNotMultiline(): void + { + $this->markTestSkipped('Configuration is not supported yet'); + } + + } From ec606aa5be0aeab4d4103a33c8b6474bb55571d2 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 18:51:05 +0300 Subject: [PATCH 06/16] Implement service prepare definitions --- .../YIiDiContainerFactory.php | 3 ++- .../YiiDiContainerFactoryState.php | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php index d0fda439..2da7190c 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -72,7 +72,8 @@ protected function handleServiceDelegateDefinition(ContainerFactoryState $state, protected function handleServicePrepareDefinition(ContainerFactoryState $state, ServicePrepareDefinition $definition): void { - // TODO: implement me + assert($state instanceof YiiDiContainerFactoryState); + $state->addServicePrepare($definition->getService()->getName(), $definition->getMethod()); } protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index 4ebf4d23..86a945b0 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -14,12 +14,13 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState { use HasMethodInjectState; use HasPropertyInjectState; + use HasServicePrepareState; private array $abstractServices = []; private array $concreteServices = []; - /** @var array */ + /** @var array */ private array $namedServices = []; - /** @var array */ + /** @var array */ private array $aliases = []; private array $instances = []; @@ -76,7 +77,7 @@ public function createDefinitions(): array $methodInject = $this->getMethodInject(); foreach ($methodInject as $class => $path) { - $def = $definitions[$class] ?? ['class' => $class]; + $def = $definitions[$class] ?? [ArrayDefinition::CLASS_NAME => $class]; foreach ($path as $method => $val) { $constructor = "$method()"; @@ -93,10 +94,10 @@ public function createDefinitions(): array $propertiesInject = $this->getPropertyInject(); foreach ($propertiesInject as $class => $path) { - $def = $definitions[$class] ?? ['class' => $class]; + $def = $definitions[$class] ?? [ArrayDefinition::CLASS_NAME => $class]; if (is_string($def)) { $def = [ - 'class' => $class, + ArrayDefinition::CLASS_NAME => $class, ]; } foreach ($path as $property => $value) { @@ -120,6 +121,16 @@ public function createDefinitions(): array $definitions[$concrete] = $definitions[$concrete] ?? $concrete; } + foreach ($this->getServicePrepares() as $service => $methods) { + if ($definitions[$service]) { + $def = is_string($definitions[$service]) ? [ArrayDefinition::CLASS_NAME => $definitions[$service]] : $definitions[$service]; + foreach ($methods as $method) { + $def["$method()"] = []; + } + $definitions[$service] = $def; + } + } + return $definitions; } } From 1718f6b1207ecaa31626aa393277c626d044bc39 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 20:26:04 +0300 Subject: [PATCH 07/16] Add yiisoft/injector package and implement container make method --- composer.json | 3 +- composer.lock | 72 ++++++++++++++++++- .../YIiDiContainerFactory.php | 31 +++++++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index b13c0ca0..89ce0e8e 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "vimeo/psalm": "^4.24", "illuminate/container": "^10.11", "jetbrains/phpstorm-attributes": "^1.1", - "yiisoft/di": "^1.4" + "yiisoft/di": "^1.4", + "yiisoft/injector": "^1.2" }, "bin": ["bin/annotated-container"], "autoload": { diff --git a/composer.lock b/composer.lock index 881ac725..58ce6655 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e403ca99ff327a6cda4b7c3c2e26edbb", + "content-hash": "d4a83d908309b9b76a446e703dfdcc3f", "packages": [ { "name": "brick/varexporter", @@ -5584,6 +5584,76 @@ } ], "time": "2021-10-26T21:43:25+00:00" + }, + { + "name": "yiisoft/injector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/injector.git", + "reference": "0dc0127a7542341bdaabda7b85204e992938b83e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/injector/zipball/0dc0127a7542341bdaabda7b85204e992938b83e", + "reference": "0dc0127a7542341bdaabda7b85204e992938b83e", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "require-dev": { + "maglnet/composer-require-checker": "^3.8|^4.2", + "phpbench/phpbench": "^1.1", + "phpunit/phpunit": "^9.5", + "psr/container": "^1.0|^2.0", + "rector/rector": "^0.18.12", + "roave/infection-static-analysis-plugin": "^1.16", + "spatie/phpunit-watcher": "^1.23", + "vimeo/psalm": "^4.30|^5.7", + "yiisoft/test-support": "^1.2" + }, + "suggest": { + "psr/container": "For automatic resolving of dependencies" + }, + "type": "library", + "autoload": { + "psr-4": { + "Yiisoft\\Injector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR-11 compatible injector. Executes a callable and makes an instances by injecting dependencies from a given DI container.", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "PSR-11", + "dependency injection", + "di", + "injector", + "reflection" + ], + "support": { + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/injector/issues?state=open", + "source": "https://github.com/yiisoft/injector", + "wiki": "https://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + } + ], + "time": "2023-12-20T09:39:03+00:00" } ], "aliases": [], diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php index 2da7190c..5602bfb8 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -5,6 +5,7 @@ use Cspray\AnnotatedContainer\AnnotatedContainer; use Cspray\AnnotatedContainer\Autowire\AutowireableFactory; use Cspray\AnnotatedContainer\Autowire\AutowireableInvoker; +use Cspray\AnnotatedContainer\Autowire\AutowireableParameter; use Cspray\AnnotatedContainer\Autowire\AutowireableParameterSet; use Cspray\AnnotatedContainer\ContainerFactory\AliasResolution\AliasDefinitionResolution; use Cspray\AnnotatedContainer\Definition\ConfigurationDefinition; @@ -12,6 +13,7 @@ use Cspray\AnnotatedContainer\Definition\ServiceDefinition; use Cspray\AnnotatedContainer\Definition\ServiceDelegateDefinition; use Cspray\AnnotatedContainer\Definition\ServicePrepareDefinition; +use Cspray\AnnotatedContainer\Exception\ParameterStoreNotFound; use Cspray\AnnotatedContainer\Exception\ServiceNotFound; use Cspray\AnnotatedContainer\Exception\UnsupportedOperation; use Cspray\AnnotatedContainer\Profiles\ActiveProfiles; @@ -19,6 +21,7 @@ use RuntimeException; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; +use Yiisoft\Injector\Injector; use function assert; use function Cspray\Typiphy\objectType; @@ -26,6 +29,12 @@ if (!class_exists(Container::class)) { throw new RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/di 1.4+!"); } +// @codeCoverageIgnoreEnd + +// @codeCoverageIgnoreStart +if (!class_exists(Injector::class)) { + throw new RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/injector 1.2+!"); +} // @codeCoverageIgnoreEnd @@ -76,6 +85,9 @@ protected function handleServicePrepareDefinition(ContainerFactoryState $state, $state->addServicePrepare($definition->getService()->getName(), $definition->getMethod()); } + /** + * @throws ParameterStoreNotFound + */ protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void { assert($state instanceof YiiDiContainerFactoryState); @@ -108,6 +120,7 @@ protected function createAnnotatedContainer(ContainerFactoryState $state, Active return new readonly class ($state) implements AnnotatedContainer { private Container $container; + private Injector $injector; public function __construct(YiiDiContainerFactoryState $state) { @@ -119,6 +132,7 @@ public function __construct(YiiDiContainerFactoryState $state) ->withStrictMode(); $this->container = new Container($config); + $this->injector = (new Injector($this->container))->withCacheReflections(); // TODO: check memory usage with cache reflections } public function getBackingContainer(): object @@ -128,8 +142,7 @@ public function getBackingContainer(): object public function make(string $classType, ?AutowireableParameterSet $parameters = null): object { - // TODO: implement me - throw UnsupportedOperation::fromMethodNotSupported(__METHOD__); + return $this->injector->make($classType, $this->convertAutowireableParameterSet($parameters)); } public function invoke(callable $callable, ?AutowireableParameterSet $parameters = null): mixed @@ -150,6 +163,20 @@ public function has(string $id): bool { return $this->container->has($id); } + + private function convertAutowireableParameterSet(?AutowireableParameterSet $parameters = null): array + { + $params = []; + if (!is_null($parameters)) { + /** @var AutowireableParameter $parameter */ + foreach ($parameters as $parameter) { + $name = $parameter->getName(); + $value = $parameter->isServiceIdentifier() ? $this->injector->make($parameter->getValue()->getName()) : $parameter->getValue(); + $params[$name] = $value; + } + } + return $params; + } }; } } From 28512808af59a79195c219611ae75d05fde6c761 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 20:55:27 +0300 Subject: [PATCH 08/16] Implement container invoke method, fixes service prepare implementation with parameters injected --- .../YIiDiContainerFactory.php | 10 ++++++--- .../YiiDiContainerFactoryState.php | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php index 5602bfb8..9caccb4b 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -147,8 +147,7 @@ public function make(string $classType, ?AutowireableParameterSet $parameters = public function invoke(callable $callable, ?AutowireableParameterSet $parameters = null): mixed { - // TODO: implement me - throw UnsupportedOperation::fromMethodNotSupported(__METHOD__); + return $this->injector->invoke($callable, $this->convertAutowireableParameterSet($parameters)); } public function get(string $id) @@ -171,7 +170,12 @@ private function convertAutowireableParameterSet(?AutowireableParameterSet $para /** @var AutowireableParameter $parameter */ foreach ($parameters as $parameter) { $name = $parameter->getName(); - $value = $parameter->isServiceIdentifier() ? $this->injector->make($parameter->getValue()->getName()) : $parameter->getValue(); + if ($parameter->isServiceIdentifier()) { + $serviceIdentifier = $parameter->getValue()->getName(); + $value = $this->container->has($serviceIdentifier) ? $this->container->get($serviceIdentifier) : $this->make($serviceIdentifier); + } else { + $value = $parameter->getValue(); + } $params[$name] = $value; } } diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index 86a945b0..81859532 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -84,7 +84,7 @@ public function createDefinitions(): array if ($constructor === ArrayDefinition::CONSTRUCTOR) { $def[$constructor] ??= []; foreach ($val as $param => $value) { - $def[$constructor][$param] = $value instanceof ContainerReference ? Reference::to($value->name) : $value; + $def[$constructor][$param] = $this->parameterValueOrReference($value); } } } @@ -101,8 +101,7 @@ public function createDefinitions(): array ]; } foreach ($path as $property => $value) { - // TODO: check case with container reference one more carefully ($value->name or $value->type->getName()) - $def["\$$property"] = $value instanceof ContainerReference ? Reference::to($value->name) : $value; + $def["\$$property"] = $this->parameterValueOrReference($value); } $definitions[$class] = $def; } @@ -125,7 +124,11 @@ public function createDefinitions(): array if ($definitions[$service]) { $def = is_string($definitions[$service]) ? [ArrayDefinition::CLASS_NAME => $definitions[$service]] : $definitions[$service]; foreach ($methods as $method) { - $def["$method()"] = []; + $params = []; + foreach ($this->parametersForMethod($service, $method) as $param => $value) { + $params[$param] = $this->parameterValueOrReference($value); + } + $def["$method()"] = $params; } $definitions[$service] = $def; } @@ -133,4 +136,14 @@ public function createDefinitions(): array return $definitions; } + + /** + * @throws InvalidConfigException + */ + private function parameterValueOrReference(mixed $value): mixed + { + // TODO: check case with container reference more carefully + // especially for property inject ($value->name or $value->type->getName()) + return $value instanceof ContainerReference ? Reference::to($value->name) : $value; + } } From 08880871b7e8c87dd60d3130d754b692a03938f4 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 3 Jun 2025 21:48:12 +0300 Subject: [PATCH 09/16] Implement service delegate --- .../YIiDiContainerFactory.php | 8 +++- .../YiiDiContainerFactoryState.php | 37 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YIiDiContainerFactory.php index 9caccb4b..74a7ce0a 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YIiDiContainerFactory.php @@ -15,7 +15,6 @@ use Cspray\AnnotatedContainer\Definition\ServicePrepareDefinition; use Cspray\AnnotatedContainer\Exception\ParameterStoreNotFound; use Cspray\AnnotatedContainer\Exception\ServiceNotFound; -use Cspray\AnnotatedContainer\Exception\UnsupportedOperation; use Cspray\AnnotatedContainer\Profiles\ActiveProfiles; use Cspray\Typiphy\ObjectType; use RuntimeException; @@ -76,7 +75,12 @@ protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefi protected function handleServiceDelegateDefinition(ContainerFactoryState $state, ServiceDelegateDefinition $definition): void { - // TODO: implement me + assert($state instanceof YiiDiContainerFactoryState); + $state->addServiceDelegate( + $definition->getServiceType()->getName(), + $definition->getDelegateType()->getName(), + $definition->getDelegateMethod() + ); } protected function handleServicePrepareDefinition(ContainerFactoryState $state, ServicePrepareDefinition $definition): void diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index 81859532..2f305fee 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -2,11 +2,12 @@ namespace Cspray\AnnotatedContainer\ContainerFactory; +use Cspray\AnnotatedContainer\Autowire\AutowireableInvoker; use Exception; +use Psr\Container\ContainerInterface; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Reference; -use function array_key_exists; use function array_map; use function is_string; @@ -18,8 +19,13 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState private array $abstractServices = []; private array $concreteServices = []; + /** @var array */ private array $namedServices = []; + + /** @var array> */ + private array $serviceDelegate = []; + /** @var array */ private array $aliases = []; private array $instances = []; @@ -58,6 +64,11 @@ public function addInstance(string $name, object $instance): void $this->instances[$name] = $instance; } + public function addServiceDelegate(string $service, string $delegate, string $delegateMethod): void + { + $this->serviceDelegate[$service] = [$delegate, $delegateMethod]; + } + /** * @throws InvalidConfigException * @throws Exception @@ -66,11 +77,14 @@ public function createDefinitions(): array { $definitions = array_map(fn($concrete): string => $concrete, $this->aliases); + foreach ($this->serviceDelegate as $service => [$delegate, $method]) { + $definitions[$service] = static function (AutowireableInvoker $invoker, ContainerInterface $container) use ($delegate, $method){ + $factory = $container->has($delegate) ? $container->get($delegate) : $invoker->make($delegate); + return $invoker->invoke($factory->$method(...)); + }; + } + foreach ($this->namedServices as $name => $service) { - if (array_key_exists($name, $definitions) && $definitions[$name] !== $service) { - // TODO: should this exception be removed? - throw new Exception("duplicate alias '$name' while trying to define named service"); - } $definitions[$name] = $service; } @@ -107,13 +121,7 @@ public function createDefinitions(): array } foreach ($this->instances as $key => $value) { - if (array_key_exists($key, $definitions) && $definitions[$key] !== $value) { - // TODO: remove me - throw new Exception("Duplicate alias '$key' while trying to define value"); - } - $definitions[$key] = $value; - -// $definitions[$key] = $definitions[$key] ?? $value; + $definitions[$key] = $definitions[$key] ?? $value; } foreach ($this->concreteServices as $concrete) { @@ -124,10 +132,7 @@ public function createDefinitions(): array if ($definitions[$service]) { $def = is_string($definitions[$service]) ? [ArrayDefinition::CLASS_NAME => $definitions[$service]] : $definitions[$service]; foreach ($methods as $method) { - $params = []; - foreach ($this->parametersForMethod($service, $method) as $param => $value) { - $params[$param] = $this->parameterValueOrReference($value); - } + $params = array_map(fn ($value) => $this->parameterValueOrReference($value), $this->parametersForMethod($service, $method)); $def["$method()"] = $params; } $definitions[$service] = $def; From b0e27ef2d469e6c3e5f3598279947f153b3ea0cf Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Wed, 4 Jun 2025 00:42:44 +0300 Subject: [PATCH 10/16] rollback skipped Configuration related tests and implement Configuration support using native Yii DI's property injection for public and writable properties and Yii's tags based workaround for private and readonly ones. Fixes typo in factory class name. Remove some todos --- ...rFactory.php => YiiDiContainerFactory.php} | 29 ++++++-- .../YiiDiContainerFactoryState.php | 26 ++++++- .../YiiDiContainerFactoryTest.php | 70 +------------------ 3 files changed, 50 insertions(+), 75 deletions(-) rename src/ContainerFactory/{YIiDiContainerFactory.php => YiiDiContainerFactory.php} (85%) diff --git a/src/ContainerFactory/YIiDiContainerFactory.php b/src/ContainerFactory/YiiDiContainerFactory.php similarity index 85% rename from src/ContainerFactory/YIiDiContainerFactory.php rename to src/ContainerFactory/YiiDiContainerFactory.php index 74a7ce0a..333baf90 100644 --- a/src/ContainerFactory/YIiDiContainerFactory.php +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -20,6 +20,7 @@ use RuntimeException; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; +use Yiisoft\Di\Reference\TagReference; use Yiisoft\Injector\Injector; use function assert; use function Cspray\Typiphy\objectType; @@ -38,7 +39,7 @@ // @codeCoverageIgnoreEnd -final class YIiDiContainerFactory extends AbstractContainerFactory implements ContainerFactory +final class YiiDiContainerFactory extends AbstractContainerFactory implements ContainerFactory { protected function getBackingContainerType(): ObjectType { @@ -113,7 +114,12 @@ protected function handleInjectDefinition(ContainerFactoryState $state, InjectDe protected function handleConfigurationDefinition(ContainerFactoryState $state, ConfigurationDefinition $definition): void { - // TODO: Configuration attribute is deprecated. Should this method be implemented? + assert($state instanceof YiiDiContainerFactoryState); + $state->addConcreteService($definition->getClass()->getName()); + $name = $definition->getName(); + if ($name !== null) { + $state->addNamedService($name, $definition->getClass()->getName()); + } } protected function createAnnotatedContainer(ContainerFactoryState $state, ActiveProfiles $activeProfiles): AnnotatedContainer @@ -133,10 +139,25 @@ public function __construct(YiiDiContainerFactoryState $state) $config = ContainerConfig::create() ->withDefinitions($state->createDefinitions()) - ->withStrictMode(); + ->withStrictMode() + ->withValidate(false); $this->container = new Container($config); - $this->injector = (new Injector($this->container))->withCacheReflections(); // TODO: check memory usage with cache reflections + $this->injector = (new Injector($this->container))->withCacheReflections(); + + $servicesWithReadOnlyProperties = $this->container->get(TagReference::id(YiiDiContainerFactoryState::TAG_INJECT_READ_ONLY_PROPERTIES)); + + foreach ($servicesWithReadOnlyProperties as $service) { + $properties = $state->getReadOnlyPropertyInjectsForService($service::class); + /** + * @var \ReflectionProperty $reflectionProperty + * @var mixed $def + */ + foreach ($properties as [$reflectionProperty, $def]) { + $value = $def instanceof ContainerReference ? $this->container->get($def->type->getName()) : $def; + $reflectionProperty->setValue($service, $value); + } + } } public function getBackingContainer(): object diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index 2f305fee..834ce382 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -5,6 +5,7 @@ use Cspray\AnnotatedContainer\Autowire\AutowireableInvoker; use Exception; use Psr\Container\ContainerInterface; +use ReflectionProperty; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Reference; @@ -13,6 +14,8 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState { + const TAG_INJECT_READ_ONLY_PROPERTIES = '__inject_read_only_properties'; + use HasMethodInjectState; use HasPropertyInjectState; use HasServicePrepareState; @@ -26,6 +29,8 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState /** @var array> */ private array $serviceDelegate = []; + private array $readOnlyPropertyInject = []; + /** @var array */ private array $aliases = []; private array $instances = []; @@ -69,6 +74,11 @@ public function addServiceDelegate(string $service, string $delegate, string $de $this->serviceDelegate[$service] = [$delegate, $delegateMethod]; } + public function getReadOnlyPropertyInjectsForService(string $service): array + { + return $this->readOnlyPropertyInject[$service] ?? []; + } + /** * @throws InvalidConfigException * @throws Exception @@ -115,7 +125,18 @@ public function createDefinitions(): array ]; } foreach ($path as $property => $value) { - $def["\$$property"] = $this->parameterValueOrReference($value); + $reflectionProperty = new ReflectionProperty($class, $property); + if ($reflectionProperty->isPublic() && !$reflectionProperty->isReadOnly()) { + // Yii DI natively supports property injection only for public and writable properties + $def["\$$property"] = $this->parameterValueOrReference($value); + } else { + // add tag to service classes that have non-public or read-only properties + // for injecting them manually after container creation + $def['tags'] ??= []; + $def['tags'][] = self::TAG_INJECT_READ_ONLY_PROPERTIES; + $this->readOnlyPropertyInject[$class] ??= []; + $this->readOnlyPropertyInject[$class][] = [$reflectionProperty, $value]; + } } $definitions[$class] = $def; } @@ -147,8 +168,7 @@ public function createDefinitions(): array */ private function parameterValueOrReference(mixed $value): mixed { - // TODO: check case with container reference more carefully - // especially for property inject ($value->name or $value->type->getName()) return $value instanceof ContainerReference ? Reference::to($value->name) : $value; } + } diff --git a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php index 3a3229ab..45573d5d 100644 --- a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php +++ b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php @@ -3,7 +3,7 @@ namespace Cspray\AnnotatedContainer\Unit\ContainerFactoryTests; use Cspray\AnnotatedContainer\ContainerFactory\ContainerFactory; -use Cspray\AnnotatedContainer\ContainerFactory\YIiDiContainerFactory; +use Cspray\AnnotatedContainer\ContainerFactory\YiiDiContainerFactory; use Cspray\AnnotatedContainer\Profiles\ActiveProfiles; use Cspray\AnnotatedContainer\Unit\ContainerFactoryTestCase; use Cspray\Typiphy\ObjectType; @@ -15,7 +15,7 @@ class YiiDiContainerFactoryTest extends ContainerFactoryTestCase protected function getContainerFactory(ActiveProfiles $activeProfiles): ContainerFactory { - return new YIiDiContainerFactory(); + return new YiiDiContainerFactory(); } protected function getBackingContainerInstanceOf(): ObjectType @@ -23,70 +23,4 @@ protected function getBackingContainerInstanceOf(): ObjectType return objectType(Container::class); } - public function testCreateArbitraryStoreOnConfigurationNotPresent() - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testConfigurationSharedInstance() - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testConfigurationValues() - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testNamedConfigurationInstanceOf() - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingConfiguration(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingNamedConfiguration(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingInjectNonServiceNotFromStoreConfigurationProperty(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingInjectEnumFromConfigurationProperty(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingInjectValueFromStoreForConfigurationProperty(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingInjectServiceForConfigurationProperty(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testCreatingConstructorPromotedConfiguration(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testCreatingAliasedConfiguration(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - public function testLoggingInjectPropertyArrayNotMultiline(): void - { - $this->markTestSkipped('Configuration is not supported yet'); - } - - } From 8f887ad4d41bfd3cebae1a4c0995d493078b6b06 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Wed, 4 Jun 2025 01:02:13 +0300 Subject: [PATCH 11/16] Remove readonly class (supported since 8.3, but 8.1 min supported in the project) --- src/ContainerFactory/YiiDiContainerFactory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ContainerFactory/YiiDiContainerFactory.php b/src/ContainerFactory/YiiDiContainerFactory.php index 333baf90..ce9edcca 100644 --- a/src/ContainerFactory/YiiDiContainerFactory.php +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -128,9 +128,9 @@ protected function createAnnotatedContainer(ContainerFactoryState $state, Active $state->addInstance(ActiveProfiles::class, $activeProfiles); - return new readonly class ($state) implements AnnotatedContainer { - private Container $container; - private Injector $injector; + return new class ($state) implements AnnotatedContainer { + private readonly Container $container; + private readonly Injector $injector; public function __construct(YiiDiContainerFactoryState $state) { From 2d22d9192586ee7ea4505fa2b3f8755d4c573776 Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Wed, 4 Jun 2025 10:45:00 +0300 Subject: [PATCH 12/16] Fix getContainerFactoryState signature after merge with release-2.x branch --- src/ContainerFactory/YiiDiContainerFactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ContainerFactory/YiiDiContainerFactory.php b/src/ContainerFactory/YiiDiContainerFactory.php index ce9edcca..ed979fcb 100644 --- a/src/ContainerFactory/YiiDiContainerFactory.php +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -9,6 +9,7 @@ use Cspray\AnnotatedContainer\Autowire\AutowireableParameterSet; use Cspray\AnnotatedContainer\ContainerFactory\AliasResolution\AliasDefinitionResolution; use Cspray\AnnotatedContainer\Definition\ConfigurationDefinition; +use Cspray\AnnotatedContainer\Definition\ContainerDefinition; use Cspray\AnnotatedContainer\Definition\InjectDefinition; use Cspray\AnnotatedContainer\Definition\ServiceDefinition; use Cspray\AnnotatedContainer\Definition\ServiceDelegateDefinition; @@ -46,7 +47,7 @@ protected function getBackingContainerType(): ObjectType return objectType(Container::class); } - protected function getContainerFactoryState(): ContainerFactoryState + protected function getContainerFactoryState(ContainerDefinition $containerDefinition): ContainerFactoryState { return new YiiDiContainerFactoryState(); } From a50d5cd123ebcd6c9674f42e56d5ad20ed31b26e Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Wed, 4 Jun 2025 21:28:45 +0300 Subject: [PATCH 13/16] Fixes getContainerFactoryState signature, implement support for injecting a collection of services --- .../YiiDiContainerFactory.php | 2 +- .../YiiDiContainerFactoryState.php | 78 ++++++++++++++++--- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/ContainerFactory/YiiDiContainerFactory.php b/src/ContainerFactory/YiiDiContainerFactory.php index ed979fcb..2d0e7f3e 100644 --- a/src/ContainerFactory/YiiDiContainerFactory.php +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -49,7 +49,7 @@ protected function getBackingContainerType(): ObjectType protected function getContainerFactoryState(ContainerDefinition $containerDefinition): ContainerFactoryState { - return new YiiDiContainerFactoryState(); + return new YiiDiContainerFactoryState($containerDefinition); } protected function handleServiceDefinition(ContainerFactoryState $state, ServiceDefinition $definition): void diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index 834ce382..e6718847 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -3,13 +3,17 @@ namespace Cspray\AnnotatedContainer\ContainerFactory; use Cspray\AnnotatedContainer\Autowire\AutowireableInvoker; -use Exception; +use Cspray\AnnotatedContainer\Definition\ContainerDefinition; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use ReflectionException; use ReflectionProperty; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Reference; use function array_map; +use function Cspray\Typiphy\arrayType; use function is_string; final class YiiDiContainerFactoryState implements ContainerFactoryState @@ -35,7 +39,7 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState private array $aliases = []; private array $instances = []; - public function __construct() + public function __construct(private readonly ContainerDefinition $containerDefinition) { } @@ -80,16 +84,18 @@ public function getReadOnlyPropertyInjectsForService(string $service): array } /** + * @throws ContainerExceptionInterface * @throws InvalidConfigException - * @throws Exception + * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function createDefinitions(): array { $definitions = array_map(fn($concrete): string => $concrete, $this->aliases); foreach ($this->serviceDelegate as $service => [$delegate, $method]) { - $definitions[$service] = static function (AutowireableInvoker $invoker, ContainerInterface $container) use ($delegate, $method){ - $factory = $container->has($delegate) ? $container->get($delegate) : $invoker->make($delegate); + $definitions[$service] = static function (AutowireableInvoker $invoker) use ($delegate, $method) { + $factory = $invoker->make($delegate); return $invoker->invoke($factory->$method(...)); }; } @@ -102,16 +108,41 @@ public function createDefinitions(): array foreach ($methodInject as $class => $path) { $def = $definitions[$class] ?? [ArrayDefinition::CLASS_NAME => $class]; - + $convertDefinitionToClosure = false; foreach ($path as $method => $val) { $constructor = "$method()"; if ($constructor === ArrayDefinition::CONSTRUCTOR) { $def[$constructor] ??= []; foreach ($val as $param => $value) { - $def[$constructor][$param] = $this->parameterValueOrReference($value); + if ($value instanceof ServiceCollectorReference) { + $convertDefinitionToClosure = true; + } + $def[$constructor][$param] = $this->parameterValueOrReference($value, $class); } } } + if ($convertDefinitionToClosure) { + $def = function (ContainerInterface $container) use ($def) { + $definition = ArrayDefinition::fromConfig($def); + $class = $definition->getClass(); + + $constructorArguments = array_map( + fn($param) => $param instanceof ServiceCollectorReference + ? $this->parameterValueOrReference($param, $class, $container) + : $param + , $definition->getConstructorArguments() + ); + + $definition = $definition->merge( + ArrayDefinition::fromPreparedData( + $class, + $constructorArguments, + $definition->getMethodsAndProperties() + )); + + return $definition->resolve($container); + }; + } $definitions[$class] = $def; } @@ -128,7 +159,7 @@ public function createDefinitions(): array $reflectionProperty = new ReflectionProperty($class, $property); if ($reflectionProperty->isPublic() && !$reflectionProperty->isReadOnly()) { // Yii DI natively supports property injection only for public and writable properties - $def["\$$property"] = $this->parameterValueOrReference($value); + $def["\$$property"] = $this->parameterValueOrReference($value, $class); } else { // add tag to service classes that have non-public or read-only properties // for injecting them manually after container creation @@ -153,7 +184,7 @@ public function createDefinitions(): array if ($definitions[$service]) { $def = is_string($definitions[$service]) ? [ArrayDefinition::CLASS_NAME => $definitions[$service]] : $definitions[$service]; foreach ($methods as $method) { - $params = array_map(fn ($value) => $this->parameterValueOrReference($value), $this->parametersForMethod($service, $method)); + $params = array_map(fn($value) => $this->parameterValueOrReference($value, $service), $this->parametersForMethod($service, $method)); $def["$method()"] = $params; } $definitions[$service] = $def; @@ -165,10 +196,35 @@ public function createDefinitions(): array /** * @throws InvalidConfigException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - private function parameterValueOrReference(mixed $value): mixed + private function parameterValueOrReference(mixed $value, string $service, ?ContainerInterface $container = null): mixed { - return $value instanceof ContainerReference ? Reference::to($value->name) : $value; + if ($value instanceof ContainerReference) { + return Reference::to($value->name); + } + + if ($value instanceof ServiceCollectorReference && ($value->collectionType === arrayType() || !is_null($container))) { + if (is_null($container)) { + return $value; + } + $values = []; + foreach ($this->containerDefinition->getServiceDefinitions() as $serviceDefinition) { + if ($serviceDefinition->isAbstract() || $serviceDefinition->getType()->getName() === $service) { + continue; + } + + if (is_a($serviceDefinition->getType()->getName(), $value->valueType->getName(), true)) { + $values[] = $value->collectionType !== arrayType() + ? $container->get($serviceDefinition->getType()->getName()) + : Reference::to($serviceDefinition->getType()->getName()); + } + } + return $value->listOf->toCollection($values); + } + + return $value; } } From f0ea68dceaed7bb3f3aab6912a20205770d3c6fc Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Tue, 24 Jun 2025 20:33:31 +0300 Subject: [PATCH 14/16] Started YiiDiContainerFactoryState refactoring and fixing psalm warnings --- .../YiiDiContainerFactoryState.php | 166 +++++++++--------- 1 file changed, 85 insertions(+), 81 deletions(-) diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index e6718847..ed8a404b 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -7,7 +7,6 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; -use ReflectionException; use ReflectionProperty; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; @@ -87,89 +86,25 @@ public function getReadOnlyPropertyInjectsForService(string $service): array * @throws ContainerExceptionInterface * @throws InvalidConfigException * @throws NotFoundExceptionInterface - * @throws ReflectionException */ public function createDefinitions(): array { $definitions = array_map(fn($concrete): string => $concrete, $this->aliases); foreach ($this->serviceDelegate as $service => [$delegate, $method]) { - $definitions[$service] = static function (AutowireableInvoker $invoker) use ($delegate, $method) { - $factory = $invoker->make($delegate); - return $invoker->invoke($factory->$method(...)); - }; + $definitions[$service] = static fn (AutowireableInvoker $invoker): mixed => $invoker->invoke($invoker->make($delegate)->$method(...)); } foreach ($this->namedServices as $name => $service) { $definitions[$name] = $service; } - $methodInject = $this->getMethodInject(); - - foreach ($methodInject as $class => $path) { - $def = $definitions[$class] ?? [ArrayDefinition::CLASS_NAME => $class]; - $convertDefinitionToClosure = false; - foreach ($path as $method => $val) { - $constructor = "$method()"; - if ($constructor === ArrayDefinition::CONSTRUCTOR) { - $def[$constructor] ??= []; - foreach ($val as $param => $value) { - if ($value instanceof ServiceCollectorReference) { - $convertDefinitionToClosure = true; - } - $def[$constructor][$param] = $this->parameterValueOrReference($value, $class); - } - } - } - if ($convertDefinitionToClosure) { - $def = function (ContainerInterface $container) use ($def) { - $definition = ArrayDefinition::fromConfig($def); - $class = $definition->getClass(); - - $constructorArguments = array_map( - fn($param) => $param instanceof ServiceCollectorReference - ? $this->parameterValueOrReference($param, $class, $container) - : $param - , $definition->getConstructorArguments() - ); - - $definition = $definition->merge( - ArrayDefinition::fromPreparedData( - $class, - $constructorArguments, - $definition->getMethodsAndProperties() - )); - - return $definition->resolve($container); - }; - } - $definitions[$class] = $def; + foreach ($this->getMethodInject() as $class => $methods) { + $definitions[$class] = $this->createMethodInjectConfig($class, $methods); } - $propertiesInject = $this->getPropertyInject(); - - foreach ($propertiesInject as $class => $path) { - $def = $definitions[$class] ?? [ArrayDefinition::CLASS_NAME => $class]; - if (is_string($def)) { - $def = [ - ArrayDefinition::CLASS_NAME => $class, - ]; - } - foreach ($path as $property => $value) { - $reflectionProperty = new ReflectionProperty($class, $property); - if ($reflectionProperty->isPublic() && !$reflectionProperty->isReadOnly()) { - // Yii DI natively supports property injection only for public and writable properties - $def["\$$property"] = $this->parameterValueOrReference($value, $class); - } else { - // add tag to service classes that have non-public or read-only properties - // for injecting them manually after container creation - $def['tags'] ??= []; - $def['tags'][] = self::TAG_INJECT_READ_ONLY_PROPERTIES; - $this->readOnlyPropertyInject[$class] ??= []; - $this->readOnlyPropertyInject[$class][] = [$reflectionProperty, $value]; - } - } - $definitions[$class] = $def; + foreach ($this->getPropertyInject() as $class => $methods) { + $definitions[$class] = $this->createPropertyInjectConfig($class, $methods); } foreach ($this->instances as $key => $value) { @@ -180,14 +115,14 @@ public function createDefinitions(): array $definitions[$concrete] = $definitions[$concrete] ?? $concrete; } - foreach ($this->getServicePrepares() as $service => $methods) { - if ($definitions[$service]) { - $def = is_string($definitions[$service]) ? [ArrayDefinition::CLASS_NAME => $definitions[$service]] : $definitions[$service]; + foreach ($this->getServicePrepares() as $class => $methods) { + if ($definitions[$class]) { + $config = is_string($definitions[$class]) ? [ArrayDefinition::CLASS_NAME => $definitions[$class]] : $definitions[$class]; foreach ($methods as $method) { - $params = array_map(fn($value) => $this->parameterValueOrReference($value, $service), $this->parametersForMethod($service, $method)); - $def["$method()"] = $params; + $params = array_map(fn($value) => $this->parameterValueOrReference($value, $class), $this->parametersForMethod($class, $method)); + $config["$method()"] = $params; } - $definitions[$service] = $def; + $definitions[$class] = $config; } } @@ -199,7 +134,7 @@ public function createDefinitions(): array * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - private function parameterValueOrReference(mixed $value, string $service, ?ContainerInterface $container = null): mixed + private function parameterValueOrReference(mixed $value, string $class, ?ContainerInterface $container = null): mixed { if ($value instanceof ContainerReference) { return Reference::to($value->name); @@ -209,22 +144,91 @@ private function parameterValueOrReference(mixed $value, string $service, ?Conta if (is_null($container)) { return $value; } + $values = []; + foreach ($this->containerDefinition->getServiceDefinitions() as $serviceDefinition) { - if ($serviceDefinition->isAbstract() || $serviceDefinition->getType()->getName() === $service) { + $service = $serviceDefinition->getType()->getName(); + + if ($serviceDefinition->isAbstract() || $service === $class) { continue; } - if (is_a($serviceDefinition->getType()->getName(), $value->valueType->getName(), true)) { + if (is_a($service, $value->valueType->getName(), true)) { $values[] = $value->collectionType !== arrayType() - ? $container->get($serviceDefinition->getType()->getName()) - : Reference::to($serviceDefinition->getType()->getName()); + ? $container->get($service) + : Reference::to($service); } } + return $value->listOf->toCollection($values); } return $value; } + private function convertDefinitionConfigToClosure(array $config): \Closure + { + return function (ContainerInterface $container) use ($config) { + $definition = ArrayDefinition::fromConfig($config); + $class = $definition->getClass(); + + $constructorArguments = array_map( + fn($value) => $value instanceof ServiceCollectorReference + ? $this->parameterValueOrReference($value, $class, $container) + : $value + , $definition->getConstructorArguments() + ); + + $definition = $definition->merge( + ArrayDefinition::fromPreparedData( + $class, + $constructorArguments, + $definition->getMethodsAndProperties() + )); + + return $definition->resolve($container); + }; + } + + private function createMethodInjectConfig(string $class, array $methods): array | \Closure + { + $config = [ArrayDefinition::CLASS_NAME => $class]; + $convertDefinitionToClosure = false; + foreach ($methods as $method => $val) { + $constructor = "$method()"; + if ($constructor === ArrayDefinition::CONSTRUCTOR) { + $config[$constructor] ??= []; + foreach ($val as $param => $value) { + if ($value instanceof ServiceCollectorReference) { + $convertDefinitionToClosure = true; + } + $config[$constructor][$param] = $this->parameterValueOrReference($value, $class); + } + } + } + return $convertDefinitionToClosure ? $this->convertDefinitionConfigToClosure($config) : $config; + } + + private function createPropertyInjectConfig(string $class, array $methods) + { + $config = [ArrayDefinition::CLASS_NAME => $class]; + + foreach ($methods as $property => $value) { + $reflectionProperty = new ReflectionProperty($class, $property); + if ($reflectionProperty->isPublic() && !$reflectionProperty->isReadOnly()) { + // Yii DI natively supports property injection only for public and writable properties + $config["\$$property"] = $this->parameterValueOrReference($value, $class); + } else { + // add tag to service classes that have non-public or read-only properties + // for injecting them manually after container creation + $config['tags'] ??= []; + $config['tags'][] = self::TAG_INJECT_READ_ONLY_PROPERTIES; + $this->readOnlyPropertyInject[$class] ??= []; + $this->readOnlyPropertyInject[$class][] = [$reflectionProperty, $value]; + } + } + return $config; + } + } From 27e99f8003e5f69b3724eadae794dfa65f20227a Mon Sep 17 00:00:00 2001 From: Dmitry Zarva Date: Thu, 17 Jul 2025 13:19:57 +0300 Subject: [PATCH 15/16] add declare strict_types --- src/ContainerFactory/YiiDiContainerFactory.php | 2 +- src/ContainerFactory/YiiDiContainerFactoryState.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContainerFactory/YiiDiContainerFactory.php b/src/ContainerFactory/YiiDiContainerFactory.php index 2d0e7f3e..ea5a1c0d 100644 --- a/src/ContainerFactory/YiiDiContainerFactory.php +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -1,4 +1,4 @@ - Date: Sat, 2 Aug 2025 12:55:41 +0300 Subject: [PATCH 16/16] - fix codding style warnings --- .../YiiDiContainerFactory.php | 57 +++++++------------ .../YiiDiContainerFactoryState.php | 53 +++++++---------- .../YiiDiContainerFactoryTest.php | 10 +--- 3 files changed, 42 insertions(+), 78 deletions(-) diff --git a/src/ContainerFactory/YiiDiContainerFactory.php b/src/ContainerFactory/YiiDiContainerFactory.php index ea5a1c0d..985ec43a 100644 --- a/src/ContainerFactory/YiiDiContainerFactory.php +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -27,33 +27,29 @@ use function Cspray\Typiphy\objectType; // @codeCoverageIgnoreStart +// phpcs:disable if (!class_exists(Container::class)) { throw new RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/di 1.4+!"); } -// @codeCoverageIgnoreEnd -// @codeCoverageIgnoreStart if (!class_exists(Injector::class)) { throw new RuntimeException("To enable the YiiDiContainerFactory please install yiisoft/injector 1.2+!"); } - +// phpcs:enable // @codeCoverageIgnoreEnd -final class YiiDiContainerFactory extends AbstractContainerFactory implements ContainerFactory -{ - protected function getBackingContainerType(): ObjectType - { +final class YiiDiContainerFactory extends AbstractContainerFactory implements ContainerFactory { + + protected function getBackingContainerType(): ObjectType { return objectType(Container::class); } - protected function getContainerFactoryState(ContainerDefinition $containerDefinition): ContainerFactoryState - { + protected function getContainerFactoryState(ContainerDefinition $containerDefinition): ContainerFactoryState { return new YiiDiContainerFactoryState($containerDefinition); } - protected function handleServiceDefinition(ContainerFactoryState $state, ServiceDefinition $definition): void - { + protected function handleServiceDefinition(ContainerFactoryState $state, ServiceDefinition $definition): void { assert($state instanceof YiiDiContainerFactoryState); if ($definition->isAbstract()) { $state->addAbstractService($definition->getType()->getName()); @@ -66,8 +62,7 @@ protected function handleServiceDefinition(ContainerFactoryState $state, Service } } - protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefinitionResolution $resolution): void - { + protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefinitionResolution $resolution): void { assert($state instanceof YiiDiContainerFactoryState); $definition = $resolution->getAliasDefinition(); if ($definition !== null) { @@ -75,8 +70,7 @@ protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefi } } - protected function handleServiceDelegateDefinition(ContainerFactoryState $state, ServiceDelegateDefinition $definition): void - { + protected function handleServiceDelegateDefinition(ContainerFactoryState $state, ServiceDelegateDefinition $definition): void { assert($state instanceof YiiDiContainerFactoryState); $state->addServiceDelegate( $definition->getServiceType()->getName(), @@ -85,8 +79,7 @@ protected function handleServiceDelegateDefinition(ContainerFactoryState $state, ); } - protected function handleServicePrepareDefinition(ContainerFactoryState $state, ServicePrepareDefinition $definition): void - { + protected function handleServicePrepareDefinition(ContainerFactoryState $state, ServicePrepareDefinition $definition): void { assert($state instanceof YiiDiContainerFactoryState); $state->addServicePrepare($definition->getService()->getName(), $definition->getMethod()); } @@ -94,8 +87,7 @@ protected function handleServicePrepareDefinition(ContainerFactoryState $state, /** * @throws ParameterStoreNotFound */ - protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void - { + protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void { assert($state instanceof YiiDiContainerFactoryState); if ($definition->getTargetIdentifier()->isMethodParameter()) { $state->addMethodInject( @@ -113,8 +105,7 @@ protected function handleInjectDefinition(ContainerFactoryState $state, InjectDe } } - protected function handleConfigurationDefinition(ContainerFactoryState $state, ConfigurationDefinition $definition): void - { + protected function handleConfigurationDefinition(ContainerFactoryState $state, ConfigurationDefinition $definition): void { assert($state instanceof YiiDiContainerFactoryState); $state->addConcreteService($definition->getClass()->getName()); $name = $definition->getName(); @@ -123,8 +114,7 @@ protected function handleConfigurationDefinition(ContainerFactoryState $state, C } } - protected function createAnnotatedContainer(ContainerFactoryState $state, ActiveProfiles $activeProfiles): AnnotatedContainer - { + protected function createAnnotatedContainer(ContainerFactoryState $state, ActiveProfiles $activeProfiles): AnnotatedContainer { assert($state instanceof YiiDiContainerFactoryState); $state->addInstance(ActiveProfiles::class, $activeProfiles); @@ -133,8 +123,7 @@ protected function createAnnotatedContainer(ContainerFactoryState $state, Active private readonly Container $container; private readonly Injector $injector; - public function __construct(YiiDiContainerFactoryState $state) - { + public function __construct(YiiDiContainerFactoryState $state) { $state->addInstance(AutowireableFactory::class, $this); $state->addInstance(AutowireableInvoker::class, $this); @@ -161,36 +150,30 @@ public function __construct(YiiDiContainerFactoryState $state) } } - public function getBackingContainer(): object - { + public function getBackingContainer(): object { return $this->container; } - public function make(string $classType, ?AutowireableParameterSet $parameters = null): object - { + public function make(string $classType, ?AutowireableParameterSet $parameters = null): object { return $this->injector->make($classType, $this->convertAutowireableParameterSet($parameters)); } - public function invoke(callable $callable, ?AutowireableParameterSet $parameters = null): mixed - { + public function invoke(callable $callable, ?AutowireableParameterSet $parameters = null): mixed { return $this->injector->invoke($callable, $this->convertAutowireableParameterSet($parameters)); } - public function get(string $id) - { + public function get(string $id) { if (!$this->has($id)) { throw ServiceNotFound::fromServiceNotInContainer($id); } return $this->container->get($id); } - public function has(string $id): bool - { + public function has(string $id): bool { return $this->container->has($id); } - private function convertAutowireableParameterSet(?AutowireableParameterSet $parameters = null): array - { + private function convertAutowireableParameterSet(?AutowireableParameterSet $parameters = null): array { $params = []; if (!is_null($parameters)) { /** @var AutowireableParameter $parameter */ diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php index a571eb49..3421f40e 100644 --- a/src/ContainerFactory/YiiDiContainerFactoryState.php +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -15,8 +15,7 @@ use function Cspray\Typiphy\arrayType; use function is_string; -final class YiiDiContainerFactoryState implements ContainerFactoryState -{ +final class YiiDiContainerFactoryState implements ContainerFactoryState { const TAG_INJECT_READ_ONLY_PROPERTIES = '__inject_read_only_properties'; use HasMethodInjectState; @@ -38,47 +37,38 @@ final class YiiDiContainerFactoryState implements ContainerFactoryState private array $aliases = []; private array $instances = []; - public function __construct(private readonly ContainerDefinition $containerDefinition) - { + public function __construct(private readonly ContainerDefinition $containerDefinition) { } - public function addConcreteService(string $name): void - { + public function addConcreteService(string $name): void { $this->concreteServices[$name] = $name; } - public function addAbstractService(string $name): void - { + public function addAbstractService(string $name): void { $this->abstractServices[$name] = $name; } - public function addNamedService(string $name, string $service): void - { + public function addNamedService(string $name, string $service): void { $this->namedServices[$name] = $service; } - public function addAlias(string $abstract, string $concrete): void - { + public function addAlias(string $abstract, string $concrete): void { $this->aliases[$abstract] = $concrete; } - public function getAliases(): array - { + public function getAliases(): array { return $this->aliases; } - public function addInstance(string $name, object $instance): void - { + public function addInstance(string $name, object $instance): void { $this->instances[$name] = $instance; } - public function addServiceDelegate(string $service, string $delegate, string $delegateMethod): void - { + public function addServiceDelegate(string $service, string $delegate, string $delegateMethod): void { $this->serviceDelegate[$service] = [$delegate, $delegateMethod]; } - public function getReadOnlyPropertyInjectsForService(string $service): array - { + public function getReadOnlyPropertyInjectsForService(string $service): array { return $this->readOnlyPropertyInject[$service] ?? []; } @@ -87,8 +77,7 @@ public function getReadOnlyPropertyInjectsForService(string $service): array * @throws InvalidConfigException * @throws NotFoundExceptionInterface */ - public function createDefinitions(): array - { + public function createDefinitions(): array { $definitions = array_map(fn($concrete): string => $concrete, $this->aliases); foreach ($this->serviceDelegate as $service => [$delegate, $method]) { @@ -134,8 +123,7 @@ public function createDefinitions(): array * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - private function parameterValueOrReference(mixed $value, string $class, ?ContainerInterface $container = null): mixed - { + private function parameterValueOrReference(mixed $value, string $class, ?ContainerInterface $container = null): mixed { if ($value instanceof ContainerReference) { return Reference::to($value->name); } @@ -167,8 +155,7 @@ private function parameterValueOrReference(mixed $value, string $class, ?Contain return $value; } - private function convertDefinitionConfigToClosure(array $config): \Closure - { + private function convertDefinitionConfigToClosure(array $config): \Closure { return function (ContainerInterface $container) use ($config) { $definition = ArrayDefinition::fromConfig($config); $class = $definition->getClass(); @@ -176,8 +163,8 @@ private function convertDefinitionConfigToClosure(array $config): \Closure $constructorArguments = array_map( fn($value) => $value instanceof ServiceCollectorReference ? $this->parameterValueOrReference($value, $class, $container) - : $value - , $definition->getConstructorArguments() + : $value, + $definition->getConstructorArguments() ); $definition = $definition->merge( @@ -185,14 +172,14 @@ private function convertDefinitionConfigToClosure(array $config): \Closure $class, $constructorArguments, $definition->getMethodsAndProperties() - )); + ) + ); return $definition->resolve($container); }; } - private function createMethodInjectConfig(string $class, array $methods): array | \Closure - { + private function createMethodInjectConfig(string $class, array $methods): array | \Closure { $config = [ArrayDefinition::CLASS_NAME => $class]; $convertDefinitionToClosure = false; foreach ($methods as $method => $val) { @@ -210,8 +197,7 @@ private function createMethodInjectConfig(string $class, array $methods): array return $convertDefinitionToClosure ? $this->convertDefinitionConfigToClosure($config) : $config; } - private function createPropertyInjectConfig(string $class, array $methods) - { + private function createPropertyInjectConfig(string $class, array $methods) { $config = [ArrayDefinition::CLASS_NAME => $class]; foreach ($methods as $property => $value) { @@ -230,5 +216,4 @@ private function createPropertyInjectConfig(string $class, array $methods) } return $config; } - } diff --git a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php index 45573d5d..285366b6 100644 --- a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php +++ b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php @@ -10,17 +10,13 @@ use Yiisoft\Di\Container; use function Cspray\Typiphy\objectType; -class YiiDiContainerFactoryTest extends ContainerFactoryTestCase -{ +class YiiDiContainerFactoryTest extends ContainerFactoryTestCase { - protected function getContainerFactory(ActiveProfiles $activeProfiles): ContainerFactory - { + protected function getContainerFactory(ActiveProfiles $activeProfiles): ContainerFactory { return new YiiDiContainerFactory(); } - protected function getBackingContainerInstanceOf(): ObjectType - { + protected function getBackingContainerInstanceOf(): ObjectType { return objectType(Container::class); } - }