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 Dec 2, 2024
1 parent c845091 commit 63ee9e4
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 41 deletions.
45 changes: 29 additions & 16 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 @@ -41,7 +42,6 @@ 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(...));
Expand Down Expand Up @@ -113,19 +113,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\ArrayNode) {
Expand Down Expand Up @@ -174,14 +161,27 @@ 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();
if (is_string($entity->value) && str_contains($entity->value, '?')) {
throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')");
} elseif ($chain) {
$entity->value = [$this->buildExpression($chain), ltrim($entity->value, ':')];
} elseif (is_string($entity->value)) {
$entity->value = (new Statement($entity->value))->entity;
}

if ($this->isFirstClassCallable($node)) {
if (is_array($entity->value)) {
if ($entity->value[0] === '') {
return new Definitions\FunctionCallable($entity->value[1]);
}
return new Definitions\MethodCallable(...$entity->value);
} else {
throw new Nette\DI\InvalidConfigurationException("Cannot create closure for '$entity->value' in config file (used in '$this->file')");
}
}

return new Statement(
Expand All @@ -191,6 +191,15 @@ private function buildExpression(array $chain): Statement
}


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) {
Expand All @@ -209,7 +218,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 @@ -353,14 +353,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 63ee9e4

Please sign in to comment.