Skip to content

Commit

Permalink
added support for first-class callable syntax in NEON
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Dec 21, 2023
1 parent da76dd6 commit 0380c42
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 2 deletions.
15 changes: 14 additions & 1 deletion src/DI/Config/Adapters/NeonAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(...));
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
22 changes: 21 additions & 1 deletion src/DI/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -645,4 +657,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;
}
}
66 changes: 66 additions & 0 deletions tests/DI/Compiler.first-class-callable.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

use Nette\DI;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


class Service
{
public $cb;


public function __construct($cb)
{
$this->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())');

0 comments on commit 0380c42

Please sign in to comment.