Skip to content

[DependencyInjection] Add support for generating lazy closures #49639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 19, 2023

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Mar 8, 2023

Q A
Branch? 6.3
Bug fix? no
New feature? no
Deprecations? no
Tickets -
License MIT
Doc PR -

On my mission to improve support for closures in DI, this allows generating lazy closures.

What is a lazy closure?

Right now, when we generate a closure, we generate something like:

return ($container->privates['foo'] ?? $container->getFooService())->someMethod(...);

As you can see, this requires instantiating the "foo" service, while the closure may never be called.

After this PR, a closure registered as a service can be declared as lazy, which will end up generating this instead (conceptually):

return fn (...$arguments) => ($container->privates['foo'] ?? $container->getFooService())->someMethod(...$arguments);

With this code, "foo" is created only if the closure is actually called.

This is opt-in since it requires the "lazy" flag on the service definition.

@carsonbot carsonbot added this to the 6.3 milestone Mar 8, 2023
@nicolas-grekas nicolas-grekas force-pushed the di-lazy-closures branch 2 times, most recently from 7b12697 to 9d54a4a Compare March 9, 2023 16:14
@nicolas-grekas nicolas-grekas force-pushed the di-lazy-closures branch 2 times, most recently from 187eb2a to 3e39308 Compare March 14, 2023 09:20
@stof
Copy link
Member

stof commented Mar 14, 2023

Generating a closure with a variadic argument will not produce the same behavior than the previous code though. Closure::fromCallable() or first-class callables are producing a callable that exposes the original signature when using Reflection.

@nicolas-grekas
Copy link
Member Author

@stof that's right. We could generate a signature from the proxied method when it's known, but that'd work only with PhpDumper, not with ContainerBuilder. But in most cases, the exact signature of a closure doesn't matter, as long at it behaves as expected when being called.

@stof
Copy link
Member

stof commented Mar 14, 2023

@nicolas-grekas then this requires an explicit documentation of this limitation, to avoid issues for cases where the callable signature matters (for instance if you try to use such callable in a controller resolver, as argument resolvers rely on the signature of the controller callable)

@nicolas-grekas
Copy link
Member Author

OK, so you triggered me :) Here is an implementation that respects the target signature (using eval() for ContainerBuilder, as already done for lazy services.)

$r = $this->container->getReflectionClass($class);

if ($r && $r->hasMethod($callable[1])) {
$signature = ProxyHelper::exportSignature($r->getMethod($callable[1]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this preserve parameter attributes ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't, and it shouldn't: proxies don't have to repeat the attributes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that such lazy callables won't be suitable for places doing runtime usages of attributes, like controller argument resolvers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. That's also the case for other lazy services: generated proxies don't copy attributes. Should be documented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it depends on the kind on proxies being generated. For lazy ghosts, we don't override methods, so they have the attributes in their inherited signatures.

@nicolas-grekas nicolas-grekas force-pushed the di-lazy-closures branch 9 times, most recently from 9ea49fb to 118bb81 Compare March 16, 2023 09:54
@fabpot
Copy link
Member

fabpot commented Mar 19, 2023

Thank you @nicolas-grekas.

@fabpot fabpot merged commit f577b3e into symfony:6.3 Mar 19, 2023
@nicolas-grekas nicolas-grekas deleted the di-lazy-closures branch March 20, 2023 16:08
ruudk added a commit to ruudk/symfony that referenced this pull request Aug 3, 2023
The LazyClosure was introduced in symfony#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
```
ruudk added a commit to ruudk/symfony that referenced this pull request Aug 3, 2023
…void`

The LazyClosure was introduced in symfony#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
```
ruudk added a commit to ruudk/symfony that referenced this pull request Aug 3, 2023
…void`

The LazyClosure was introduced in symfony#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
```
ruudk added a commit to ruudk/symfony that referenced this pull request Aug 3, 2023
…void`

The LazyClosure was introduced in symfony#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
```
nicolas-grekas added a commit that referenced this pull request Aug 3, 2023
… when return type of closure is `void` (ruudk)

This PR was squashed before being merged into the 6.3 branch.

Discussion
----------

[DependencyInjection] Do not add `return` in `LazyClosure` when return type of closure is `void`

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       |
| License       | MIT
| Doc PR        |

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
    {
        // the `return` below causes the error
        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
```

/cc `@nicolas`-grekas

Commits
-------

692704e [DependencyInjection] Do not add `return` in `LazyClosure` when return type of closure is `void`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants