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 11, 2023
1 parent 4d20f5f commit 73ec5ce
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 12 deletions.
22 changes: 18 additions & 4 deletions src/DI/Config/Adapters/NeonAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function load(string $file): array
$node = $decoder->parseToNode($input);
$traverser = new Neon\Traverser;
$node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...));
$node = $traverser->traverse($node, $this->firstClassCallableVisitor(...));
$node = $traverser->traverse($node, $this->convertAtSignVisitor(...));
$node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...));
return $this->process((array) $node->toValue());
Expand Down Expand Up @@ -165,11 +166,24 @@ 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 && $attr->value->value === '...') {
trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED);
unset($node->attributes[$i]);
$index = true;

private function firstClassCallableVisitor(Neon\Node $node)
{
if (!$node instanceof Neon\Node\EntityNode) {
return;
}

foreach ($node->attributes as $attr) {
if ($attr->key === null && $attr->value instanceof Neon\Node\LiteralNode && $attr->value->value === '...') {
if (count($node->attributes) === 1) {
$attr->value->value = Nette\DI\Resolver::getFirstClassCallable()[0];
} else {
throw new Nette\DI\InvalidConfigurationException("Replace ... with _ in configuration file '$this->file'.");
}
}
}
}
Expand Down
22 changes: 21 additions & 1 deletion src/DI/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,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 @@ -188,6 +191,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) && Strings::contains($entity, '?'): // PHP literal
break;

Expand Down Expand Up @@ -657,4 +669,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())');
13 changes: 6 additions & 7 deletions tests/DI/NeonAdapter.preprocess.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,12 @@ Assert::equal(


// ... deprecated
$data = @$adapter->load(Tester\FileMock::create('
- Class(arg1, ..., [...])
', 'neon'));

Assert::equal(
[new Statement('Class', ['arg1', 2 => ['...']])],
$data,
Assert::exception(
fn() => $adapter->load(Tester\FileMock::create('
- Class(arg1, ..., [...])
', 'neon')),
Nette\DI\InvalidConfigurationException::class,
'Replace ... with _ in configuration file %a%',
);


Expand Down

0 comments on commit 73ec5ce

Please sign in to comment.