diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index b01ccc934..dafc5bc9f 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -42,6 +42,7 @@ public function load(string $file): array $node = $decoder->parseToNode($input); $traverser = new Neon\Traverser; $node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...)); + $node = $traverser->traverse($node, $this->firstClassCallableVisitor(...)); $node = $traverser->traverse($node, $this->convertAtSignVisitor(...)); $node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...)); return $this->process((array) $node->toValue()); @@ -165,11 +166,24 @@ private function removeUnderscoreVisitor(Neon\Node $node) if ($attr->key === null && $attr->value instanceof Neon\Node\LiteralNode && $attr->value->value === '_') { unset($node->attributes[$i]); $index = true; + } + } + } - } elseif ($attr->key === null && $attr->value instanceof Neon\Node\LiteralNode && $attr->value->value === '...') { - trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED); - unset($node->attributes[$i]); - $index = true; + + private function firstClassCallableVisitor(Neon\Node $node) + { + if (!$node instanceof Neon\Node\EntityNode) { + return; + } + + foreach ($node->attributes as $attr) { + if ($attr->key === null && $attr->value instanceof Neon\Node\LiteralNode && $attr->value->value === '...') { + if (count($node->attributes) === 1) { + $attr->value->value = Nette\DI\Resolver::getFirstClassCallable()[0]; + } else { + throw new Nette\DI\InvalidConfigurationException("Replace ... with _ in configuration file '$this->file'."); + } } } } diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index 5e81c7a77..ed1fc39eb 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -95,7 +95,10 @@ public function resolveEntityType(Statement $statement): ?string { $entity = $this->normalizeEntity($statement); - if (is_array($entity)) { + 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]) { @@ -188,6 +191,15 @@ public function completeStatement(Statement $statement, bool $currentServiceAllo : 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) && Strings::contains($entity, '?'): // PHP literal break; @@ -657,4 +669,12 @@ private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\ ? $itemType : null; } + + + /** @internal */ + public static function getFirstClassCallable(): array + { + static $x = [new Nette\PhpGenerator\Literal('...')]; + return $x; + } } diff --git a/tests/DI/Compiler.first-class-callable.phpt b/tests/DI/Compiler.first-class-callable.phpt new file mode 100644 index 000000000..c2601dff0 --- /dev/null +++ b/tests/DI/Compiler.first-class-callable.phpt @@ -0,0 +1,66 @@ +cb = $cb; + } + + + public function foo() + { + } +} + + +test('Valid callables', function () { + $config = ' + services: + - Service( Service::foo(...), @a::foo(...), ::trim(...) ) + a: stdClass + '; + $loader = new DI\Config\Loader; + $compiler = new DI\Compiler; + $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); + $code = $compiler->compile(); + + Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->foo(...), trim(...));', $code); +}); + + +// Invalid callable 1 +Assert::exception(function () { + $config = ' + services: + - Service(...) + '; + $loader = new DI\Config\Loader; + $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(...)'); + + +// Invalid callable 2 +Assert::exception(function () { + $config = ' + services: + - Service( Service(...) ) + '; + $loader = new DI\Config\Loader; + $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())'); diff --git a/tests/DI/NeonAdapter.preprocess.phpt b/tests/DI/NeonAdapter.preprocess.phpt index 89552d5bc..f6ed9653c 100644 --- a/tests/DI/NeonAdapter.preprocess.phpt +++ b/tests/DI/NeonAdapter.preprocess.phpt @@ -71,13 +71,12 @@ Assert::equal( // ... deprecated -$data = @$adapter->load(Tester\FileMock::create(' -- Class(arg1, ..., [...]) -', 'neon')); - -Assert::equal( - [new Statement('Class', ['arg1', 2 => ['...']])], - $data, +Assert::exception( + fn() => $adapter->load(Tester\FileMock::create(' + - Class(arg1, ..., [...]) + ', 'neon')), + Nette\DI\InvalidConfigurationException::class, + 'Replace ... with _ in configuration file %a%', );