diff --git a/composer.json b/composer.json index f09ed3fc..18f04c11 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,9 @@ "jetbrains/phpstorm-attributes": "^1.1", "php-di/php-di": "^7.0", "rdlowrey/auryn": "^1.4", - "roave/security-advisories": "dev-latest" + "roave/security-advisories": "dev-latest", + "yiisoft/di": "^1.4", + "yiisoft/injector": "^1.2" }, "bin": ["bin/annotated-container"], "autoload": { diff --git a/composer.lock b/composer.lock index db42bffd..8a44ae91 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": "53633a34c26de6bd972e49bd8d1aeeb4", + "content-hash": "1b44f0ae9b9e4a63d164bc1e13105029", "packages": [ { "name": "brick/varexporter", @@ -1767,6 +1767,280 @@ } ], "time": "2024-06-06T22:04:19+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" + }, + { + "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": [], @@ -1782,6 +2056,6 @@ "ext-libxml": "*", "composer-runtime-api": "^2" }, - "platform-dev": [], + "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..985ec43a --- /dev/null +++ b/src/ContainerFactory/YiiDiContainerFactory.php @@ -0,0 +1,195 @@ +isAbstract()) { + $state->addAbstractService($definition->getType()->getName()); + } else { + $state->addConcreteService($definition->getType()->getName()); + } + $alias = $definition->getName(); + if ($alias !== null) { + $state->addNamedService($alias, $definition->getType()->getName()); + } + } + + protected function handleAliasDefinition(ContainerFactoryState $state, AliasDefinitionResolution $resolution): void { + 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 { + assert($state instanceof YiiDiContainerFactoryState); + $state->addServiceDelegate( + $definition->getServiceType()->getName(), + $definition->getDelegateType()->getName(), + $definition->getDelegateMethod() + ); + } + + protected function handleServicePrepareDefinition(ContainerFactoryState $state, ServicePrepareDefinition $definition): void { + assert($state instanceof YiiDiContainerFactoryState); + $state->addServicePrepare($definition->getService()->getName(), $definition->getMethod()); + } + + /** + * @throws ParameterStoreNotFound + */ + protected function handleInjectDefinition(ContainerFactoryState $state, InjectDefinition $definition): void { + 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 { + 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 { + assert($state instanceof YiiDiContainerFactoryState); + + $state->addInstance(ActiveProfiles::class, $activeProfiles); + + return new class ($state) implements AnnotatedContainer { + private readonly Container $container; + private readonly Injector $injector; + + public function __construct(YiiDiContainerFactoryState $state) { + $state->addInstance(AutowireableFactory::class, $this); + $state->addInstance(AutowireableInvoker::class, $this); + + $config = ContainerConfig::create() + ->withDefinitions($state->createDefinitions()) + ->withStrictMode() + ->withValidate(false); + + $this->container = new Container($config); + $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 { + return $this->container; + } + + 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 { + return $this->injector->invoke($callable, $this->convertAutowireableParameterSet($parameters)); + } + + 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); + } + + private function convertAutowireableParameterSet(?AutowireableParameterSet $parameters = null): array { + $params = []; + if (!is_null($parameters)) { + /** @var AutowireableParameter $parameter */ + foreach ($parameters as $parameter) { + $name = $parameter->getName(); + 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; + } + } + return $params; + } + }; + } +} diff --git a/src/ContainerFactory/YiiDiContainerFactoryState.php b/src/ContainerFactory/YiiDiContainerFactoryState.php new file mode 100644 index 00000000..3421f40e --- /dev/null +++ b/src/ContainerFactory/YiiDiContainerFactoryState.php @@ -0,0 +1,219 @@ + */ + private array $namedServices = []; + + /** @var array> */ + private array $serviceDelegate = []; + + private array $readOnlyPropertyInject = []; + + /** @var array */ + private array $aliases = []; + private array $instances = []; + + public function __construct(private readonly ContainerDefinition $containerDefinition) { + } + + public function addConcreteService(string $name): void { + $this->concreteServices[$name] = $name; + } + + public function addAbstractService(string $name): void { + $this->abstractServices[$name] = $name; + } + + public function addNamedService(string $name, string $service): void { + $this->namedServices[$name] = $service; + } + + 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; + } + + public function addServiceDelegate(string $service, string $delegate, string $delegateMethod): void { + $this->serviceDelegate[$service] = [$delegate, $delegateMethod]; + } + + public function getReadOnlyPropertyInjectsForService(string $service): array { + return $this->readOnlyPropertyInject[$service] ?? []; + } + + /** + * @throws ContainerExceptionInterface + * @throws InvalidConfigException + * @throws NotFoundExceptionInterface + */ + public function createDefinitions(): array { + $definitions = array_map(fn($concrete): string => $concrete, $this->aliases); + + foreach ($this->serviceDelegate as $service => [$delegate, $method]) { + $definitions[$service] = static fn (AutowireableInvoker $invoker): mixed => $invoker->invoke($invoker->make($delegate)->$method(...)); + } + + foreach ($this->namedServices as $name => $service) { + $definitions[$name] = $service; + } + + foreach ($this->getMethodInject() as $class => $methods) { + $definitions[$class] = $this->createMethodInjectConfig($class, $methods); + } + + foreach ($this->getPropertyInject() as $class => $methods) { + $definitions[$class] = $this->createPropertyInjectConfig($class, $methods); + } + + foreach ($this->instances as $key => $value) { + $definitions[$key] = $definitions[$key] ?? $value; + } + + foreach ($this->concreteServices as $concrete) { + $definitions[$concrete] = $definitions[$concrete] ?? $concrete; + } + + 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, $class), $this->parametersForMethod($class, $method)); + $config["$method()"] = $params; + } + $definitions[$class] = $config; + } + } + + return $definitions; + } + + /** + * @throws InvalidConfigException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function parameterValueOrReference(mixed $value, string $class, ?ContainerInterface $container = null): mixed { + 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) { + $service = $serviceDefinition->getType()->getName(); + + if ($serviceDefinition->isAbstract() || $service === $class) { + continue; + } + + if (is_a($service, $value->valueType->getName(), true)) { + $values[] = $value->collectionType !== arrayType() + ? $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; + } +} diff --git a/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php new file mode 100644 index 00000000..285366b6 --- /dev/null +++ b/test/Unit/ContainerFactoryTests/YiiDiContainerFactoryTest.php @@ -0,0 +1,22 @@ +