Skip to content

Commit

Permalink
Do not add return in LazyClosure when return type of closure is `…
Browse files Browse the repository at this point in the history
…void`

The LazyClosure was introduced in #49639 and generates PHP code using reflection.

It works, but when the callable has a `void` return type, it would produce code like this:
```php
return $container->services['closure2'] = (new class(fn() => new \Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure {
    public function __invoke(string $name) : void
    {
        return $this->service->__invoke(...\func_get_args());
    }
})->__invoke(...);
```

That `return` statement before calling the `$this->service` is not allowed and causes an error:
```
Compile Error: A void function must not return a value
```
  • Loading branch information
ruudk committed Aug 3, 2023
1 parent 1a15b12 commit c06e574
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 10 deletions.
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\DependencyInjection\Argument;

use ReflectionNamedType;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
Expand Down Expand Up @@ -78,7 +79,8 @@ public static function getCode(string $initializer, array $callable, Definition
throw new RuntimeException("Cannot create lazy closure{$id} because its corresponding callable is invalid.");
}

$code = ProxyHelper::exportSignature($r->getMethod($method), true, $args);
$methodReflector = $r->getMethod($method);
$code = ProxyHelper::exportSignature($methodReflector, true, $args);

if ($asClosure) {
$code = ' { '.preg_replace('/: static$/', ': \\'.$r->name, $code);
Expand All @@ -87,7 +89,7 @@ public static function getCode(string $initializer, array $callable, Definition
}

$code = 'new class('.$initializer.') extends \\'.self::class
.$code.' { return $this->service->'.$callable[1].'('.$args.'); } '
.$code.' { '.($methodReflector->getReturnType() instanceof ReflectionNamedType && $methodReflector->getReturnType()->getName() === 'void' ? '' : 'return ').'$this->service->'.$callable[1].'('.$args.'); } '
.'}';

return $asClosure ? '('.$code.')->'.$method.'(...)' : $code;
Expand Down
Expand Up @@ -48,6 +48,7 @@
use Symfony\Component\DependencyInjection\Tests\Compiler\AInterface;
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
use Symfony\Component\DependencyInjection\Tests\Compiler\FooAnnotation;
use Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid;
use Symfony\Component\DependencyInjection\Tests\Compiler\IInterface;
use Symfony\Component\DependencyInjection\Tests\Compiler\MyCallable;
use Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface;
Expand Down Expand Up @@ -1874,12 +1875,18 @@ public function testAutowireClosure()
public function testLazyClosure()
{
$container = new ContainerBuilder();
$container->register('closure', 'Closure')
$container->register('closure1', 'Closure')
->setPublic('true')
->setFactory(['Closure', 'fromCallable'])
->setLazy(true)
->setArguments([[new Reference('foo'), 'cloneFoo']]);
$container->register('closure2', 'Closure')
->setPublic('true')
->setFactory(['Closure', 'fromCallable'])
->setLazy(true)
->setArguments([[new Reference('foo_void'), '__invoke']]);
$container->register('foo', Foo::class);
$container->register('foo_void', FooVoid::class);
$container->compile();
$dumper = new PhpDumper($container);

Expand All @@ -1890,11 +1897,18 @@ public function testLazyClosure()
$container = new \Symfony_DI_PhpDumper_Test_Lazy_Closure();

$cloned = Foo::$counter;
$this->assertInstanceOf(\Closure::class, $container->get('closure'));
$this->assertInstanceOf(\Closure::class, $container->get('closure1'));
$this->assertSame($cloned, Foo::$counter);
$this->assertInstanceOf(Foo::class, $container->get('closure')());
$this->assertInstanceOf(Foo::class, $container->get('closure1')());
$this->assertSame(1 + $cloned, Foo::$counter);
$this->assertSame(1, (new \ReflectionFunction($container->get('closure')))->getNumberOfParameters());
$this->assertSame(1, (new \ReflectionFunction($container->get('closure1')))->getNumberOfParameters());

$counter = FooVoid::$counter;
$this->assertInstanceOf(\Closure::class, $container->get('closure2'));
$this->assertSame($counter, FooVoid::$counter);
$container->get('closure2')('Hello');
$this->assertSame(1 + $counter, FooVoid::$counter);
$this->assertSame(1, (new \ReflectionFunction($container->get('closure2')))->getNumberOfParameters());
}

public function testLazyAutowireAttribute()
Expand Down
Expand Up @@ -38,6 +38,16 @@ public function cloneFoo(\stdClass $bar = null): static
}
}

class FooVoid
{
public static int $counter = 0;

public function __invoke(string $name): void
{
++self::$counter;
}
}

class Bar
{
public function __construct(Foo $foo)
Expand Down
Expand Up @@ -20,7 +20,8 @@ public function __construct()
{
$this->services = $this->privates = [];
$this->methodMap = [
'closure' => 'getClosureService',
'closure1' => 'getClosure1Service',
'closure2' => 'getClosure2Service',
];

$this->aliases = [];
Expand All @@ -40,6 +41,7 @@ public function getRemovedIds(): array
{
return [
'foo' => true,
'foo_void' => true,
];
}

Expand All @@ -49,12 +51,22 @@ protected function createProxy($class, \Closure $factory)
}

/**
* Gets the public 'closure' shared service.
* Gets the public 'closure1' shared service.
*
* @return \Closure
*/
protected static function getClosureService($container, $lazyLoad = true)
protected static function getClosure1Service($container, $lazyLoad = true)
{
return $container->services['closure'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function cloneFoo(?\stdClass $bar = null): \Symfony\Component\DependencyInjection\Tests\Compiler\Foo { return $this->service->cloneFoo(...\func_get_args()); } })->cloneFoo(...);
return $container->services['closure1'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function cloneFoo(?\stdClass $bar = null): \Symfony\Component\DependencyInjection\Tests\Compiler\Foo { return $this->service->cloneFoo(...\func_get_args()); } })->cloneFoo(...);
}

/**
* Gets the public 'closure2' shared service.
*
* @return \Closure
*/
protected static function getClosure2Service($container, $lazyLoad = true)
{
return $container->services['closure2'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function __invoke(string $name): void { $this->service->__invoke(...\func_get_args()); } })->__invoke(...);
}
}

0 comments on commit c06e574

Please sign in to comment.