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: = Tracy\Helpers::editorLink($file) ?>
+
+
+
+
+
+ | Name |
+ Autowired |
+ Service |
+ Tags |
+
+
+
+ $type): ?>
+ |
+
+ = Tracy\Helpers::escapeHtml($name) ?>
+
+= Tracy\Helpers::escapeHtml($name) ?>
+
+
+ |
+
+ = Tracy\Helpers::escapeHtml($autowired ? 'yes' : (isset($wiring[$type]) ? 'no' : '?')) ?>
+
+ |
+
+ = Dumper::toHtml($instances[$name], [Dumper::COLLAPSE => true, Dumper::LIVE => true, Dumper::DEPTH => 5]) ?>
+
+ = Tracy\Helpers::escapeHtml(get_class($instances[$name])) ?>
+
+ = Tracy\Helpers::escapeHtml($type) ?>
+
+ |
+
+ = Tracy\Helpers::escapeHtml(key($tags[$name])) ?>
+ = = Dumper::toHtml(current($tags[$name]), [Dumper::COLLAPSE => true]) ?>
+
+ = Dumper::toHtml($tags[$name], [Dumper::COLLAPSE => true]) ?>
+
+ |
+
+
+
+
+
Parameters
+
+
+ disabled via 'di › export › parameters'
+ = Dumper::toHtml($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 @@
+
+
+ = Tracy\Helpers::escapeHtml($elapsedTime ? sprintf('%0.1f ms', $elapsedTime * 1000) : '') ?>
+
+
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)}
+
+
+
+
+ | Name |
+ Autowired |
+ Service |
+ Tags |
+
+
+
+ {foreach $services as $name => $type}
+ {do $name = (string) $name}
+ {do $autowired = in_array($name, array_merge($wiring[$type][0] ?? [], $wiring[$type][1] ?? []), strict: true)}
+
+ |
+ {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}
+ |
+
+ {/foreach}
+
+
+
+
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: = Helpers::editorLink($file) ?>
-
-
-
-
- | Name |
- Autowired |
- Service |
- Tags |
-
-
-
- $type): ?>
-
-
-
- | = is_numeric($name) ? "$name" : Helpers::escapeHtml($name) ?> |
- = $autowired ? 'yes' : (isset($wiring[$type]) ? 'no' : '?') ?> |
-
-
- = Dumper::toHtml($instances[$name], [Dumper::COLLAPSE => true, Dumper::LIVE => true, Dumper::DEPTH => 5]); ?>
-
- = get_class($instances[$name]) ?>
-
- = Helpers::escapeHtml($type) ?>
-
- |
- true])
- : Dumper::toHtml($tags[$name], [Dumper::COLLAPSE => true]);
- } ?> |
-
-
-
-
-
-
Parameters
-
-
- = $parameters === null ? "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 @@
-
-
-= $elapsedTime ? sprintf('%0.1f ms', $elapsedTime * 1000) : '' ?>
-
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'],
+);