From 7fd81d98af9a1170e26551848805e923a2db7328 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 2 Dec 2024 02:17:53 +0100 Subject: [PATCH] added FunctionCallable & MethodCallable, expressions representing first-class callables --- src/DI/Config/Adapters/NeonAdapter.php | 48 ++++++++++++------- src/DI/Definitions/FunctionCallable.php | 44 +++++++++++++++++ src/DI/Definitions/MethodCallable.php | 53 +++++++++++++++++++++ src/DI/Definitions/Statement.php | 14 +----- src/DI/Resolver.php | 8 ---- tests/DI/Compiler.first-class-callable.phpt | 8 ++-- 6 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 src/DI/Definitions/FunctionCallable.php create mode 100644 src/DI/Definitions/MethodCallable.php diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index 840cae8c1..3252c9f85 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -11,6 +11,7 @@ use Nette; use Nette\DI; +use Nette\DI\Definitions; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\Neon; @@ -42,7 +43,6 @@ public function load(string $file): array $node = $decoder->parseToNode($input); $traverser = new Neon\Traverser; $node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...)); - $node = $traverser->traverse($node, $this->firstClassCallableVisitor(...)); $node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...)); $node = $traverser->traverse($node, $this->convertAtSignVisitor(...)); $node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...)); @@ -114,19 +114,6 @@ function (&$val): void { } - private function firstClassCallableVisitor(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->attributes[0]->value->value = Nette\DI\Resolver::getFirstClassCallable()[0]; - } - } - - private function preventMergingVisitor(Node $node): void { if ($node instanceof Node\ArrayItemNode @@ -181,14 +168,37 @@ private function entityToExpressionVisitor(Node $node): Node } - private function buildExpression(array $chain): Statement + private function buildExpression(array $chain): Definitions\Expression { $node = array_pop($chain); $entity = $node->toValue(); - return new Statement( + $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 === '...'; } @@ -210,7 +220,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; 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/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/Statement.php b/src/DI/Definitions/Statement.php index ba65b1ee2..3c4b526a2 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -74,10 +74,7 @@ public function resolveType(Resolver $resolver): ?string { $entity = $this->normalizeEntity($resolver); - if ($this->arguments === Resolver::getFirstClassCallable()) { - return \Closure::class; - - } elseif (is_array($entity)) { + if (is_array($entity)) { if ($entity[0] instanceof Expression) { $entity[0] = $entity[0]->resolveType($resolver); if (!$entity[0]) { @@ -145,15 +142,6 @@ public function complete(Resolver $resolver): void $arguments = $this->arguments; switch (true) { - case $this->arguments === Resolver::getFirstClassCallable(): - if (!is_array($entity) || !Php\Helpers::isIdentifier($entity[1])) { - throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); - } - if ($entity[0] instanceof self) { - $entity[0]->complete($resolver); - } - break; - case is_string($entity) && str_contains($entity, '?'): // PHP literal break; diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index d39183856..543bb39f9 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -352,14 +352,6 @@ private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\ } - /** @internal */ - public static function getFirstClassCallable(): array - { - static $x = [new Nette\PhpGenerator\Literal('...')]; - return $x; - } - - /** @deprecated */ public function resolveReferenceType(Reference $ref): ?string { 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%)");