diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 96d05c498..995988d49 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b908378f2..189614db5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,9 +22,9 @@ jobs: - run: composer install --no-progress --prefer-dist - run: vendor/bin/tester tests -s -C - if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: output + name: output-${{ matrix.php }} path: tests/**/output diff --git a/composer.json b/composer.json index c3bec0059..f0a143343 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,22 @@ "php": "8.1 - 8.4", "ext-tokenizer": "*", "ext-ctype": "*", - "nette/neon": "^3.3", + "nette/neon": "^3.4", "nette/php-generator": "^4.1.6", "nette/robot-loader": "^4.0", - "nette/schema": "^1.2.5", + "nette/schema": "^1.3", "nette/utils": "^4.0" }, "require-dev": { "nette/tester": "^2.5.2", "tracy/tracy": "^2.9", - "phpstan/phpstan": "^1.0" + "phpstan/phpstan-nette": "^2.0@stable" }, "autoload": { - "classmap": ["src/"] + "classmap": ["src/"], + "psr-4": { + "Nette\\": "src" + } }, "minimum-stability": "dev", "scripts": { @@ -39,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } diff --git a/src/Bridges/DITracy/ContainerPanel.php b/src/Bridges/DITracy/ContainerPanel.php index 82051f931..57f4431e9 100644 --- a/src/Bridges/DITracy/ContainerPanel.php +++ b/src/Bridges/DITracy/ContainerPanel.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\Container; use Tracy; +use const SORT_NATURAL; /** @@ -40,7 +41,7 @@ public function getTab(): string { return Nette\Utils\Helpers::capture(function () { $elapsedTime = $this->elapsedTime; - require __DIR__ . '/templates/ContainerPanel.tab.phtml'; + require __DIR__ . '/dist/tab.phtml'; }); } @@ -76,7 +77,7 @@ public function getPanel(): string $parameters = $rc->getMethod('getStaticParameters')->getDeclaringClass()->getName() === Container::class ? null : $container->getParameters(); - require __DIR__ . '/templates/ContainerPanel.panel.phtml'; + require __DIR__ . '/dist/panel.phtml'; }); } } diff --git a/src/Bridges/DITracy/dist/panel.phtml b/src/Bridges/DITracy/dist/panel.phtml new file mode 100644 index 000000000..8525b5794 --- /dev/null +++ b/src/Bridges/DITracy/dist/panel.phtml @@ -0,0 +1,87 @@ + + + + +

Nette DI Container

+ +
+
+

Source: +

+ + + + + + + + + + + + $type): ?> + + + + + + +
NameAutowiredServiceTags
+ + + + + + + + + + + true, Dumper::LIVE => true, Dumper::DEPTH => 5]) ?> + + + + + + + + =  true]) ?> + + true]) ?> + +
+ +

Parameters

+ +
+ disabled via 'di › export › parameters' + + +
+
+
diff --git a/src/Bridges/DITracy/dist/tab.phtml b/src/Bridges/DITracy/dist/tab.phtml new file mode 100644 index 000000000..f9cb72857 --- /dev/null +++ b/src/Bridges/DITracy/dist/tab.phtml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Bridges/DITracy/panel.latte b/src/Bridges/DITracy/panel.latte new file mode 100644 index 000000000..87d51026e --- /dev/null +++ b/src/Bridges/DITracy/panel.latte @@ -0,0 +1,84 @@ +{use Tracy\Dumper} + + + +

Nette DI Container

+ +
+
+

Source: {Tracy\Helpers::editorLink($file)}

+ + + + + + + + + + + + {foreach $services as $name => $type} + {do $name = (string) $name} + {do $autowired = in_array($name, array_merge($wiring[$type][0] ?? [], $wiring[$type][1] ?? []), strict: true)} + + + + + + + {/foreach} + +
NameAutowiredServiceTags
+ {if is_numeric($name)}{$name}{else}{$name}{/if} + + {$autowired ? yes : (isset($wiring[$type]) ? no : '?')} + + {if isset($instances[$name]) && !$instances[$name] instanceof Nette\DI\Container} + {Dumper::toHtml($instances[$name], [Dumper::COLLAPSE => true, Dumper::LIVE => true, Dumper::DEPTH => 5])} + {elseif isset($instances[$name])} + {get_class($instances[$name])} + {elseif is_string($type)} + {$type} + {/if} + + {if !isset($tags[$name])} + {elseif count($tags[$name]) === 1} + {key($tags[$name])} = {Dumper::toHtml(current($tags[$name]), [Dumper::COLLAPSE => true])} + {else} + {Dumper::toHtml($tags[$name], [Dumper::COLLAPSE => true])} + {/if} +
+ +

Parameters

+ +
+ {if $parameters === null} + disabled via 'di › export › parameters' + {else} + {Dumper::toHtml($parameters)} + {/if} +
+
+
diff --git a/src/Bridges/DITracy/tab.latte b/src/Bridges/DITracy/tab.latte new file mode 100644 index 000000000..42322c5ec --- /dev/null +++ b/src/Bridges/DITracy/tab.latte @@ -0,0 +1,6 @@ + + + + {$elapsedTime ? sprintf('%0.1f ms', $elapsedTime * 1000) : ''} + diff --git a/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml b/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml deleted file mode 100644 index b84022914..000000000 --- a/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml +++ /dev/null @@ -1,81 +0,0 @@ - - - -

Nette DI Container

- -
-
-

Source:

- - - - - - - - - - - - $type): ?> - - - - - - - - - - -
NameAutowiredServiceTags
$name" : Helpers::escapeHtml($name) ?> - - true, Dumper::LIVE => true, Dumper::DEPTH => 5]); ?> - - - - - - true]) - : Dumper::toHtml($tags[$name], [Dumper::COLLAPSE => true]); - } ?>
- -

Parameters

- -
- disabled via 'di › export › parameters'" : Dumper::toHtml($parameters) ?> -
-
-
diff --git a/src/Bridges/DITracy/templates/ContainerPanel.tab.phtml b/src/Bridges/DITracy/templates/ContainerPanel.tab.phtml deleted file mode 100644 index 640319dde..000000000 --- a/src/Bridges/DITracy/templates/ContainerPanel.tab.phtml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/DI/Attributes/Inject.php b/src/DI/Attributes/Inject.php index 325e2b291..f7fd89bab 100644 --- a/src/DI/Attributes/Inject.php +++ b/src/DI/Attributes/Inject.php @@ -12,7 +12,11 @@ use Attribute; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] class Inject { + public function __construct( + public readonly ?string $tag = null, + ) { + } } diff --git a/src/DI/Autowiring.php b/src/DI/Autowiring.php index 601f59ed7..fea6f8cd7 100644 --- a/src/DI/Autowiring.php +++ b/src/DI/Autowiring.php @@ -9,6 +9,8 @@ namespace Nette\DI; +use function array_merge, class_exists, class_implements, class_parents, count, implode, interface_exists, is_a, is_array, natsort, sprintf, str_contains; + /** * Autowiring. @@ -40,35 +42,66 @@ public function __construct(ContainerBuilder $builder) * @throws ServiceCreationException when multiple found */ public function getByType(string $type, bool $throw = false): ?string + { + return $this->getByTypeAndTag($type, null, $throw); + } + + + /** + * Resolves service name by type and tag. + * @return ($throw is true ? string : ?string) + * @throws MissingServiceException when not found + * @throws ServiceCreationException when multiple found + */ + public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string { $type = Helpers::normalizeClass($type); $types = $this->highPriority; - if (empty($types[$type])) { + $services = $types[$type] ?? []; + + if ($services === []) { if ($throw) { if (!class_exists($type) && !interface_exists($type)) { throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type)); } - throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type)); } - return null; + } - } elseif (count($types[$type]) === 1) { - return $types[$type][0]; - - } else { - $list = $types[$type]; - natsort($list); - $hint = count($list) === 2 && ($tmp = str_contains($list[0], '.') xor str_contains($list[1], '.')) - ? '. If you want to overwrite service ' . $list[$tmp ? 0 : 1] . ', give it proper name.' - : ''; - throw new ServiceCreationException(sprintf( - "Multiple services of type $type found: %s%s", - implode(', ', $list), - $hint, - )); + if ($tag !== null) { + $services = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()[$tag] ?? false) !== false); + if ($services === []) { + if ($throw) { + throw new MissingServiceException(sprintf('Service of type %s with tag "%s" not found.', $type, $tag)); + } + return null; + } } + + if (count($services) === 1) { + return reset($services); + } + + if ($tag === null) { + $default = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()['default'] ?? false) !== false); + if (count($default) === 1) { + return reset($default); + } + } + + natsort($services); + $hint = count($services) === 2 && ($tmp = str_contains($services[0], '.') xor str_contains($services[1], '.')) + ? '. If you want to overwrite service ' . $services[$tmp ? 0 : 1] . ', give it proper name.' + : ''; + + throw new ServiceCreationException(sprintf( + 'Multiple services of type %s%s found: %s%s', + $type, + $tag !== null ? " with tag '$tag'" : '', + implode(', ', $services), + $hint, + )); } diff --git a/src/DI/Compiler.php b/src/DI/Compiler.php index 80e9d0fb0..6f675174d 100644 --- a/src/DI/Compiler.php +++ b/src/DI/Compiler.php @@ -11,6 +11,7 @@ use Nette; use Nette\Schema; +use function array_diff_key, array_filter, array_keys, array_merge, assert, count, implode, key, sprintf, strtolower; /** diff --git a/src/DI/CompilerExtension.php b/src/DI/CompilerExtension.php index 8d623b891..ed6b17200 100644 --- a/src/DI/CompilerExtension.php +++ b/src/DI/CompilerExtension.php @@ -10,6 +10,7 @@ namespace Nette\DI; use Nette; +use function array_diff_key, array_keys, func_num_args, implode, is_object, is_string, key, sprintf, str_replace, str_starts_with, substr_replace; /** diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index fcc2d5459..9363cf67b 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -11,10 +11,12 @@ use Nette; use Nette\DI; +use Nette\DI\Definitions; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\Neon; use Nette\Neon\Node; +use function array_walk_recursive, constant, count, defined, implode, is_array, is_string, ltrim, preg_match, preg_replace, sprintf, str_contains, str_ends_with, str_starts_with, substr; /** @@ -24,6 +26,7 @@ final class NeonAdapter implements Nette\DI\Config\Adapter { private const PreventMergingSuffix = '!'; private string $file; + private \WeakMap $parents; /** @@ -40,61 +43,22 @@ public function load(string $file): array $decoder = new Neon\Decoder; $node = $decoder->parseToNode($input); $traverser = new Neon\Traverser; - $node = $traverser->traverse($node, $this->firstClassCallableVisitor(...)); + $node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...)); $node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...)); $node = $traverser->traverse($node, $this->convertAtSignVisitor(...)); $node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...)); $node = $traverser->traverse($node, $this->resolveConstantsVisitor(...)); - return $this->process((array) $node->toValue()); + $node = $traverser->traverse($node, $this->preventMergingVisitor(...)); + $this->connectParentsVisitor($traverser, $node); + $node = $traverser->traverse($node, leave: $this->entityToExpressionVisitor(...)); + return (array) $node->toValue(); } - /** @throws Nette\InvalidStateException */ + /** @deprecated */ public function process(array $arr): array { - $res = []; - foreach ($arr as $key => $val) { - if (is_string($key) && str_ends_with($key, self::PreventMergingSuffix)) { - if (!is_array($val) && $val !== null) { - throw new Nette\DI\InvalidConfigurationException(sprintf( - "Replacing operator is available only for arrays, item '%s' is not array (used in '%s')", - $key, - $this->file, - )); - } - - $key = substr($key, 0, -1); - $val[DI\Config\Helpers::PREVENT_MERGING] = true; - } - - if (is_array($val)) { - $val = $this->process($val); - - } elseif ($val instanceof Neon\Entity) { - if ($val->value === Neon\Neon::Chain) { - $tmp = null; - foreach ($this->process($val->attributes) as $st) { - $tmp = new Statement( - $tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')], - $st->arguments, - ); - } - - $val = $tmp; - } else { - $tmp = $this->process([$val->value]); - if (is_string($tmp[0]) && str_contains($tmp[0], '?')) { - throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')"); - } - - $val = new Statement($tmp[0], $this->process($val->attributes)); - } - } - - $res[$key] = $val; - } - - return $res; + return $arr; } @@ -111,7 +75,7 @@ function (&$val): void { } }, ); - return "# generated by Nette\n\n" . Neon\Neon::encode($data, Neon\Neon::BLOCK); + return "# generated by Nette\n\n" . Neon\Neon::encode($data, blockMode: true); } @@ -151,19 +115,94 @@ function (&$val): void { } - private function firstClassCallableVisitor(Node $node): void + private function preventMergingVisitor(Node $node): void + { + if ($node instanceof Node\ArrayItemNode + && $node->key instanceof Node\LiteralNode + && is_string($node->key->value) + && str_ends_with($node->key->value, self::PreventMergingSuffix) + ) { + if ($node->value instanceof Node\LiteralNode && $node->value->value === null) { + $node->value = new Node\InlineArrayNode('['); + } elseif (!$node->value instanceof Node\ArrayNode) { + throw new Nette\DI\InvalidConfigurationException(sprintf( + "Replacing operator is available only for arrays, item '%s' is not array (used in '%s')", + $node->key->value, + $this->file, + )); + } + + $node->key->value = substr($node->key->value, 0, -1); + $node->value->items[] = $item = new Node\ArrayItemNode; + $item->key = new Node\LiteralNode(DI\Config\Helpers::PREVENT_MERGING); + $item->value = new Node\LiteralNode(true); + } + } + + + private function deprecatedQuestionMarkVisitor(Node $node): void { if ($node instanceof Node\EntityNode - && count($node->attributes) === 1 - && $node->attributes[0]->key === null - && $node->attributes[0]->value instanceof Node\LiteralNode - && $node->attributes[0]->value->value === '...' + && ($node->value instanceof Node\LiteralNode || $node->value instanceof Node\StringNode) + && is_string($node->value->value) + && str_contains($node->value->value, '?') + ) { + throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')"); + } + } + + + private function entityToExpressionVisitor(Node $node): Node + { + if ($node instanceof Node\EntityChainNode) { + return new Node\LiteralNode($this->buildExpression($node->chain)); + + } elseif ( + $node instanceof Node\EntityNode + && !$this->parents[$node] instanceof Node\EntityChainNode ) { - $node->attributes[0]->value->value = Nette\DI\Resolver::getFirstClassCallable()[0]; + return new Node\LiteralNode($this->buildExpression([$node])); + + } else { + return $node; } } + private function buildExpression(array $chain): Definitions\Expression + { + $node = array_pop($chain); + $entity = $node->toValue(); + $stmt = new Statement( + $chain ? [$this->buildExpression($chain), ltrim($entity->value, ':')] : $entity->value, + $entity->attributes, + ); + + if ($this->isFirstClassCallable($node)) { + $entity = $stmt->getEntity(); + if (is_array($entity)) { + if ($entity[0] === '') { + return new Definitions\FunctionCallable($entity[1]); + } + return new Definitions\MethodCallable(...$entity); + } else { + throw new Nette\DI\InvalidConfigurationException("Cannot create closure for '$entity' in config file (used in '$this->file')"); + } + } + + return $stmt; + } + + + private function isFirstClassCallable(Node\EntityNode $node): bool + { + return array_keys($node->attributes) === [0] + && $node->attributes[0]->key === null + && $node->attributes[0]->value instanceof Node\LiteralNode + && $node->attributes[0]->value->value === '...'; + } + + private function removeUnderscoreVisitor(Node $node): void { if (!$node instanceof Node\EntityNode) { @@ -182,7 +221,11 @@ private function removeUnderscoreVisitor(Node $node): void unset($node->attributes[$i]); $index = true; - } elseif ($attr->value instanceof Node\LiteralNode && $attr->value->value === '...') { + } elseif ( + $attr->value instanceof Node\LiteralNode + && $attr->value->value === '...' + && !$this->isFirstClassCallable($node) + ) { trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED); unset($node->attributes[$i]); $index = true; @@ -240,4 +283,21 @@ private function resolveConstantsVisitor(Node $node): void } } } + + + private function connectParentsVisitor(Neon\Traverser $traverser, Node $node): void + { + $this->parents = new \WeakMap; + $stack = []; + $traverser->traverse( + $node, + enter: function (Node $node) use (&$stack) { + $this->parents[$node] = end($stack); + $stack[] = $node; + }, + leave: function () use (&$stack) { + array_pop($stack); + }, + ); + } } diff --git a/src/DI/Config/Helpers.php b/src/DI/Config/Helpers.php index 9124a5236..3f47edbd9 100644 --- a/src/DI/Config/Helpers.php +++ b/src/DI/Config/Helpers.php @@ -10,6 +10,7 @@ namespace Nette\DI\Config; use Nette; +use function is_array; /** diff --git a/src/DI/Config/Loader.php b/src/DI/Config/Loader.php index cd845f019..5bfb0d185 100644 --- a/src/DI/Config/Loader.php +++ b/src/DI/Config/Loader.php @@ -11,6 +11,8 @@ use Nette; use Nette\Utils\Validators; +use function array_unique, dirname, is_file, is_object, is_readable, pathinfo, preg_match, sprintf, strtolower; +use const PATHINFO_EXTENSION; /** diff --git a/src/DI/Container.php b/src/DI/Container.php index d72639a63..00caa858f 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -10,6 +10,7 @@ namespace Nette\DI; use Nette; +use function array_flip, array_key_exists, array_keys, array_map, array_merge, array_values, class_exists, count, get_class_methods, implode, interface_exists, is_a, is_object, natsort, sprintf, str_replace, ucfirst; /** @@ -235,24 +236,76 @@ public function createService(string $name): object * Returns an instance of the autowired service of the given type. If it has not been created yet, it creates it. * @template T of object * @param class-string $type + * @param bool $throw throw exception if service doesn't exist? * @return ($throw is true ? T : ?T) * @throws MissingServiceException */ public function getByType(string $type, bool $throw = true): ?object + { + return $this->getByTypeAndTag($type, null, $throw); + } + + + /** + * Returns an instance of the autowired service of the given type and tag. If it has not been created yet, it creates it. + * @template T of object + * @param class-string $type + * @param bool $throw throw exception if service doesn't exist? + * @return ($throw is true ? T : ?T) + * @throws MissingServiceException + */ + public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = true): ?object { $type = Helpers::normalizeClass($type); if (!empty($this->wiring[$type][0])) { - if (count($names = $this->wiring[$type][0]) === 1) { - return $this->getService($names[0]); + $names = $this->wiring[$type][0]; + + // Filter by tag if specified + if ($tag !== null) { + $taggedNames = []; + foreach ($names as $name) { + $serviceTags = $this->findByTag($tag); + if (isset($serviceTags[$name])) { + $taggedNames[] = $name; + } + } + $names = $taggedNames; + } + + // Try to find service with tag default + if ($tag === null && count($names) > 1) { + $defaultTagNames = []; + foreach ($names as $name) { + if (isset($this->findByTag('default')[$name])) { + $defaultTagNames[] = $name; + } + } + + if ($defaultTagNames !== []) { + $names = $defaultTagNames; + } } - natsort($names); - throw new MissingServiceException(sprintf("Multiple services of type $type found: %s.", implode(', ', $names))); + if (count($names) === 1) { + return $this->getService($names[0]); + } elseif (count($names) > 1) { + natsort($names); + + throw new MissingServiceException(sprintf( + 'Multiple services of type %s%s found: %s.', + $type, + $tag !== null ? " with tag '$tag'" : '', + implode(', ', $names), + )); + } + } - } elseif ($throw) { + if ($throw) { if (!class_exists($type) && !interface_exists($type)) { throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type)); - } elseif ($this->findByType($type)) { + } elseif ($tag !== null) { + throw new MissingServiceException(sprintf("Service of type %s with tag '%s' not found.", $type, $tag)); + } elseif ($this->findByType($type) !== []) { throw new MissingServiceException(sprintf("Service of type %s is not autowired or is missing in di\u{a0}›\u{a0}export\u{a0}›\u{a0}types.", $type)); } else { throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type)); @@ -320,6 +373,9 @@ private function preventDeadLock(string $key, \Closure $callback): mixed /** * Creates an instance of the class and passes dependencies to the constructor using autowiring. + * @template T of object + * @param class-string $class + * @return T */ public function createInstance(string $class, array $args = []): object { @@ -359,7 +415,7 @@ public function callMethod(callable $function, array $args = []): mixed private function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array { return Resolver::autowireArguments($function, $args, fn(string $type, bool $single) => $single - ? $this->getByType($type) + ? $this->getByType($type, throw: true) : array_map($this->getService(...), $this->findAutowired($type))); } diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index f2844d508..1a0ffa455 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -11,6 +11,7 @@ use Nette; use Nette\DI\Definitions\Definition; +use function array_diff, array_filter, array_walk_recursive, class_implements, class_parents, is_a, is_int, key, ksort, preg_match, sprintf, strtolower; /** @@ -22,10 +23,10 @@ class ContainerBuilder ThisService = 'self', ThisContainer = 'container'; - /** @deprecated use ContainerBuilder::ThisService */ + #[\Deprecated('use ContainerBuilder::ThisService')] public const THIS_SERVICE = self::ThisService; - /** @deprecated use ContainerBuilder::ThisContainer */ + #[\Deprecated('use ContainerBuilder::ThisContainer')] public const THIS_CONTAINER = self::ThisContainer; public array $parameters = []; @@ -214,9 +215,20 @@ public function addExcludedClasses(array $types): static * @throws MissingServiceException */ public function getByType(string $type, bool $throw = false): ?string + { + return $this->getByTypeAndTag($type, null, $throw); + } + + + /** + * Resolves autowired service name by type and tag. + * @return ($throw is true ? string : ?string) + * @throws MissingServiceException + */ + public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string { $this->needResolved(); - return $this->autowiring->getByType($type, $throw); + return $this->autowiring->getByTypeAndTag($type, $tag, $throw); } @@ -394,8 +406,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/ContainerLoader.php b/src/DI/ContainerLoader.php index 8162013d7..3b9acf4f9 100644 --- a/src/DI/ContainerLoader.php +++ b/src/DI/ContainerLoader.php @@ -10,6 +10,8 @@ namespace Nette\DI; use Nette; +use function class_exists, file_get_contents, file_put_contents, flock, fopen, function_exists, hash, is_file, rename, serialize, sprintf, strlen, substr, unlink, unserialize; +use const LOCK_EX, LOCK_UN; /** diff --git a/src/DI/Definitions/AccessorDefinition.php b/src/DI/Definitions/AccessorDefinition.php index bd6c1a1a5..51a55ef91 100644 --- a/src/DI/Definitions/AccessorDefinition.php +++ b/src/DI/Definitions/AccessorDefinition.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\Helpers; use Nette\Utils\Type; +use function count, interface_exists, sprintf, str_starts_with, substr; /** @@ -103,11 +104,11 @@ public function complete(Nette\DI\Resolver $resolver): void $this->setReference(Type::fromReflection($method)->getSingleName()); } - $this->reference = $resolver->normalizeReference($this->reference); + $this->reference->complete($resolver); } - public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $class = (new Nette\PhpGenerator\ClassType) ->addImplement($this->getType()); @@ -123,6 +124,6 @@ public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGe ->setBody('return $this->container->getService(?);', [$this->reference->getValue()]) ->setReturnType((string) Type::fromReflection($rm)); - $method->setBody('return new class ($this) ' . $class . ';'); + return 'return new class ($this) ' . $class . ';'; } } diff --git a/src/DI/Definitions/Definition.php b/src/DI/Definitions/Definition.php index ca96659b8..da5568960 100644 --- a/src/DI/Definitions/Definition.php +++ b/src/DI/Definitions/Definition.php @@ -10,6 +10,7 @@ namespace Nette\DI\Definitions; use Nette; +use function class_exists, interface_exists, is_array, is_string, sprintf; /** @@ -147,7 +148,7 @@ abstract public function resolveType(Nette\DI\Resolver $resolver): void; abstract public function complete(Nette\DI\Resolver $resolver): void; - abstract public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void; + //abstract public function generateCode(Nette\DI\PhpGenerator $generator): string; final public function setNotifier(?\Closure $notifier): void @@ -159,6 +160,13 @@ final public function setNotifier(?\Closure $notifier): void /********************* deprecated stuff from former ServiceDefinition ****************d*g**/ + /** @deprecated */ + public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + { + $method->setBody($this->generateCode($generator)); + } + + /** @deprecated Use setType() */ public function setClass(?string $type) { diff --git a/src/DI/Definitions/Expression.php b/src/DI/Definitions/Expression.php new file mode 100644 index 000000000..da7b4a24f --- /dev/null +++ b/src/DI/Definitions/Expression.php @@ -0,0 +1,24 @@ +resolveEntityType($this->resultDefinition->getCreator())) + ($class = $this->resultDefinition->getCreator()->resolveType($resolver)) && ($ctor = (new \ReflectionClass($class))->getConstructor()) ) { foreach ($ctor->getParameters() as $param) { @@ -196,7 +197,7 @@ public function convertArguments(array &$args): void } - public function generateMethod(Php\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $class = (new Php\ClassType) ->addImplement($this->getType()); @@ -218,7 +219,7 @@ public function generateMethod(Php\Method $method, Nette\DI\PhpGenerator $genera ->setReturnType((string) Type::fromReflection($rm)) ->setBody($body); - $method->setBody('return new class ($this) ' . $class . ';'); + return 'return new class ($this) ' . $class . ';'; } diff --git a/src/DI/Definitions/FunctionCallable.php b/src/DI/Definitions/FunctionCallable.php new file mode 100644 index 000000000..1de1a15ab --- /dev/null +++ b/src/DI/Definitions/FunctionCallable.php @@ -0,0 +1,44 @@ +function . '(...)'; + } +} diff --git a/src/DI/Definitions/ImportedDefinition.php b/src/DI/Definitions/ImportedDefinition.php index e9116653b..cc8caf70e 100644 --- a/src/DI/Definitions/ImportedDefinition.php +++ b/src/DI/Definitions/ImportedDefinition.php @@ -10,7 +10,6 @@ namespace Nette\DI\Definitions; use Nette; -use Nette\DI\PhpGenerator; /** @@ -34,9 +33,9 @@ public function complete(Nette\DI\Resolver $resolver): void } - public function generateMethod(Nette\PhpGenerator\Method $method, PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { - $method->setBody( + return $generator->formatPhp( 'throw new Nette\DI\ServiceCreationException(?);', ["Unable to create imported service '{$this->getName()}', it must be added using addService()"], ); diff --git a/src/DI/Definitions/LocatorDefinition.php b/src/DI/Definitions/LocatorDefinition.php index a9e3f735a..9b77c7945 100644 --- a/src/DI/Definitions/LocatorDefinition.php +++ b/src/DI/Definitions/LocatorDefinition.php @@ -10,6 +10,7 @@ namespace Nette\DI\Definitions; use Nette; +use function array_map, interface_exists, lcfirst, preg_match, sprintf, str_starts_with, substr; /** @@ -121,13 +122,13 @@ 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); } } - public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $class = (new Nette\PhpGenerator\ClassType) ->addImplement($this->getType()); @@ -171,6 +172,6 @@ public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGe } } - $method->setBody('return new class ($this) ' . $class . ';'); + return 'return new class ($this) ' . $class . ';'; } } diff --git a/src/DI/Definitions/MethodCallable.php b/src/DI/Definitions/MethodCallable.php new file mode 100644 index 000000000..116926c3d --- /dev/null +++ b/src/DI/Definitions/MethodCallable.php @@ -0,0 +1,53 @@ +objectOrClass instanceof Expression) { + $this->objectOrClass->complete($resolver); + } + } + + + public function generateCode(PhpGenerator $generator): string + { + return is_string($this->objectOrClass) + ? $generator->formatPhp('?::?(...)', [new Php\Literal($this->objectOrClass), $this->method]) + : $generator->formatPhp('?->?(...)', [new Php\Literal($this->objectOrClass->generateCode($generator)), $this->method]); + } +} diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index 25625dec7..72fb52397 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -9,33 +9,39 @@ namespace Nette\DI\Definitions; +use Nette\DI; + + /** * Reference to service. Either by name or by type or reference to the 'self' service. */ -final class Reference +final class Reference extends Expression { public const Self = 'self'; - /** @deprecated use Reference::Self */ + #[\Deprecated('use Reference::Self')] public const SELF = self::Self; private string $value; + private ?string $tag; - public static function fromType(string $value): static + + public static function fromType(string $value, ?string $tag = null): static { if (!str_contains($value, '\\')) { $value = '\\' . $value; } - return new static($value); + return new static($value, $tag); } - public function __construct(string $value) + public function __construct(string $value, ?string $tag = null) { $this->value = $value; + $this->tag = $tag; } @@ -61,4 +67,59 @@ public function isSelf(): bool { return $this->value === self::Self; } + + + public function resolveType(DI\Resolver $resolver): ?string + { + if ($this->isSelf()) { + return $resolver->getCurrentService(type: true); + + } elseif ($this->isType()) { + return ltrim($this->value, '\\'); + } + + $def = $resolver->getContainerBuilder()->getDefinition($this->value); + if (!$def->getType()) { + $resolver->resolveDefinition($def); + } + + return $def->getType(); + } + + + /** + * 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 { + $reference = $resolver->getByType($this->value, $this->tag); + $this->value = $reference->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) { + $this->isSelf() => '$service', + $this->value === DI\ContainerBuilder::ThisContainer => '$this', + default => $generator->formatPhp('$this->getService(?)', [$this->value]), + }; + } } diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index ff934c26a..eb5b46b57 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\ServiceCreationException; use Nette\Utils\Strings; +use function array_pop, class_exists, class_parents, count, implode, is_string, preg_grep, serialize, strpbrk, unserialize; /** @@ -141,7 +142,7 @@ public function resolveType(Nette\DI\Resolver $resolver): void $this->setCreator($this->getType(), $this->creator->arguments ?? []); } elseif (!$this->getType()) { - $type = $resolver->resolveEntityType($this->creator); + $type = $this->creator->resolveType($resolver); if (!$type) { throw new ServiceCreationException('Unknown service type, specify it or declare return type of factory method.'); } @@ -161,14 +162,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->completeStatement($setup, true); + foreach ($this->setup as $setup) { + $setup->complete($resolver->withCurrentServiceAvailable()); } } @@ -181,11 +182,11 @@ private function prependSelf(Statement $setup): Statement } - public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $lines = []; foreach ([$this->creator, ...$this->setup] as $stmt) { - $lines[] = $generator->formatStatement($stmt) . ";\n"; + $lines[] = $stmt->generateCode($generator) . ";\n"; } if ($this->canBeLazy() && !preg_grep('#(?:func_get_arg|func_num_args)#i', $lines)) { // latteFactory workaround @@ -193,15 +194,15 @@ public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGe $lines[0] = (new \ReflectionClass($class))->hasMethod('__construct') ? $generator->formatPhp("\$service->__construct(...?:);\n", [$this->creator->arguments]) : ''; - $method->setBody("return new ReflectionClass($class::class)->newLazyGhost(function (\$service) {\n" + return "return new ReflectionClass($class::class)->newLazyGhost(function (\$service) {\n" . Strings::indent(implode('', $lines)) - . '});'); + . '});'; } elseif (count($lines) === 1) { - $method->setBody('return ' . $lines[0]); + return 'return ' . $lines[0]; } else { - $method->setBody('$service = ' . implode('', $lines) . 'return $service;'); + return '$service = ' . implode('', $lines) . 'return $service;'; } } diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index cd80f6d44..9c65b3953 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -10,6 +10,13 @@ namespace Nette\DI\Definitions; use Nette; +use Nette\DI; +use Nette\DI\Resolver; +use Nette\DI\ServiceCreationException; +use Nette\PhpGenerator as Php; +use Nette\Utils\Callback; +use Nette\Utils\Validators; +use function array_keys, class_exists, explode, is_array, is_string, str_contains, str_starts_with, substr; /** @@ -17,7 +24,7 @@ * * @property string|array|Definition|Reference|null $entity */ -final class Statement implements Nette\Schema\DynamicParameter +final class Statement extends Expression implements Nette\Schema\DynamicParameter { use Nette\SmartObject; @@ -62,6 +69,313 @@ public function getEntity(): string|array|Definition|Reference|null { return $this->entity; } + + + public function resolveType(Resolver $resolver): ?string + { + $entity = $this->normalizeEntity($resolver); + + if (is_array($entity)) { + if ($entity[0] instanceof Expression) { + $entity[0] = $entity[0]->resolveType($resolver); + if (!$entity[0]) { + return null; + } + } + + try { + $reflection = Callback::toReflection($entity[0] === '' ? $entity[1] : $entity); + assert($reflection instanceof \ReflectionMethod || $reflection instanceof \ReflectionFunction); + $refClass = $reflection instanceof \ReflectionMethod + ? $reflection->getDeclaringClass() + : null; + } catch (\ReflectionException $e) { + $refClass = $reflection = null; + } + + if (isset($e) || ($refClass && (!$reflection->isPublic() + || ($refClass->isTrait() && !$reflection->isStatic()) + ))) { + throw new ServiceCreationException(sprintf('Method %s() is not callable.', Callback::toString($entity)), 0, $e ?? null); + } + + $resolver->addDependency($reflection); + + $type = Nette\Utils\Type::fromReflection($reflection) ?? ($annotation = DI\Helpers::getReturnTypeAnnotation($reflection)); + if ($type && !in_array($type->getSingleName(), ['object', 'mixed'], strict: true)) { + if (isset($annotation)) { + trigger_error('Annotation @return should be replaced with native return type at ' . Callback::toString($entity), E_USER_DEPRECATED); + } + + return DI\Helpers::ensureClassType( + $type, + sprintf('return type of %s()', Callback::toString($entity)), + allowNullable: true, + ); + } + + return null; + + } elseif ($entity instanceof Expression) { + return $entity->resolveType($resolver); + + } elseif (is_string($entity)) { // class + if (!class_exists($entity)) { + throw new ServiceCreationException(sprintf( + interface_exists($entity) + ? "Interface %s can not be used as 'create' or 'factory', did you mean 'implement'?" + : "Class '%s' not found.", + $entity, + )); + } + + return $entity; + } + + return null; + } + + + public function complete(Resolver $resolver): void + { + $entity = $this->normalizeEntity($resolver); + $this->convertReferences($resolver); + $arguments = $this->arguments; + + switch (true) { + 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. + */ + public function generateCode(DI\PhpGenerator $generator): string + { + $entity = $this->entity; + $arguments = $this->arguments; + + switch (true) { + case is_string($entity) && str_contains($entity, '?'): // PHP literal + return $generator->formatPhp($entity, $arguments); + + case is_string($entity): // create class + return $arguments + ? $generator->formatPhp("new $entity(...?:)", [$arguments]) + : $generator->formatPhp("new $entity", []); + + case is_array($entity): + switch (true) { + case $entity[1][0] === '$': // property getter, setter or appender + $name = substr($entity[1], 1); + if ($append = (str_ends_with($name, '[]'))) { + $name = substr($name, 0, -2); + } + + $prop = $entity[0] instanceof Reference + ? $generator->formatPhp('?->?', [$entity[0], $name]) + : $generator->formatPhp('?::$?', [$entity[0], $name]); + return $arguments + ? $generator->formatPhp(($append ? '?[]' : '?') . ' = ?', [new Php\Literal($prop), $arguments[0]]) + : $prop; + + case $entity[0] instanceof self: + $inner = $generator->formatPhp('?', [$entity[0]]); + if (str_starts_with($inner, 'new ')) { + $inner = "($inner)"; + } + + return $generator->formatPhp('?->?(...?:)', [new Php\Literal($inner), $entity[1], $arguments]); + + case $entity[0] instanceof Reference: + return $generator->formatPhp('?->?(...?:)', [$entity[0], $entity[1], $arguments]); + + case $entity[0] === '': // function call + return $generator->formatPhp('?(...?:)', [new Php\Literal($entity[1]), $arguments]); + + case is_string($entity[0]): // static method call + return $generator->formatPhp('?::?(...?:)', [new Php\Literal($entity[0]), $entity[1], $arguments]); + } + } + + throw new Nette\InvalidStateException; + } } diff --git a/src/DI/DependencyChecker.php b/src/DI/DependencyChecker.php index 94eb9fdc8..0fefd3e0d 100644 --- a/src/DI/DependencyChecker.php +++ b/src/DI/DependencyChecker.php @@ -13,6 +13,8 @@ use Nette\Utils\Reflection; use ReflectionClass; use ReflectionMethod; +use function array_combine, array_flip, array_keys, array_map, array_merge, array_unique, class_implements, class_parents, class_uses, count, get_debug_type, get_parent_class, hash, is_object, is_string, rtrim, serialize, sprintf, str_contains; +use const PHP_VERSION_ID, SORT_REGULAR; /** @@ -22,7 +24,7 @@ class DependencyChecker { public const Version = 1; - /** @deprecated use DependencyChecker::Version */ + #[\Deprecated('use DependencyChecker::Version')] public const VERSION = self::Version; /** @var array */ diff --git a/src/DI/Extensions/DIExtension.php b/src/DI/Extensions/DIExtension.php index 99e9de0f1..0bcf76948 100644 --- a/src/DI/Extensions/DIExtension.php +++ b/src/DI/Extensions/DIExtension.php @@ -12,6 +12,8 @@ use Nette; use Nette\DI\Definitions\ServiceDefinition; use Tracy; +use function array_flip, array_intersect_key, is_array, microtime; +use const PHP_VERSION_ID; /** diff --git a/src/DI/Extensions/DecoratorExtension.php b/src/DI/Extensions/DecoratorExtension.php index 0725d8086..00c2596bc 100644 --- a/src/DI/Extensions/DecoratorExtension.php +++ b/src/DI/Extensions/DecoratorExtension.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\Definitions; use Nette\Schema\Expect; +use function array_filter, array_values, class_exists, interface_exists, is_a, is_array, key, sprintf; /** diff --git a/src/DI/Extensions/DefinitionSchema.php b/src/DI/Extensions/DefinitionSchema.php index 7935324e8..97a16166a 100644 --- a/src/DI/Extensions/DefinitionSchema.php +++ b/src/DI/Extensions/DefinitionSchema.php @@ -16,6 +16,7 @@ use Nette\Schema\Context; use Nette\Schema\Expect; use Nette\Schema\Schema; +use function array_keys, end, get_class, interface_exists, is_array, is_string, method_exists, preg_match, substr; /** diff --git a/src/DI/Extensions/ExtensionsExtension.php b/src/DI/Extensions/ExtensionsExtension.php index 591ad4fda..026680138 100644 --- a/src/DI/Extensions/ExtensionsExtension.php +++ b/src/DI/Extensions/ExtensionsExtension.php @@ -10,6 +10,7 @@ namespace Nette\DI\Extensions; use Nette; +use function is_a, is_int, sprintf; /** diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index 4c40b30c0..df8899bc9 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -13,6 +13,7 @@ use Nette\DI; use Nette\DI\Definitions; use Nette\Utils\Reflection; +use function array_keys, array_reverse, array_search, array_unshift, get_class_methods, is_a, is_subclass_of, ksort, sprintf, str_starts_with, uksort; /** @@ -22,7 +23,7 @@ final class InjectExtension extends DI\CompilerExtension { public const TagInject = 'nette.inject'; - /** @deprecated use InjectExtension::TagInject */ + #[\Deprecated('use InjectExtension::TagInject')] public const TAG_INJECT = self::TagInject; @@ -49,15 +50,57 @@ public function beforeCompile(): void private function updateDefinition(Definitions\ServiceDefinition $def): void { - $resolvedType = (new DI\Resolver($this->getContainerBuilder()))->resolveEntityType($def->getCreator()); + $resolvedType = $def->getCreator()->resolveType(new DI\Resolver($this->getContainerBuilder())); $class = is_subclass_of($resolvedType, $def->getType()) ? $resolvedType : $def->getType(); $setups = $def->getSetup(); - foreach (self::getInjectProperties($class) as $property => $type) { + // Inject attributes in constructor parameters + $constructor = (new \ReflectionClass($class))->getConstructor(); + if ($constructor !== null) { + foreach ($constructor->getParameters() as $param) { + $attributes = $param->getAttributes(DI\Attributes\Inject::class); + if ($attributes !== []) { + $injectAttribute = $attributes[0]->newInstance(); + $tag = $injectAttribute->tag; + if ($tag === null) { + throw new Nette\InvalidStateException(sprintf( + 'Attribute #[Inject] on parameter $%s in %s is redundant.', + $param->getName(), + Reflection::toString($constructor), + )); + } + + $type = Nette\Utils\Type::fromReflection($param); + if ($type === null) { + throw new Nette\InvalidStateException(sprintf( + 'Parameter $%s in %s has no type hint.', + $param->getName(), + Reflection::toString($constructor), + )); + } + + // Update the creator arguments to use the tagged service + $creator = $def->getCreator(); + $arguments = $creator->arguments; + $arguments[$param->getName()] = Definitions\Reference::fromType((string) $type, $tag); + $def->setCreator($creator->getEntity(), $arguments); + } + } + } + + // Inject attributes in properties + foreach (self::getInjectProperties($class) as $property => $typeAndTag) { + $type = $typeAndTag['type']; + $tag = $typeAndTag['tag']; + $builder = $this->getContainerBuilder(); - $inject = new Definitions\Statement(['@self', '$' . $property], [Definitions\Reference::fromType((string) $type)]); + $inject = new Definitions\Statement( + ['@self', '$' . $property], + [Definitions\Reference::fromType($type, $tag)], + ); + foreach ($setups as $key => $setup) { if ($setup->getEntity() == $inject->getEntity()) { // intentionally == $inject = $setup; @@ -66,14 +109,46 @@ private function updateDefinition(Definitions\ServiceDefinition $def): void } } - if ($builder) { - self::checkType($class, $property, $type, $builder); + if ($builder !== null) { + self::checkType($class, $property, $type, $builder, $tag); } array_unshift($setups, $inject); } foreach (array_reverse(self::getInjectMethods($class)) as $method) { $inject = new Definitions\Statement(['@self', $method]); + $methodReflection = new \ReflectionMethod($class, $method); + $arguments = []; + + // Inject attributes in inject methods + foreach ($methodReflection->getParameters() as $param) { + $attributes = $param->getAttributes(DI\Attributes\Inject::class); + if ($attributes !== []) { + $injectAttribute = $attributes[0]->newInstance(); + $tag = $injectAttribute->tag; + if ($tag === null) { + throw new Nette\InvalidStateException(sprintf( + 'Parameter %s has #[Inject] attribute, but no tag specified.', + Reflection::toString($param), + )); + } + + $type = Nette\Utils\Type::fromReflection($param); + if ($type === null) { + throw new Nette\InvalidStateException(sprintf( + 'Parameter $%s in %s has no type hint.', + $param->getName(), + Reflection::toString($methodReflection), + )); + } + $arguments[$param->getName()] = Definitions\Reference::fromType((string) $type, $tag); + } + } + + if ($arguments !== []) { + $inject = new Definitions\Statement(['@self', $method], $arguments); + } + foreach ($setups as $key => $setup) { if ($setup->getEntity() == $inject->getEntity()) { // intentionally == $inject = $setup; @@ -112,11 +187,16 @@ public static function getInjectMethods(string $class): array /** * Generates list of properties with annotation @inject. * @internal + * @return array */ public static function getInjectProperties(string $class): array { $res = []; foreach ((new \ReflectionClass($class))->getProperties() as $rp) { + if ($rp->isPromoted()) { + continue; // Setup is in constructor + } + $hasAttr = $rp->getAttributes(DI\Attributes\Inject::class); if ($hasAttr || DI\Helpers::parseAnnotation($rp, 'inject') !== null) { if (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly()) { @@ -129,7 +209,12 @@ public static function getInjectProperties(string $class): array $type = Nette\Utils\Type::fromString($annotation); } - $res[$rp->getName()] = DI\Helpers::ensureClassType($type, 'type of property ' . Reflection::toString($rp)); + $tag = null; + if ($hasAttr !== []) { + $tag = $hasAttr[0]->newInstance()->tag; + } + + $res[$rp->getName()] = ['type' => DI\Helpers::ensureClassType($type, 'type of property ' . Reflection::toString($rp)), 'tag' => $tag]; } } @@ -147,9 +232,11 @@ public static function callInjects(DI\Container $container, object $service): vo $container->callMethod([$service, $method]); } - foreach (self::getInjectProperties($service::class) as $property => $type) { - self::checkType($service, $property, $type, $container); - $service->$property = $container->getByType($type); + foreach (self::getInjectProperties($service::class) as $property => $propertyInfo) { + $type = $propertyInfo['type']; + $tag = $propertyInfo['tag']; + self::checkType($service, $property, $type, $container, $tag); + $service->$property = $container->getByTypeAndTag($type, $tag, throw: true); } } @@ -159,12 +246,14 @@ private static function checkType( string $name, ?string $type, DI\Container|DI\ContainerBuilder $container, + ?string $tag = null, ): void { - if (!$container->getByType($type, throw: false)) { + if (!$container->getByTypeAndTag($type, $tag, throw: false)) { throw new Nette\DI\MissingServiceException(sprintf( - 'Service of type %s required by %s not found. Did you add it to configuration file?', + 'Service of type %s%s required by %s not found. Did you add it to configuration file?', $type, + $tag !== null ? " with tag '$tag'" : '', Reflection::toString(new \ReflectionProperty($class, $name)), )); } diff --git a/src/DI/Extensions/ParametersExtension.php b/src/DI/Extensions/ParametersExtension.php index 144554d5f..1d2f3365d 100644 --- a/src/DI/Extensions/ParametersExtension.php +++ b/src/DI/Extensions/ParametersExtension.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\DynamicParameter; use Nette\DI\Helpers; +use function array_diff_key, array_fill_keys, array_keys, array_walk_recursive, implode, var_export; /** diff --git a/src/DI/Extensions/SearchExtension.php b/src/DI/Extensions/SearchExtension.php index 0be9a8b25..4739b3777 100644 --- a/src/DI/Extensions/SearchExtension.php +++ b/src/DI/Extensions/SearchExtension.php @@ -13,6 +13,7 @@ use Nette\Loaders\RobotLoader; use Nette\Schema\Expect; use Nette\Utils\Arrays; +use function array_filter, array_keys, array_merge, array_unique, class_exists, count, implode, in_array, interface_exists, is_dir, is_string, method_exists, preg_match, preg_quote, sprintf, str_contains, str_replace, trait_exists; /** diff --git a/src/DI/Extensions/ServicesExtension.php b/src/DI/Extensions/ServicesExtension.php index 38d7d47af..dc69ec4a7 100644 --- a/src/DI/Extensions/ServicesExtension.php +++ b/src/DI/Extensions/ServicesExtension.php @@ -13,6 +13,7 @@ use Nette\DI\Definitions; use Nette\DI\Definitions\Statement; use Nette\DI\Helpers; +use function array_replace, array_values, is_array, is_int, is_string, key, preg_match, substr; /** diff --git a/src/DI/Helpers.php b/src/DI/Helpers.php index 48e421fbf..51bcda464 100644 --- a/src/DI/Helpers.php +++ b/src/DI/Helpers.php @@ -14,6 +14,8 @@ use Nette\DI\Definitions\Statement; use Nette\Utils\Reflection; use Nette\Utils\Type; +use function array_key_exists, array_keys, array_shift, class_exists, explode, get_debug_type, implode, interface_exists, is_array, is_scalar, is_string, preg_match, preg_quote, preg_replace, preg_split, settype, sprintf, str_replace, strlen, strncmp, substr, trim, ucfirst, var_export; +use const PREG_SPLIT_DELIM_CAPTURE; /** diff --git a/src/DI/PhpGenerator.php b/src/DI/PhpGenerator.php index b5176c40d..35788b294 100644 --- a/src/DI/PhpGenerator.php +++ b/src/DI/PhpGenerator.php @@ -9,10 +9,9 @@ namespace Nette\DI; -use Nette; -use Nette\DI\Definitions\Reference; -use Nette\DI\Definitions\Statement; +use Nette\DI\Definitions\Expression; use Nette\PhpGenerator as Php; +use function array_walk_recursive, is_array, is_object, is_string, ksort, sprintf, str_contains, str_ends_with, str_starts_with, substr; /** @@ -103,58 +102,10 @@ public function generateMethod(Definitions\Definition $def): Php\Method } - /** - * Formats PHP code for class instantiating, function calling or property setting in PHP. - */ - public function formatStatement(Statement $statement): string + /** @deprecated */ + public function formatStatement(Definitions\Statement $statement): string { - $entity = $statement->getEntity(); - $arguments = $statement->arguments; - - switch (true) { - case is_string($entity) && str_contains($entity, '?'): // PHP literal - return $this->formatPhp($entity, $arguments); - - case is_string($entity): // create class - return $arguments - ? $this->formatPhp("new $entity(...?:)", [$arguments]) - : $this->formatPhp("new $entity", []); - - case is_array($entity): - switch (true) { - case $entity[1][0] === '$': // property getter, setter or appender - $name = substr($entity[1], 1); - if ($append = (str_ends_with($name, '[]'))) { - $name = substr($name, 0, -2); - } - - $prop = $entity[0] instanceof Reference - ? $this->formatPhp('?->?', [$entity[0], $name]) - : $this->formatPhp('?::$?', [$entity[0], $name]); - return $arguments - ? $this->formatPhp(($append ? '?[]' : '?') . ' = ?', [new Php\Literal($prop), $arguments[0]]) - : $prop; - - case $entity[0] instanceof Statement: - $inner = $this->formatPhp('?', [$entity[0]]); - if (str_starts_with($inner, 'new ')) { - $inner = "($inner)"; - } - - return $this->formatPhp('?->?(...?:)', [new Php\Literal($inner), $entity[1], $arguments]); - - case $entity[0] instanceof Reference: - return $this->formatPhp('?->?(...?:)', [$entity[0], $entity[1], $arguments]); - - case $entity[0] === '': // function call - return $this->formatPhp('?(...?:)', [new Php\Literal($entity[1]), $arguments]); - - case is_string($entity[0]): // static method call - return $this->formatPhp('?::?(...?:)', [new Php\Literal($entity[0]), $entity[1], $arguments]); - } - } - - throw new Nette\InvalidStateException; + return $statement->generateCode($this); } @@ -171,18 +122,8 @@ public function formatPhp(string $statement, array $args): string public function convertArguments(array $args): array { array_walk_recursive($args, function (&$val): void { - if ($val instanceof Statement) { - $val = new Php\Literal($this->formatStatement($val)); - - } elseif ($val instanceof Reference) { - $name = $val->getValue(); - if ($val->isSelf()) { - $val = new Php\Literal('$service'); - } elseif ($name === ContainerBuilder::ThisContainer) { - $val = new Php\Literal('$this'); - } else { - $val = ContainerBuilder::literal('$this->getService(?)', [$name]); - } + if ($val instanceof Expression) { + $val = new Php\Literal($val->generateCode($this)); } elseif ( is_object($val) && !$val instanceof Php\Literal && !$val instanceof \DateTimeInterface diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index ceab3d135..71ee05c10 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -13,11 +13,9 @@ use Nette\DI\Definitions\Definition; 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; +use function array_filter, array_key_exists, array_map, array_merge, array_values, array_walk_recursive, assert, class_exists, count, ctype_digit, explode, function_exists, gettype, implode, in_array, interface_exists, is_a, is_array, is_int, is_scalar, is_string, iterator_to_array, ltrim, preg_match, preg_replace, sprintf, str_contains, str_ends_with, str_replace, str_starts_with, strlen, substr; /** @@ -42,6 +40,32 @@ public function __construct(ContainerBuilder $builder) } + public function withCurrentService(Definition $definition): self + { + $dolly = clone $this; + $dolly->currentService = in_array($definition, $this->builder->getDefinitions(), strict: true) + ? $definition + : null; + $dolly->currentServiceType = $definition->getType(); + $dolly->currentServiceAllowed = false; + return $dolly; + } + + + public function withCurrentServiceAvailable(): self + { + $dolly = clone $this; + $dolly->currentServiceAllowed = true; + return $dolly; + } + + + public function getCurrentService(bool $type = false): Definition|string|null + { + return $type ? $this->currentServiceType : $this->currentService; + } + + public function getContainerBuilder(): ContainerBuilder { return $this->builder; @@ -59,7 +83,6 @@ public function resolveDefinition(Definition $def): void $this->recursive->attach($def); $def->resolveType($this); - if (!$def->getType()) { throw new ServiceCreationException('Type of service is unknown.'); } @@ -72,349 +95,35 @@ public function resolveDefinition(Definition $def): void } - public function resolveReferenceType(Reference $ref): ?string - { - if ($ref->isSelf()) { - return $this->currentServiceType; - } elseif ($ref->isType()) { - return ltrim($ref->getValue(), '\\'); - } - - $def = $this->resolveReference($ref); - if (!$def->getType()) { - $this->resolveDefinition($def); - } - - return $def->getType(); - } - - - public function resolveEntityType(Statement $statement): ?string - { - $entity = $this->normalizeEntity($statement); - - if ($statement->arguments === self::getFirstClassCallable()) { - return \Closure::class; - - } elseif (is_array($entity)) { - if ($entity[0] instanceof Reference || $entity[0] instanceof Statement) { - $entity[0] = $this->resolveEntityType($entity[0] instanceof Statement ? $entity[0] : new Statement($entity[0])); - if (!$entity[0]) { - return null; - } - } - - try { - $reflection = Callback::toReflection($entity[0] === '' ? $entity[1] : $entity); - assert($reflection instanceof \ReflectionMethod || $reflection instanceof \ReflectionFunction); - $refClass = $reflection instanceof \ReflectionMethod - ? $reflection->getDeclaringClass() - : null; - } catch (\ReflectionException $e) { - $refClass = $reflection = null; - } - - if (isset($e) || ($refClass && (!$reflection->isPublic() - || ($refClass->isTrait() && !$reflection->isStatic()) - ))) { - throw new ServiceCreationException(sprintf('Method %s() is not callable.', Callback::toString($entity)), 0, $e ?? null); - } - - $this->addDependency($reflection); - - $type = Nette\Utils\Type::fromReflection($reflection) ?? ($annotation = Helpers::getReturnTypeAnnotation($reflection)); - if ($type && !in_array($type->getSingleName(), ['object', 'mixed'], strict: true)) { - if (isset($annotation)) { - trigger_error('Annotation @return should be replaced with native return type at ' . Callback::toString($entity), E_USER_DEPRECATED); - } - - return Helpers::ensureClassType( - $type, - sprintf('return type of %s()', Callback::toString($entity)), - allowNullable: true, - ); - } - - return null; - - } elseif ($entity instanceof Reference) { // alias or factory - return $this->resolveReferenceType($entity); - - } elseif (is_string($entity)) { // class - if (!class_exists($entity)) { - throw new ServiceCreationException(sprintf( - interface_exists($entity) - ? "Interface %s can not be used as 'create' or 'factory', did you mean 'implement'?" - : "Class '%s' not found.", - $entity, - )); - } - - return $entity; - } - - return null; - } - - public function completeDefinition(Definition $def): void { - $this->currentService = in_array($def, $this->builder->getDefinitions(), strict: true) - ? $def - : null; - $this->currentServiceType = $def->getType(); - $this->currentServiceAllowed = false; - try { - $def->complete($this); - + $def->complete($this->withCurrentService($def)); $this->addDependency(new \ReflectionClass($def->getType())); } catch (\Throwable $e) { throw $this->completeException($e, $def); - - } finally { - $this->currentService = $this->currentServiceType = null; - } - } - - - public function completeStatement(Statement $statement, bool $currentServiceAllowed = false): Statement - { - $this->currentServiceAllowed = $currentServiceAllowed; - $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], $this->currentServiceAllowed); - } - 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], $this->currentServiceAllowed); - // 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 Reference - ? $this->resolveReferenceType($entity[0]) - : $this->resolveEntityType($entity[0] instanceof Statement ? $entity[0] : new Statement($entity[0])) - ) { - $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, $this->currentServiceAllowed); - } - } 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] */ - private 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); } } - public function resolveReference(Reference $ref): Definition - { - return $ref->isSelf() - ? $this->currentService - : $this->builder->getDefinition($ref->getValue()); - } - - /** * Returns named reference to service resolved by type (or 'self' reference for local-autowiring). * @throws ServiceCreationException when multiple found * @throws MissingServiceException when not found */ - public function getByType(string $type): Reference + public function getByType(string $type, ?string $tag = null): Reference { if ( $this->currentService && $this->currentServiceAllowed && is_a($this->currentServiceType, $type, allow_string: true) + && $tag === null ) { return new Reference(Reference::Self); } - $name = $this->builder->getByType($type, throw: true); + $name = $this->builder->getByTypeAndTag($type, $tag, true); if ( !$this->currentServiceAllowed && $this->currentService === $this->builder->getDefinition($name) @@ -422,7 +131,7 @@ public function getByType(string $type): Reference throw new MissingServiceException; } - return new Reference($name); + return new Reference($name, $tag); } @@ -436,7 +145,8 @@ public function addDependency(\ReflectionClass|\ReflectionFunctionAbstract|strin } - private function completeException(\Throwable $e, Definition $def): ServiceCreationException + /** @internal */ + public function completeException(\Throwable $e, Definition $def): ServiceCreationException { if ($e instanceof ServiceCreationException && str_starts_with($e->getMessage(), "Service '")) { return $e; @@ -464,7 +174,8 @@ private function completeException(\Throwable $e, Definition $def): ServiceCreat } - private function entityToString($entity): string + /** @internal */ + public function entityToString($entity): string { $referenceToText = fn(Reference $ref): string => $ref->isSelf() && $this->currentService ? '@' . $this->currentService->getName() @@ -491,23 +202,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($this->resolveReferenceType(new Reference($pair[0])) . '::' . $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); } @@ -654,10 +354,50 @@ private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\ } - /** @internal */ - public static function getFirstClassCallable(): array + /** @deprecated */ + public function resolveReferenceType(Reference $ref): ?string + { + return $ref->resolveType($this); + } + + + /** @deprecated */ + public function resolveEntityType(Statement $statement): ?string + { + return $statement->resolveType($this); + } + + + /** @deprecated */ + public function resolveReference(Reference $ref): Definition + { + return $ref->isSelf() + ? $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 { - static $x = [new Nette\PhpGenerator\Literal('...')]; - return $x; + return (new Statement(null, $arguments))->completeArguments($this, $arguments); } } diff --git a/src/DI/exceptions.php b/src/DI/exceptions.php index 83fcb526d..9f13a87d3 100644 --- a/src/DI/exceptions.php +++ b/src/DI/exceptions.php @@ -13,7 +13,7 @@ /** - * Service not found exception. + * The requested service was not found in the container. */ class MissingServiceException extends Nette\InvalidStateException { @@ -21,7 +21,7 @@ class MissingServiceException extends Nette\InvalidStateException /** - * Service creation exception. + * Failed to create the service instance. */ class ServiceCreationException extends Nette\InvalidStateException { @@ -34,7 +34,7 @@ public function setMessage(string $message): static /** - * Not allowed when container is resolving. + * Operation is not allowed while container is resolving dependencies. */ class NotAllowedDuringResolvingException extends Nette\InvalidStateException { @@ -42,7 +42,7 @@ class NotAllowedDuringResolvingException extends Nette\InvalidStateException /** - * Error in configuration. + * The DI container configuration is invalid. */ class InvalidConfigurationException extends Nette\InvalidStateException { diff --git a/tests/DI/Compiler.addExtension.phpt b/tests/DI/Compiler.addExtension.phpt index e27c1d1f7..071642abf 100644 --- a/tests/DI/Compiler.addExtension.phpt +++ b/tests/DI/Compiler.addExtension.phpt @@ -21,21 +21,21 @@ class FooExtension extends DI\CompilerExtension } -testException('', function () { +testException('adding extension during loadConfiguration triggers deprecation', function () { $compiler = new DI\Compiler; $compiler->addExtension('foo', new FooExtension); $container = createContainer($compiler); }, Nette\DeprecatedException::class, "Extensions 'bar' were added while container was being compiled."); -testException('', function () { +testException('duplicate extension name throws error', function () { $compiler = new DI\Compiler; $compiler->addExtension('foo', new FooExtension); $compiler->addExtension('foo', new FooExtension); }, Nette\InvalidArgumentException::class, "Name 'foo' is already used or reserved."); -testException('', function () { +testException('extension name conflict due to case-insensitivity', function () { $compiler = new DI\Compiler; $compiler->addExtension('foo', new FooExtension); $compiler->addExtension('Foo', new FooExtension); diff --git a/tests/DI/Compiler.first-class-callable.phpt b/tests/DI/Compiler.first-class-callable.phpt index c2601dff0..bc8ab2966 100644 --- a/tests/DI/Compiler.first-class-callable.phpt +++ b/tests/DI/Compiler.first-class-callable.phpt @@ -28,7 +28,7 @@ class Service test('Valid callables', function () { $config = ' services: - - Service( Service::foo(...), @a::foo(...), ::trim(...) ) + - Service( Service::foo(...), @a::b()::foo(...), ::trim(...) ) a: stdClass '; $loader = new DI\Config\Loader; @@ -36,7 +36,7 @@ test('Valid callables', function () { $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); $code = $compiler->compile(); - Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->foo(...), trim(...));', $code); + Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->b()->foo(...), trim(...));', $code); }); @@ -50,7 +50,7 @@ Assert::exception(function () { $compiler = new DI\Compiler; $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); $compiler->compile(); -}, Nette\DI\ServiceCreationException::class, 'Service of type Closure: Cannot create closure for Service(...)'); +}, Nette\DI\InvalidConfigurationException::class, "Cannot create closure for 'Service' in config file (used in %a%)"); // Invalid callable 2 @@ -63,4 +63,4 @@ Assert::exception(function () { $compiler = new DI\Compiler; $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); $compiler->compile(); -}, Nette\DI\ServiceCreationException::class, 'Service of type Service: Cannot create closure for Service(...) (used in Service::__construct())'); +}, Nette\DI\InvalidConfigurationException::class, "Cannot create closure for 'Service' in config file (used in %a%)"); diff --git a/tests/DI/Compiler.referenceBug.phpt b/tests/DI/Compiler.referenceBug.phpt deleted file mode 100644 index 4351067ae..000000000 --- a/tests/DI/Compiler.referenceBug.phpt +++ /dev/null @@ -1,41 +0,0 @@ -args = func_get_args(); - } -} - - -$container = createContainer(new DI\Compiler, ' -services: - - stdClass - a: Lorem(x: true) - b: Lorem(x: Lorem(x: true)) - c: Lorem("@test") -'); - - -Assert::same(['@foo', '@@foo', '@\stdClass', true], $container->getService('a')->args); -Assert::equal(['@foo', '@@foo', '@\stdClass', new Lorem('@foo', '@@foo', '@\stdClass', true)], $container->getService('b')->args); -Assert::same(['@test'], $container->getService('c')->args); diff --git a/tests/DI/ContainerBuilder.autowiring.types.phpt b/tests/DI/ContainerBuilder.autowiring.types.phpt index f14f11531..66e3b1f3e 100644 --- a/tests/DI/ContainerBuilder.autowiring.types.phpt +++ b/tests/DI/ContainerBuilder.autowiring.types.phpt @@ -30,7 +30,7 @@ class Bar extends Foo implements IBar } -test('Autowiring limited to Bar class and its subclasses', function () { +test('autowire using self type only', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -43,7 +43,7 @@ test('Autowiring limited to Bar class and its subclasses', function () { }); -test('Autowiring limited to Bar class via self', function () { +test('autowire with "self" keyword works correctly', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -56,7 +56,7 @@ test('Autowiring limited to Bar class via self', function () { }); -test('Autowiring limited to IBar interface and its implementations', function () { +test('autowire via interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -69,7 +69,7 @@ test('Autowiring limited to IBar interface and its implementations', function () }); -test('Autowiring limited to Foo class and its subclasses', function () { +test('autowire via parent class returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -82,7 +82,7 @@ test('Autowiring limited to Foo class and its subclasses', function () { }); -test('Autowiring limited to IFoo interface and its implementations', function () { +test('autowire using implemented interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -95,7 +95,7 @@ test('Autowiring limited to IFoo interface and its implementations', function () }); -test('Autowiring limited to two interfaces', function () { +test('autowire with multiple types registers for all', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -108,7 +108,7 @@ test('Autowiring limited to two interfaces', function () { }); -test('Autowiring limited to two classes', function () { +test('autowire with redundant types excludes mismatches', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -121,7 +121,7 @@ test('Autowiring limited to two classes', function () { }); -test('Autowiring limited to class and interface', function () { +test('autowire with parent and interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -134,7 +134,7 @@ test('Autowiring limited to class and interface', function () { }); -test('Autowiring limited to class and interface', function () { +test('autowire with self and interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -147,7 +147,7 @@ test('Autowiring limited to class and interface', function () { }); -test('Distribution between two services with parent-child relation', function () { +test('separate definitions for parent and self types', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -164,7 +164,7 @@ test('Distribution between two services with parent-child relation', function () }); -test('Distribution between two services of same type', function () { +test('prefer autowired service when multiple exist', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one') ->setType(stdClass::class); @@ -177,7 +177,7 @@ test('Distribution between two services of same type', function () { }); -test('', function () { +test('autowire with override of secondary definition', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -194,7 +194,7 @@ test('', function () { }); -test('', function () { +test('ambiguous autowiring throws exception for multiple services', function () { $builder = new DI\ContainerBuilder; $bar = $builder->addDefinition('bar') ->setType(Bar::class) @@ -221,7 +221,7 @@ test('', function () { }); -test('', function () { +test('incompatible autowired type triggers exception', function () { $builder = new DI\ContainerBuilder; $bar = $builder->addDefinition('bar') ->setType(Foo::class) diff --git a/tests/DI/ContainerBuilder.create.error.phpt b/tests/DI/ContainerBuilder.create.error.phpt index 0af01c12c..6d4ba9a5a 100644 --- a/tests/DI/ContainerBuilder.create.error.phpt +++ b/tests/DI/ContainerBuilder.create.error.phpt @@ -14,20 +14,20 @@ use Nette\DI\Definitions\Statement; require __DIR__ . '/../bootstrap.php'; -testException('', function () { +testException('non-existent class in type causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setType('X')->setCreator('Unknown'); }, Nette\InvalidArgumentException::class, "Service 'one': Class or interface 'X' not found."); -testException('', function () { +testException('missing class in creator triggers service creation error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition(null)->setCreator('Unknown'); $builder->complete(); }, Nette\DI\ServiceCreationException::class, "Service (Unknown::__construct()): Class 'Unknown' not found."); -testException('', function () { +testException('undefined class in dependency throws error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('@two'); $builder->addDefinition('two')->setCreator('Unknown'); @@ -35,7 +35,7 @@ testException('', function () { }, Nette\InvalidStateException::class, "Service 'two': Class 'Unknown' not found."); -testException('', function () { +testException('reference to undefined class in dependency causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator(new Reference('two')); $builder->addDefinition('two')->setCreator('Unknown'); @@ -43,21 +43,21 @@ testException('', function () { }, Nette\InvalidStateException::class, "Service 'two': Class 'Unknown' not found."); -testException('', function () { +testException('non-callable method in creator causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('stdClass::foo'); $builder->complete(); }, Nette\InvalidStateException::class, "Service 'one': Method stdClass::foo() is not callable."); -testException('', function () { +testException('uncallable magic method in creator triggers error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Nette\DI\Container::foo'); // has __magic $builder->complete(); }, Nette\InvalidStateException::class, "Service 'one': Method Nette\\DI\\Container::foo() is not callable."); -testException('', function () { +testException('non-existent interface in factory definition causes error', function () { $builder = new DI\ContainerBuilder; $builder->addFactoryDefinition('one') ->setImplement('Unknown'); @@ -70,7 +70,7 @@ interface Bad4 public function create(); } -testException('', function () { +testException('undeclared return type in factory interface triggers error', function () { $builder = new DI\ContainerBuilder; $builder->addFactoryDefinition('one') ->setImplement(Bad4::class); @@ -82,7 +82,7 @@ interface Bad5 public function get($arg); } -testException('', function () { +testException('method with parameters in accessor interface causes error', function () { $builder = new DI\ContainerBuilder; $builder->addAccessorDefinition('one') ->setImplement(Bad5::class); @@ -97,7 +97,7 @@ class Bad6 } } -testException('', function () { +testException('non-callable factory method due to protection level', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Bad6::create'); $builder->complete(); @@ -111,7 +111,7 @@ class Bad7 } } -testException('', function () { +testException('factory method without return type causes unknown service type error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Bad7::create'); $builder->complete(); @@ -125,7 +125,7 @@ class Bad8 } } -testException('', function () { +testException('private constructor in service type causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setType(Bad8::class); $builder->complete(); @@ -139,13 +139,13 @@ class Good } } -testException('fail in argument', function () { +testException('unknown class in constructor argument triggers error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator(Good::class, [new Statement('Unknown')]); $builder->complete(); }, Nette\InvalidStateException::class, "Service 'one' (type of Good): Class 'Unknown' not found. (used in Good::__construct())"); -testException('fail in argument', function () { +testException('private constructor in argument service causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator(Good::class, [new Statement(Bad8::class)]); $builder->complete(); @@ -173,7 +173,7 @@ trait Bad10 } } -testException('trait cannot be instantiated', function () { +testException('trait method is not callable as service creator', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Bad10::method'); $builder->complete(); @@ -194,7 +194,7 @@ class MethodParam } } -testException('autowiring fail', function () { +testException('ambiguous constructor dependency triggers multiple services error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -204,7 +204,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of ConstructorParam): Multiple services of type stdClass found: a, b (required by \$x in ConstructorParam::__construct())"); -testException('forced autowiring fail', function () { +testException('ambiguous constructor dependency via argument reference', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -214,7 +214,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of ConstructorParam): Multiple services of type stdClass found: a, b (used in ConstructorParam::__construct())"); -testException('autowiring fail in chain', function () { +testException('ambiguous method parameter dependency triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -224,7 +224,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of MethodParam): Multiple services of type stdClass found: a, b (required by \$x in MethodParam::foo())"); -testException('forced autowiring fail in chain', function () { +testException('ambiguous dependency in method call triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -234,7 +234,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of MethodParam): Multiple services of type stdClass found: a, b (used in foo())"); -testException('autowiring fail in argument', function () { +testException('multiple services in constructor dependency cause ambiguity', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -244,7 +244,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (required by \$x in ConstructorParam::__construct()) (used in Good::__construct())"); -testException('forced autowiring fail in argument', function () { +testException('ambiguous dependency in constructor argument triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -254,7 +254,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in ConstructorParam::__construct())"); -testException('autowiring fail in chain in argument', function () { +testException('ambiguous dependency in method parameter causes error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -264,7 +264,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (required by \$x in MethodParam::foo()) (used in Good::__construct())"); -testException('forced autowiring fail in chain in argument', function () { +testException('ambiguous dependency in method call triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -274,7 +274,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in foo())"); -testException('forced autowiring fail in property passing', function () { +testException('ambiguous dependency in property setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -287,7 +287,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in @bad::\$a)"); -testException('autowiring fail in rich property passing', function () { +testException('ambiguous dependency in method setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -300,7 +300,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in foo())"); -testException('autowiring fail in method calling', function () { +testException('ambiguous dependency in method call during setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -313,7 +313,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of MethodParam): Multiple services of type stdClass found: a, b (required by \$x in MethodParam::foo())"); -testException('forced autowiring fail in method calling', function () { +testException('ambiguous dependency in method call on service triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -326,7 +326,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in @bad::bar())"); -testException('autowiring fail in rich method calling', function () { +testException('ambiguous dependency in method call setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass diff --git a/tests/DI/InjectExtension.basic.phpt b/tests/DI/InjectExtension.basic.phpt index 1b6d3fb52..939125010 100644 --- a/tests/DI/InjectExtension.basic.phpt +++ b/tests/DI/InjectExtension.basic.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\DI; +use Nette\DI\Attributes\Inject; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Tester\Assert; @@ -34,6 +35,9 @@ class ParentClass /** @var stdClass @inject */ public $a; + #[Inject(tag: 'tag')] + public stdClass $aTag; + public function injectA() { @@ -91,7 +95,14 @@ extensions: ext: LastExtension services: - std: stdClass + std: + create: stdClass + tags: + - default + stdTag: + create: stdClass + tags: + - tag a: ConcreteDependencyA b: ConcreteDependencyB two: @@ -113,6 +124,7 @@ Assert::equal([ new Statement([new Reference('self'), 'injectD']), new Statement([new Reference('self'), '$e'], [new Reference('a')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), + new Statement([new Reference('self'), '$aTag'], [new Reference('stdTag', 'tag')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), ], $builder->getDefinition('last.one')->getSetup()); @@ -123,6 +135,7 @@ Assert::equal([ new Statement([new Reference('self'), 'injectD']), new Statement([new Reference('self'), '$e'], [new Reference('a')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), + new Statement([new Reference('self'), '$aTag'], [new Reference('stdTag', 'tag')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), ], $builder->getDefinition('ext.one')->getSetup()); @@ -133,5 +146,6 @@ Assert::equal([ new Statement([new Reference('self'), 'injectD']), new Statement([new Reference('self'), '$e'], [new Reference('b')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), + new Statement([new Reference('self'), '$aTag'], [new Reference('stdTag', 'tag')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), ], $builder->getDefinition('two')->getSetup()); diff --git a/tests/DI/InjectExtension.errors.phpt b/tests/DI/InjectExtension.errors.phpt index e2dc16c88..d38b0b8c0 100644 --- a/tests/DI/InjectExtension.errors.phpt +++ b/tests/DI/InjectExtension.errors.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\DI; +use Nette\DI\Attributes\Inject; use Nette\InvalidStateException; use Tester\Assert; @@ -14,6 +15,11 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +class Known +{ +} + + class ServiceA { /** @var DateTimeImmutable @inject */ @@ -28,6 +34,23 @@ class ServiceB } +class ServiceB2 +{ + #[Inject(tag: 'test')] + public Known $a; +} + + +class ServiceB3 +{ + public function __construct( + #[Inject] + private Known $a, + ) { + } +} + + class ServiceC { /** @inject */ @@ -74,6 +97,32 @@ services: Check the type of property ServiceB::\$a."); +Assert::exception(function () { + $compiler = new DI\Compiler; + $compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); + createContainer($compiler, ' +services: + known: Known + service: + create: ServiceB2 + inject: yes +'); +}, InvalidStateException::class, "Service of type Known with tag 'test' required by ServiceB2::\$a not found. Did you add it to configuration file?"); + + +Assert::exception(function () { + $compiler = new DI\Compiler; + $compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); + createContainer($compiler, ' +services: + known: Known + service: + create: ServiceB3 + inject: yes +'); +}, InvalidStateException::class, 'Attribute #[Inject] on parameter $a in ServiceB3::__construct() is redundant.'); + + Assert::exception(function () { $compiler = new DI\Compiler; $compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); diff --git a/tests/DI/InjectExtension.getInjectProperties().php74.phpt b/tests/DI/InjectExtension.getInjectProperties().php74.phpt deleted file mode 100644 index 33a5cb95e..000000000 --- a/tests/DI/InjectExtension.getInjectProperties().php74.phpt +++ /dev/null @@ -1,39 +0,0 @@ - A\AInjected::class, - 'varC' => A\AInjected::class, - ], InjectExtension::getInjectProperties(A\AClass::class)); -} diff --git a/tests/DI/InjectExtension.getInjectProperties().php80.phpt b/tests/DI/InjectExtension.getInjectProperties().php80.phpt deleted file mode 100644 index e24155f65..000000000 --- a/tests/DI/InjectExtension.getInjectProperties().php80.phpt +++ /dev/null @@ -1,38 +0,0 @@ - InjectExtension::getInjectProperties(AClass::class), - Nette\InvalidStateException::class, - "Type of property AClass::\$var is expected to not be nullable/built-in/complex, 'AClass|stdClass' given.", -); - -Assert::same([ - 'varA' => 'stdClass', -], InjectExtension::getInjectProperties(EClass::class)); diff --git a/tests/DI/InjectExtension.getInjectProperties().phpt b/tests/DI/InjectExtension.getInjectProperties().phpt index 79c00b86e..2be133e9e 100644 --- a/tests/DI/InjectExtension.getInjectProperties().phpt +++ b/tests/DI/InjectExtension.getInjectProperties().phpt @@ -10,14 +10,14 @@ namespace A { class AClass { - /** @var AInjected @inject */ - public $varA; + /** @var Different @inject */ + public AInjected $varA; /** @var B\BInjected @inject */ public $varB; - /** @var AInjected @inject */ - public $varC; + /** @inject */ + public AInjected $varC; /** @var AInjected */ public $varD; @@ -26,16 +26,23 @@ namespace A class AInjected { } + + class BadClass + { + /** @inject */ + public AClass|\stdClass $var; + } } namespace A\B { use A; + use Nette\DI\Attributes\Inject; class BClass extends A\AClass { - /** @var BInjected @inject */ - public $varF; + #[Inject] + public BInjected $varF; } class BInjected @@ -77,22 +84,61 @@ namespace { Assert::same([ - 'varA' => A\AInjected::class, - 'varB' => A\B\BInjected::class, - 'varC' => A\AInjected::class, + 'varA' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varB' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], + 'varC' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], ], InjectExtension::getInjectProperties(A\AClass::class)); Assert::same([ - 'varA' => A\AInjected::class, - 'varB' => A\B\BInjected::class, - 'varC' => A\AInjected::class, - 'varF' => A\B\BInjected::class, + 'varA' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varB' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], + 'varC' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varF' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], ], InjectExtension::getInjectProperties(A\B\BClass::class)); Assert::same([ - 'var1' => A\AInjected::class, - 'var2' => A\B\BInjected::class, - 'var3' => C\CInjected::class, - 'var4' => C\CInjected::class, + 'var1' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'var2' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], + 'var3' => [ + 'type' => C\CInjected::class, + 'tag' => null, + ], + 'var4' => [ + 'type' => C\CInjected::class, + 'tag' => null, + ], ], InjectExtension::getInjectProperties(C\CClass::class)); + + Assert::exception( + fn() => InjectExtension::getInjectProperties(A\BadClass::class), + Nette\InvalidStateException::class, + "Type of property A\\BadClass::\$var is expected to not be nullable/built-in/complex, 'A\\AClass|stdClass' given.", + ); } diff --git a/tests/DI/InjectExtension.getInjectProperties().traits.phpt b/tests/DI/InjectExtension.getInjectProperties().traits.phpt index 1fc6c211e..5059354aa 100644 --- a/tests/DI/InjectExtension.getInjectProperties().traits.phpt +++ b/tests/DI/InjectExtension.getInjectProperties().traits.phpt @@ -16,11 +16,15 @@ namespace A namespace B { use A\AInjected; + use Nette\DI\Attributes\Inject; trait BTrait { - /** @var AInjected @inject */ - public $varA; + #[Inject] + public AInjected $varA; + + #[Inject(tag: 'tagB')] + public AInjected $varB; } } @@ -42,6 +46,13 @@ namespace { Assert::same([ - 'varA' => A\AInjected::class, + 'varA' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varB' => [ + 'type' => A\AInjected::class, + 'tag' => 'tagB', + ], ], InjectExtension::getInjectProperties(C\CClass::class)); } diff --git a/tests/DI/InjectExtension.tags.phpt b/tests/DI/InjectExtension.tags.phpt new file mode 100644 index 000000000..e3bb4c1fe --- /dev/null +++ b/tests/DI/InjectExtension.tags.phpt @@ -0,0 +1,223 @@ +injectedA = $injectedA; + $this->injectedB = $injectedB; + } + + + public function getInjectedA(): Dependency + { + return $this->injectedA; + } + + + public function getInjectedB(): Dependency + { + return $this->injectedB; + } + + + public function getPrivateA(): Dependency + { + return $this->privateA; + } + + + public function getPrivateB(): Dependency + { + return $this->privateB; + } +} + + +$compiler = new DI\Compiler; +$compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); +$container = createContainer($compiler, ' +services: + a: + create: DependencyA + tags: + - default + b: + create: DependencyB + tags: + - alt + c: + create: DependencyA + tags: + - duplicate + service: + create: Service + inject: true +'); + + +$builder = $compiler->getContainerBuilder(); + +Assert::same( + $builder->getByType(Dependency::class), + 'a', +); + +Assert::same( + $builder->getByTypeAndTag(Dependency::class, tag: 'alt'), + 'b', +); + +Assert::same( + $builder->getByTypeAndTag(Dependency::class, tag: 'duplicate'), + 'c', +); + +Assert::same( + $builder->getByTypeAndTag(Dependency::class, tag: 'default'), + 'a', +); + + +Assert::equal( + $container->getByType(Service::class)->dependencyA, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyADuplicate, + new DependencyA, +); + +Assert::notSame( + $container->getByType(Service::class)->dependencyA, + $container->getByType(Service::class)->dependencyADuplicate, +); + +Assert::same( + $container->getByType(Service::class)->dependencyA, + $container->getByType(Service::class)->dependencyC, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyB, + new DependencyB, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyC, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyD, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyE, + new DependencyB, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyF, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->getInjectedA(), + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->getInjectedB(), + new DependencyB, +); + +Assert::equal( + $container->getByType(Service::class)->getPrivateA(), + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->getPrivateB(), + new DependencyB, +); + + +Assert::equal( + $container->getByType(Dependency::class), + new DependencyA, +); + +Assert::equal( + $container->getByTypeAndTag(Dependency::class, tag: 'alt'), + new DependencyB, +); + +Assert::equal( + $container->getByTypeAndTag(Dependency::class, tag: 'default'), + new DependencyA, +); + +Assert::same( + $container->findByType(Dependency::class), + ['a', 'b', 'c'], +);