Skip to content

Commit

Permalink
added FunctionCallable & MethodCallable, expressions representing fir…
Browse files Browse the repository at this point in the history
…st-class callables
  • Loading branch information
dg committed Jan 10, 2025
1 parent 45ef8bd commit d200095
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 42 deletions.
48 changes: 31 additions & 17 deletions src/DI/Config/Adapters/NeonAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(...));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 === '...';
}


Expand All @@ -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;
Expand Down
44 changes: 44 additions & 0 deletions src/DI/Definitions/FunctionCallable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\DI\Definitions;

use Nette;
use Nette\DI\PhpGenerator;
use Nette\DI\Resolver;
use Nette\PhpGenerator as Php;


final class FunctionCallable extends Expression
{
public function __construct(
public string $function,
) {
if (!Php\Helpers::isIdentifier($function)) {
throw new Nette\InvalidArgumentException("Function name '$function' is not valid.");
}
}


public function resolveType(Resolver $resolver): ?string
{
return \Closure::class;
}


public function complete(Resolver $resolver): void
{
}


public function generateCode(PhpGenerator $generator): string
{
return $this->function . '(...)';
}
}
53 changes: 53 additions & 0 deletions src/DI/Definitions/MethodCallable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\DI\Definitions;

use Nette;
use Nette\DI\PhpGenerator;
use Nette\DI\Resolver;
use Nette\PhpGenerator as Php;


final class MethodCallable extends Expression
{
public function __construct(
public Expression|string $objectOrClass,
public string $method,
) {
if (is_string($objectOrClass) && !Php\Helpers::isNamespaceIdentifier($objectOrClass)) {
throw new Nette\InvalidArgumentException("Class name '$objectOrClass' is not valid.");
}
if (!Php\Helpers::isIdentifier($method)) {
throw new Nette\InvalidArgumentException("Method name '$method' is not valid.");
}
}


public function resolveType(Resolver $resolver): ?string
{
return \Closure::class;
}


public function complete(Resolver $resolver): void
{
if ($this->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]);
}
}
14 changes: 1 addition & 13 deletions src/DI/Definitions/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down Expand Up @@ -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;

Expand Down
8 changes: 0 additions & 8 deletions src/DI/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
8 changes: 4 additions & 4 deletions tests/DI/Compiler.first-class-callable.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ 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;
$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);
Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->b()->foo(...), trim(...));', $code);
});


Expand All @@ -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
Expand All @@ -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%)");

0 comments on commit d200095

Please sign in to comment.