Skip to content

Commit ae91921

Browse files
committed
added FunctionCallable & MethodCallable, expressions representing first-class callables
1 parent d0a24e3 commit ae91921

File tree

6 files changed

+133
-42
lines changed

6 files changed

+133
-42
lines changed

src/DI/Config/Adapters/NeonAdapter.php

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Nette;
1313
use Nette\DI;
14+
use Nette\DI\Definitions;
1415
use Nette\DI\Definitions\Reference;
1516
use Nette\DI\Definitions\Statement;
1617
use Nette\Neon;
@@ -43,7 +44,6 @@ public function load(string $file): array
4344
$node = $decoder->parseToNode($input);
4445
$traverser = new Neon\Traverser;
4546
$node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...));
46-
$node = $traverser->traverse($node, $this->firstClassCallableVisitor(...));
4747
$node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...));
4848
$node = $traverser->traverse($node, $this->convertAtSignVisitor(...));
4949
$node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...));
@@ -115,19 +115,6 @@ function (&$val): void {
115115
}
116116

117117

118-
private function firstClassCallableVisitor(Node $node): void
119-
{
120-
if ($node instanceof Node\EntityNode
121-
&& count($node->attributes) === 1
122-
&& $node->attributes[0]->key === null
123-
&& $node->attributes[0]->value instanceof Node\LiteralNode
124-
&& $node->attributes[0]->value->value === '...'
125-
) {
126-
$node->attributes[0]->value->value = Nette\DI\Resolver::getFirstClassCallable()[0];
127-
}
128-
}
129-
130-
131118
private function preventMergingVisitor(Node $node): void
132119
{
133120
if ($node instanceof Node\ArrayItemNode
@@ -182,14 +169,37 @@ private function entityToExpressionVisitor(Node $node): Node
182169
}
183170

184171

185-
private function buildExpression(array $chain): Statement
172+
private function buildExpression(array $chain): Definitions\Expression
186173
{
187174
$node = array_pop($chain);
188175
$entity = $node->toValue();
189-
return new Statement(
176+
$stmt = new Statement(
190177
$chain ? [$this->buildExpression($chain), ltrim($entity->value, ':')] : $entity->value,
191178
$entity->attributes,
192179
);
180+
181+
if ($this->isFirstClassCallable($node)) {
182+
$entity = $stmt->getEntity();
183+
if (is_array($entity)) {
184+
if ($entity[0] === '') {
185+
return new Definitions\FunctionCallable($entity[1]);
186+
}
187+
return new Definitions\MethodCallable(...$entity);
188+
} else {
189+
throw new Nette\DI\InvalidConfigurationException("Cannot create closure for '$entity' in config file (used in '$this->file')");
190+
}
191+
}
192+
193+
return $stmt;
194+
}
195+
196+
197+
private function isFirstClassCallable(Node\EntityNode $node): bool
198+
{
199+
return array_keys($node->attributes) === [0]
200+
&& $node->attributes[0]->key === null
201+
&& $node->attributes[0]->value instanceof Node\LiteralNode
202+
&& $node->attributes[0]->value->value === '...';
193203
}
194204

195205

@@ -211,7 +221,11 @@ private function removeUnderscoreVisitor(Node $node): void
211221
unset($node->attributes[$i]);
212222
$index = true;
213223

214-
} elseif ($attr->value instanceof Node\LiteralNode && $attr->value->value === '...') {
224+
} elseif (
225+
$attr->value instanceof Node\LiteralNode
226+
&& $attr->value->value === '...'
227+
&& !$this->isFirstClassCallable($node)
228+
) {
215229
trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED);
216230
unset($node->attributes[$i]);
217231
$index = true;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\DI\Definitions;
11+
12+
use Nette;
13+
use Nette\DI\PhpGenerator;
14+
use Nette\DI\Resolver;
15+
use Nette\PhpGenerator as Php;
16+
17+
18+
final class FunctionCallable extends Expression
19+
{
20+
public function __construct(
21+
public string $function,
22+
) {
23+
if (!Php\Helpers::isIdentifier($function)) {
24+
throw new Nette\InvalidArgumentException("Function name '$function' is not valid.");
25+
}
26+
}
27+
28+
29+
public function resolveType(Resolver $resolver): ?string
30+
{
31+
return \Closure::class;
32+
}
33+
34+
35+
public function complete(Resolver $resolver): void
36+
{
37+
}
38+
39+
40+
public function generateCode(PhpGenerator $generator): string
41+
{
42+
return $this->function . '(...)';
43+
}
44+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\DI\Definitions;
11+
12+
use Nette;
13+
use Nette\DI\PhpGenerator;
14+
use Nette\DI\Resolver;
15+
use Nette\PhpGenerator as Php;
16+
17+
18+
final class MethodCallable extends Expression
19+
{
20+
public function __construct(
21+
public Expression|string $objectOrClass,
22+
public string $method,
23+
) {
24+
if (is_string($objectOrClass) && !Php\Helpers::isNamespaceIdentifier($objectOrClass)) {
25+
throw new Nette\InvalidArgumentException("Class name '$objectOrClass' is not valid.");
26+
}
27+
if (!Php\Helpers::isIdentifier($method)) {
28+
throw new Nette\InvalidArgumentException("Method name '$method' is not valid.");
29+
}
30+
}
31+
32+
33+
public function resolveType(Resolver $resolver): ?string
34+
{
35+
return \Closure::class;
36+
}
37+
38+
39+
public function complete(Resolver $resolver): void
40+
{
41+
if ($this->objectOrClass instanceof Expression) {
42+
$this->objectOrClass->complete($resolver);
43+
}
44+
}
45+
46+
47+
public function generateCode(PhpGenerator $generator): string
48+
{
49+
return is_string($this->objectOrClass)
50+
? $generator->formatPhp('?::?(...)', [new Php\Literal($this->objectOrClass), $this->method])
51+
: $generator->formatPhp('?->?(...)', [new Php\Literal($this->objectOrClass->generateCode($generator)), $this->method]);
52+
}
53+
}

src/DI/Definitions/Statement.php

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,7 @@ public function resolveType(Resolver $resolver): ?string
7575
{
7676
$entity = $this->normalizeEntity($resolver);
7777

78-
if ($this->arguments === Resolver::getFirstClassCallable()) {
79-
return \Closure::class;
80-
81-
} elseif (is_array($entity)) {
78+
if (is_array($entity)) {
8279
if ($entity[0] instanceof Expression) {
8380
$entity[0] = $entity[0]->resolveType($resolver);
8481
if (!$entity[0]) {
@@ -146,15 +143,6 @@ public function complete(Resolver $resolver): void
146143
$arguments = $this->arguments;
147144

148145
switch (true) {
149-
case $this->arguments === Resolver::getFirstClassCallable():
150-
if (!is_array($entity) || !Php\Helpers::isIdentifier($entity[1])) {
151-
throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity));
152-
}
153-
if ($entity[0] instanceof self) {
154-
$entity[0]->complete($resolver);
155-
}
156-
break;
157-
158146
case is_string($entity) && str_contains($entity, '?'): // PHP literal
159147
break;
160148

src/DI/Resolver.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -353,14 +353,6 @@ private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\
353353
}
354354

355355

356-
/** @internal */
357-
public static function getFirstClassCallable(): array
358-
{
359-
static $x = [new Nette\PhpGenerator\Literal('...')];
360-
return $x;
361-
}
362-
363-
364356
/** @deprecated */
365357
public function resolveReferenceType(Reference $ref): ?string
366358
{

tests/DI/Compiler.first-class-callable.phpt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ class Service
2828
test('Valid callables', function () {
2929
$config = '
3030
services:
31-
- Service( Service::foo(...), @a::foo(...), ::trim(...) )
31+
- Service( Service::foo(...), @a::b()::foo(...), ::trim(...) )
3232
a: stdClass
3333
';
3434
$loader = new DI\Config\Loader;
3535
$compiler = new DI\Compiler;
3636
$compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon')));
3737
$code = $compiler->compile();
3838

39-
Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->foo(...), trim(...));', $code);
39+
Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->b()->foo(...), trim(...));', $code);
4040
});
4141

4242

@@ -50,7 +50,7 @@ Assert::exception(function () {
5050
$compiler = new DI\Compiler;
5151
$compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon')));
5252
$compiler->compile();
53-
}, Nette\DI\ServiceCreationException::class, 'Service of type Closure: Cannot create closure for Service(...)');
53+
}, Nette\DI\InvalidConfigurationException::class, "Cannot create closure for 'Service' in config file (used in %a%)");
5454

5555

5656
// Invalid callable 2
@@ -63,4 +63,4 @@ Assert::exception(function () {
6363
$compiler = new DI\Compiler;
6464
$compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon')));
6565
$compiler->compile();
66-
}, Nette\DI\ServiceCreationException::class, 'Service of type Service: Cannot create closure for Service(...) (used in Service::__construct())');
66+
}, Nette\DI\InvalidConfigurationException::class, "Cannot create closure for 'Service' in config file (used in %a%)");

0 commit comments

Comments
 (0)