From 673b9a81c1b99d4a1dfca15329b0282b57b9483d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 1 Dec 2024 21:06:01 +0100 Subject: [PATCH] Resolver::completeStatement() moved to Statement & Reference --- src/DI/ContainerBuilder.php | 4 +- src/DI/Definitions/AccessorDefinition.php | 2 +- src/DI/Definitions/Expression.php | 3 + src/DI/Definitions/LocatorDefinition.php | 4 +- src/DI/Definitions/Reference.php | 26 +++ src/DI/Definitions/ServiceDefinition.php | 10 +- src/DI/Definitions/Statement.php | 199 +++++++++++++++- src/DI/Resolver.php | 264 +++------------------- 8 files changed, 270 insertions(+), 242 deletions(-) diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index f2844d508..089ef301a 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -394,8 +394,8 @@ public static function literal(string $code, ?array $args = null): Nette\PhpGene public function formatPhp(string $statement, array $args): string { array_walk_recursive($args, function (&$val): void { - if ($val instanceof Nette\DI\Definitions\Statement) { - $val = (new Resolver($this))->completeStatement($val); + if ($val instanceof Nette\DI\Definitions\Expression) { + $val->complete(new Resolver($this)); } elseif ($val instanceof Definition) { $val = new Definitions\Reference($val->getName()); diff --git a/src/DI/Definitions/AccessorDefinition.php b/src/DI/Definitions/AccessorDefinition.php index 3c4b1d3fd..c416ca9a7 100644 --- a/src/DI/Definitions/AccessorDefinition.php +++ b/src/DI/Definitions/AccessorDefinition.php @@ -103,7 +103,7 @@ public function complete(Nette\DI\Resolver $resolver): void $this->setReference(Type::fromReflection($method)->getSingleName()); } - $this->reference = $resolver->normalizeReference($this->reference); + $this->reference->complete($resolver); } diff --git a/src/DI/Definitions/Expression.php b/src/DI/Definitions/Expression.php index df8eb0ee5..da7b4a24f 100644 --- a/src/DI/Definitions/Expression.php +++ b/src/DI/Definitions/Expression.php @@ -17,5 +17,8 @@ abstract class Expression abstract public function resolveType(Nette\DI\Resolver $resolver): ?string; + abstract public function complete(Nette\DI\Resolver $resolver): void; + + abstract public function generateCode(Nette\DI\PhpGenerator $generator): string; } diff --git a/src/DI/Definitions/LocatorDefinition.php b/src/DI/Definitions/LocatorDefinition.php index 5693302a4..3cdfb1615 100644 --- a/src/DI/Definitions/LocatorDefinition.php +++ b/src/DI/Definitions/LocatorDefinition.php @@ -121,8 +121,8 @@ public function complete(Nette\DI\Resolver $resolver): void } } - foreach ($this->references as $name => $ref) { - $this->references[$name] = $resolver->normalizeReference($ref); + foreach ($this->references as $ref) { + $ref->complete($resolver); } } diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index c28f7bb59..ed6db4305 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -84,6 +84,32 @@ public function resolveType(DI\Resolver $resolver): ?string } + /** + * Normalizes reference to 'self' or named reference (or leaves it typed if it is not possible during resolving) and checks existence of service. + */ + public function complete(DI\Resolver $resolver): void + { + if ($this->isSelf()) { + return; + + } elseif ($this->isType()) { + try { + $this->value = $resolver->getByType($this->value)->value; + } catch (DI\NotAllowedDuringResolvingException) { + } + return; + } + + if (!$resolver->getContainerBuilder()->hasDefinition($this->value)) { + throw new DI\ServiceCreationException(sprintf("Reference to missing service '%s'.", $this->value)); + } + + if ($this->value === $resolver->getCurrentService()?->getName()) { + $this->value = self::Self; + } + } + + public function generateCode(DI\PhpGenerator $generator): string { return match (true) { diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index 34d8fc2da..0bf8d3b9d 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -161,14 +161,14 @@ public function complete(Nette\DI\Resolver $resolver): void { $entity = $this->creator->getEntity(); if ($entity instanceof Reference && !$this->creator->arguments && !$this->setup) { - $ref = $resolver->normalizeReference($entity); - $this->setCreator([new Reference(Nette\DI\ContainerBuilder::ThisContainer), 'getService'], [$ref->getValue()]); + $entity->complete($resolver); + $this->setCreator([new Reference(Nette\DI\ContainerBuilder::ThisContainer), 'getService'], [$entity->getValue()]); } - $this->creator = $resolver->completeStatement($this->creator); + $this->creator->complete($resolver); - foreach ($this->setup as &$setup) { - $setup = $resolver->withCurrentServiceAvailable()->completeStatement($setup); + foreach ($this->setup as $setup) { + $setup->complete($resolver->withCurrentServiceAvailable()); } } diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index 31212b7ba..ba65b1ee2 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -15,6 +15,7 @@ use Nette\DI\ServiceCreationException; use Nette\PhpGenerator as Php; use Nette\Utils\Callback; +use Nette\Utils\Validators; /** @@ -71,7 +72,7 @@ public function getEntity(): string|array|Definition|Reference|null public function resolveType(Resolver $resolver): ?string { - $entity = $resolver->normalizeEntity($this); + $entity = $this->normalizeEntity($resolver); if ($this->arguments === Resolver::getFirstClassCallable()) { return \Closure::class; @@ -137,6 +138,202 @@ interface_exists($entity) } + public function complete(Resolver $resolver): void + { + $entity = $this->normalizeEntity($resolver); + $this->convertReferences($resolver); + $arguments = $this->arguments; + + switch (true) { + case $this->arguments === Resolver::getFirstClassCallable(): + if (!is_array($entity) || !Php\Helpers::isIdentifier($entity[1])) { + throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); + } + if ($entity[0] instanceof self) { + $entity[0]->complete($resolver); + } + break; + + case is_string($entity) && str_contains($entity, '?'): // PHP literal + break; + + case $entity === 'not': + if (count($arguments) !== 1) { + throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); + } + + $this->entity = ['', '!']; + break; + + case $entity === 'bool': + case $entity === 'int': + case $entity === 'float': + case $entity === 'string': + if (count($arguments) !== 1) { + throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); + } + + $arguments = [$arguments[0], $entity]; + $this->entity = [DI\Helpers::class, 'convertType']; + break; + + case is_string($entity): // create class + if (!class_exists($entity)) { + throw new ServiceCreationException(sprintf("Class '%s' not found.", $entity)); + } elseif ((new \ReflectionClass($entity))->isAbstract()) { + throw new ServiceCreationException(sprintf('Class %s is abstract.', $entity)); + } elseif (($rm = (new \ReflectionClass($entity))->getConstructor()) !== null && !$rm->isPublic()) { + throw new ServiceCreationException(sprintf('Class %s has %s constructor.', $entity, $rm->isProtected() ? 'protected' : 'private')); + } elseif ($constructor = (new \ReflectionClass($entity))->getConstructor()) { + $arguments = $resolver->autowireServices($constructor, $arguments); + $resolver->addDependency($constructor); + } elseif ($arguments) { + throw new ServiceCreationException(sprintf( + 'Unable to pass arguments, class %s has no constructor.', + $entity, + )); + } + + break; + + case $entity instanceof Reference: + if ($arguments) { + $e = $resolver->completeException(new ServiceCreationException(sprintf('Parameters were passed to reference @%s, although references cannot have any parameters.', $entity->getValue())), $resolver->getCurrentService()); + trigger_error($e->getMessage(), E_USER_DEPRECATED); + } + $this->entity = [new Reference(DI\ContainerBuilder::ThisContainer), DI\Container::getMethodName($entity->getValue())]; + break; + + case is_array($entity): + if (!preg_match('#^\$?(\\\\?' . Php\Helpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) { + throw new ServiceCreationException(sprintf( + "Expected function, method or property name, '%s' given.", + $entity[1], + )); + } + + switch (true) { + case $entity[0] === '': // function call + if (!function_exists($entity[1])) { + throw new ServiceCreationException(sprintf("Function %s doesn't exist.", $entity[1])); + } + + $rf = new \ReflectionFunction($entity[1]); + $arguments = $resolver->autowireServices($rf, $arguments); + $resolver->addDependency($rf); + break; + + case $entity[0] instanceof self: + $entity[0]->complete($resolver); + // break omitted + + case is_string($entity[0]): // static method call + case $entity[0] instanceof Reference: + if ($entity[1][0] === '$') { // property getter, setter or appender + Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Callback::toString($entity) . "'"); + if (!$arguments && str_ends_with($entity[1], '[]')) { + throw new ServiceCreationException(sprintf('Missing argument for %s.', $entity[1])); + } + } elseif ( + $type = ($entity[0] instanceof Expression ? $entity[0] : new self($entity[0]))->resolveType($resolver) + ) { + $rc = new \ReflectionClass($type); + if ($rc->hasMethod($entity[1])) { + $rm = $rc->getMethod($entity[1]); + if (!$rm->isPublic()) { + throw new ServiceCreationException(sprintf('%s::%s() is not callable.', $type, $entity[1])); + } + + $arguments = $resolver->autowireServices($rm, $arguments); + $resolver->addDependency($rm); + } + } + } + } + + try { + $this->arguments = $this->completeArguments($resolver, $arguments); + } catch (ServiceCreationException $e) { + if (!str_contains($e->getMessage(), ' (used in')) { + $e->setMessage($e->getMessage() . " (used in {$resolver->entityToString($entity)})"); + } + + throw $e; + } + } + + + public function completeArguments(Resolver $resolver, array $arguments): array + { + array_walk_recursive($arguments, function (&$val) use ($resolver): void { + if ($val instanceof self) { + if ($val->entity === 'typed' || $val->entity === 'tagged') { + $services = []; + $current = $resolver->getCurrentService()?->getName(); + foreach ($val->arguments as $argument) { + foreach ($val->entity === 'tagged' ? $resolver->getContainerBuilder()->findByTag($argument) : $resolver->getContainerBuilder()->findAutowired($argument) as $name => $foo) { + if ($name !== $current) { + $services[] = new Reference($name); + } + } + } + + $val = $this->completeArguments($resolver, $services); + } else { + $val->complete($resolver); + } + } elseif ($val instanceof Definition || $val instanceof Reference) { + $val = (new self($val))->normalizeEntity($resolver); + } + }); + return $arguments; + } + + + /** Returns literal, Class, Reference, [Class, member], [, globalFunc], [Reference, member], [Statement, member] */ + private function normalizeEntity(Resolver $resolver): string|array|Reference|null + { + if (is_array($this->entity)) { + $item = &$this->entity[0]; + } else { + $item = &$this->entity; + } + + if ($item instanceof Definition) { + if ($resolver->getContainerBuilder()->getDefinition($item->getName()) !== $item) { + throw new ServiceCreationException(sprintf("Service '%s' does not match the expected service.", $item->getName())); + + } + $item = new Reference($item->getName()); + } + + if ($item instanceof Reference) { + $item->complete($resolver); + } + + return $this->entity; + } + + + private function convertReferences(Resolver $resolver): void + { + array_walk_recursive($this->arguments, function (&$val) use ($resolver): void { + if (is_string($val) && strlen($val) > 1 && $val[0] === '@' && $val[1] !== '@') { + $pair = explode('::', substr($val, 1), 2); + if (!isset($pair[1])) { // @service + $val = new Reference($pair[0]); + } elseif (preg_match('#^[A-Z][a-zA-Z0-9_]*$#D', $pair[1])) { // @service::CONSTANT + $val = DI\ContainerBuilder::literal((new Reference($pair[0]))->resolveType($resolver) . '::' . $pair[1]); + } else { // @service::property + $val = new self([new Reference($pair[0]), '$' . $pair[1]]); + } + } elseif (is_string($val) && str_starts_with($val, '@@')) { // escaped text @@ + $val = substr($val, 1); + } + }); + } + + /** * Formats PHP code for class instantiating, function calling or property setting in PHP. */ diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index 8bf14a6f8..d39183856 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -11,14 +11,10 @@ use Nette; use Nette\DI\Definitions\Definition; -use Nette\DI\Definitions\Expression; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; -use Nette\PhpGenerator\Helpers as PhpHelpers; use Nette\Utils\Arrays; -use Nette\Utils\Callback; use Nette\Utils\Reflection; -use Nette\Utils\Validators; /** @@ -43,7 +39,7 @@ public function __construct(ContainerBuilder $builder) } - private function withCurrentService(Definition $definition): self + public function withCurrentService(Definition $definition): self { $dolly = clone $this; $dolly->currentService = in_array($definition, $this->builder->getDefinitions(), strict: true) @@ -110,215 +106,6 @@ public function completeDefinition(Definition $def): void } - public function completeStatement(Statement $statement): Statement - { - $entity = $this->normalizeEntity($statement); - $arguments = $this->convertReferences($statement->arguments); - $getter = fn(string $type, bool $single) => $single - ? $this->getByType($type) - : array_values(array_filter($this->builder->findAutowired($type), fn($obj) => $obj !== $this->currentService)); - - switch (true) { - case $statement->arguments === self::getFirstClassCallable(): - if (!is_array($entity) || !PhpHelpers::isIdentifier($entity[1])) { - throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); - } - if ($entity[0] instanceof Statement) { - $entity[0] = $this->completeStatement($entity[0]); - } - break; - - case is_string($entity) && str_contains($entity, '?'): // PHP literal - break; - - case $entity === 'not': - if (count($arguments) !== 1) { - throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); - } - - $entity = ['', '!']; - break; - - case $entity === 'bool': - case $entity === 'int': - case $entity === 'float': - case $entity === 'string': - if (count($arguments) !== 1) { - throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); - } - - $arguments = [$arguments[0], $entity]; - $entity = [Helpers::class, 'convertType']; - break; - - case is_string($entity): // create class - if (!class_exists($entity)) { - throw new ServiceCreationException(sprintf("Class '%s' not found.", $entity)); - } elseif ((new \ReflectionClass($entity))->isAbstract()) { - throw new ServiceCreationException(sprintf('Class %s is abstract.', $entity)); - } elseif (($rm = (new \ReflectionClass($entity))->getConstructor()) !== null && !$rm->isPublic()) { - throw new ServiceCreationException(sprintf('Class %s has %s constructor.', $entity, $rm->isProtected() ? 'protected' : 'private')); - } elseif ($constructor = (new \ReflectionClass($entity))->getConstructor()) { - $arguments = self::autowireArguments($constructor, $arguments, $getter); - $this->addDependency($constructor); - } elseif ($arguments) { - throw new ServiceCreationException(sprintf( - 'Unable to pass arguments, class %s has no constructor.', - $entity, - )); - } - - break; - - case $entity instanceof Reference: - if ($arguments) { - $e = $this->completeException(new ServiceCreationException(sprintf('Parameters were passed to reference @%s, although references cannot have any parameters.', $entity->getValue())), $this->currentService); - trigger_error($e->getMessage(), E_USER_DEPRECATED); - } - $entity = [new Reference(ContainerBuilder::ThisContainer), Container::getMethodName($entity->getValue())]; - break; - - case is_array($entity): - if (!preg_match('#^\$?(\\\\?' . PhpHelpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) { - throw new ServiceCreationException(sprintf( - "Expected function, method or property name, '%s' given.", - $entity[1], - )); - } - - switch (true) { - case $entity[0] === '': // function call - if (!function_exists($entity[1])) { - throw new ServiceCreationException(sprintf("Function %s doesn't exist.", $entity[1])); - } - - $rf = new \ReflectionFunction($entity[1]); - $arguments = self::autowireArguments($rf, $arguments, $getter); - $this->addDependency($rf); - break; - - case $entity[0] instanceof Statement: - $entity[0] = $this->completeStatement($entity[0]); - // break omitted - - case is_string($entity[0]): // static method call - case $entity[0] instanceof Reference: - if ($entity[1][0] === '$') { // property getter, setter or appender - Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Callback::toString($entity) . "'"); - if (!$arguments && str_ends_with($entity[1], '[]')) { - throw new ServiceCreationException(sprintf('Missing argument for %s.', $entity[1])); - } - } elseif ( - $type = ($entity[0] instanceof Expression ? $entity[0] : new Statement($entity[0]))->resolveType($this) - ) { - $rc = new \ReflectionClass($type); - if ($rc->hasMethod($entity[1])) { - $rm = $rc->getMethod($entity[1]); - if (!$rm->isPublic()) { - throw new ServiceCreationException(sprintf('%s::%s() is not callable.', $type, $entity[1])); - } - - $arguments = self::autowireArguments($rm, $arguments, $getter); - $this->addDependency($rm); - } - } - } - } - - try { - $arguments = $this->completeArguments($arguments); - } catch (ServiceCreationException $e) { - if (!str_contains($e->getMessage(), ' (used in')) { - $e->setMessage($e->getMessage() . " (used in {$this->entityToString($entity)})"); - } - - throw $e; - } - - return new Statement($entity, $arguments); - } - - - public function completeArguments(array $arguments): array - { - array_walk_recursive($arguments, function (&$val): void { - if ($val instanceof Statement) { - $entity = $val->getEntity(); - if ($entity === 'typed' || $entity === 'tagged') { - $services = []; - $current = $this->currentService?->getName(); - foreach ($val->arguments as $argument) { - foreach ($entity === 'tagged' ? $this->builder->findByTag($argument) : $this->builder->findAutowired($argument) as $name => $foo) { - if ($name !== $current) { - $services[] = new Reference($name); - } - } - } - - $val = $this->completeArguments($services); - } else { - $val = $this->completeStatement($val); - } - } elseif ($val instanceof Definition || $val instanceof Reference) { - $val = $this->normalizeEntity(new Statement($val)); - } - }); - return $arguments; - } - - - /** Returns literal, Class, Reference, [Class, member], [, globalFunc], [Reference, member], [Statement, member] */ - public function normalizeEntity(Statement $statement): string|array|Reference|null - { - $entity = $statement->getEntity(); - if (is_array($entity)) { - $item = &$entity[0]; - } else { - $item = &$entity; - } - - if ($item instanceof Definition) { - if ($this->builder->getDefinition($item->getName()) !== $item) { - throw new ServiceCreationException(sprintf("Service '%s' does not match the expected service.", $item->getName())); - - } - $item = new Reference($item->getName()); - } - - if ($item instanceof Reference) { - $item = $this->normalizeReference($item); - } - - return $entity; - } - - - /** - * Normalizes reference to 'self' or named reference (or leaves it typed if it is not possible during resolving) and checks existence of service. - */ - public function normalizeReference(Reference $ref): Reference - { - $service = $ref->getValue(); - if ($ref->isSelf()) { - return $ref; - } elseif ($ref->isName()) { - if (!$this->builder->hasDefinition($service)) { - throw new ServiceCreationException(sprintf("Reference to missing service '%s'.", $service)); - } - - return $this->currentService && $service === $this->currentService->getName() - ? new Reference(Reference::Self) - : $ref; - } - - try { - return $this->getByType($service); - } catch (NotAllowedDuringResolvingException) { - return new Reference($service); - } - } - - /** * Returns named reference to service resolved by type (or 'self' reference for local-autowiring). * @throws ServiceCreationException when multiple found @@ -385,7 +172,8 @@ public function completeException(\Throwable $e, Definition $def): ServiceCreati } - private function entityToString($entity): string + /** @internal */ + public function entityToString($entity): string { $referenceToText = fn(Reference $ref): string => $ref->isSelf() && $this->currentService ? '@' . $this->currentService->getName() @@ -412,23 +200,12 @@ private function entityToString($entity): string } - private function convertReferences(array $arguments): array + public function autowireServices(\ReflectionFunctionAbstract $method, array $arguments): array { - array_walk_recursive($arguments, function (&$val): void { - if (is_string($val) && strlen($val) > 1 && $val[0] === '@' && $val[1] !== '@') { - $pair = explode('::', substr($val, 1), 2); - if (!isset($pair[1])) { // @service - $val = new Reference($pair[0]); - } elseif (preg_match('#^[A-Z][a-zA-Z0-9_]*$#D', $pair[1])) { // @service::CONSTANT - $val = ContainerBuilder::literal((new Reference($pair[0]))->resolveType($this) . '::' . $pair[1]); - } else { // @service::property - $val = new Statement([new Reference($pair[0]), '$' . $pair[1]]); - } - } elseif (is_string($val) && str_starts_with($val, '@@')) { // escaped text @@ - $val = substr($val, 1); - } - }); - return $arguments; + $getter = fn(string $type, bool $single) => $single + ? $this->getByType($type) + : array_values(array_filter($this->builder->findAutowired($type), fn($obj) => $obj !== $this->currentService)); + return self::autowireArguments($method, $arguments, $getter); } @@ -604,4 +381,29 @@ public function resolveReference(Reference $ref): Definition ? $this->currentService : $this->builder->getDefinition($ref->getValue()); } + + + /** @deprecated */ + public function normalizeReference(Reference $ref): Reference + { + $ref->complete($this); + return $ref; + } + + + /** @deprecated */ + public function completeStatement(Statement $statement, bool $currentServiceAllowed = false): Statement + { + $resolver = $this->withCurrentService($this->currentService); + $resolver->currentServiceAllowed = $currentServiceAllowed; + $statement->complete($resolver); + return $statement; + } + + + /** @deprecated */ + public function completeArguments(array $arguments): array + { + return (new Statement(null, $arguments))->completeArguments($this, $arguments); + } }