Skip to content

Commit 3d0f86d

Browse files
committed
added FunctionCallable & MethodCallable, expressions representing first-class callables
1 parent 60b1e7c commit 3d0f86d

File tree

6 files changed

+133
-42
lines changed

6 files changed

+133
-42
lines changed

src/DI/Config/Adapters/NeonAdapter.php

+31-17
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;
@@ -42,7 +43,6 @@ public function load(string $file): array
4243
$node = $decoder->parseToNode($input);
4344
$traverser = new Neon\Traverser;
4445
$node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...));
45-
$node = $traverser->traverse($node, $this->firstClassCallableVisitor(...));
4646
$node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...));
4747
$node = $traverser->traverse($node, $this->convertAtSignVisitor(...));
4848
$node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...));
@@ -114,19 +114,6 @@ function (&$val): void {
114114
}
115115

116116

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

183170

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

194204

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

213-
} elseif ($attr->value instanceof Node\LiteralNode && $attr->value->value === '...') {
223+
} elseif (
224+
$attr->value instanceof Node\LiteralNode
225+
&& $attr->value->value === '...'
226+
&& !$this->isFirstClassCallable($node)
227+
) {
214228
trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED);
215229
unset($node->attributes[$i]);
216230
$index = true;
+44
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+
}

src/DI/Definitions/MethodCallable.php

+53
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

+1-13
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ public function resolveType(Resolver $resolver): ?string
7474
{
7575
$entity = $this->normalizeEntity($resolver);
7676

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

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

src/DI/Resolver.php

-8
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,6 @@ private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\
352352
}
353353

354354

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

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

+4-4
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)