From 114b2ba9db2262a96c002656ce72523ff581df39 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 28 Feb 2023 15:28:36 +0100 Subject: [PATCH] added support for first-class callable syntax in NEON --- src/DI/Config/Adapters/NeonAdapter.php | 15 ++++- src/DI/Resolver.php | 22 ++++++- tests/DI/Compiler.first-class-callable.phpt | 66 +++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 tests/DI/Compiler.first-class-callable.phpt diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index 5e1302906..61afdfc2d 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -39,6 +39,7 @@ 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->removeUnderscoreVisitor(...)); $node = $traverser->traverse($node, $this->convertAtSignVisitor(...)); $node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...)); @@ -148,6 +149,19 @@ function (&$val): void { } + private function firstClassCallableVisitor(Neon\Node $node) + { + if ($node instanceof Neon\Node\EntityNode + && count($node->attributes) === 1 + && $node->attributes[0]->key === null + && $node->attributes[0]->value instanceof Neon\Node\LiteralNode + && $node->attributes[0]->value->value === '...' + ) { + $node->attributes[0]->value->value = Nette\DI\Resolver::getFirstClassCallable()[0]; + } + } + + private function removeUnderscoreVisitor(Neon\Node $node) { if (!$node instanceof Neon\Node\EntityNode) { @@ -163,7 +177,6 @@ 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 diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index afd4e7c77..743c81880 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -93,7 +93,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]) { @@ -186,6 +189,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) && str_contains($entity, '?'): // PHP literal break; @@ -653,4 +665,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())');