Skip to content

Commit

Permalink
feature #49836 [DependencyInjection] Add support for `#[Autowire(lazy…
Browse files Browse the repository at this point in the history
…: class-string)]` (nicolas-grekas)

This PR was merged into the 6.3 branch.

Discussion
----------

[DependencyInjection] Add support for `#[Autowire(lazy: class-string)]`

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

This PR finishes what started in #49685.

It adds support for defining the interfaces to proxy when using laziness with the Autowire attribute.

```php
    public function __construct(
        #[Autowire(service: 'foo', lazy: FooInterface::class)]
        FooInterface|BarInterface $foo,
    ) {
    }
```

It also adds support for lazy-autowiring of intersection types:

```php
    public function __construct(
        #[Autowire(service: 'foobar', lazy: true)]
        FooInterface&BarInterface $foobar,
    ) {
    }
```

Commits
-------

d127ebf [DependencyInjection] Add support for `#[Autowire(lazy: class-string)]`
  • Loading branch information
nicolas-grekas committed Mar 28, 2023
2 parents 827dbbf + d127ebf commit 99081f9
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
class Autowire
{
public readonly string|array|Expression|Reference|ArgumentInterface|null $value;
public readonly bool|array $lazy;

/**
* Use only ONE of the following.
Expand All @@ -34,16 +35,17 @@ class Autowire
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
* @param string|null $env Environment variable name (ie 'SOME_ENV_VARIABLE')
* @param string|null $param Parameter name (ie 'some.parameter.name')
* @param bool|class-string|class-string[] $lazy Whether to use lazy-loading for this argument
*/
public function __construct(
string|array|ArgumentInterface $value = null,
string $service = null,
string $expression = null,
string $env = null,
string $param = null,
public bool $lazy = false,
bool|string|array $lazy = false,
) {
if ($lazy) {
if ($this->lazy = \is_string($lazy) ? [$lazy] : $lazy) {
if (null !== ($expression ?? $env ?? $param)) {
throw new LogicException('#[Autowire] attribute cannot be $lazy and use $expression, $env, or $param.');
}
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ CHANGELOG
* Add `#[Exclude]` to skip autoregistering a class
* Add support for generating lazy closures
* Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`
* Add support for `#[Autowire(lazy: true)]`
* Add support for `#[Autowire(lazy: true|class-string)]`
* Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
* Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,33 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
->setFactory(['Closure', 'fromCallable'])
->setArguments([$value + [1 => '__invoke']])
->setLazy($attribute->lazy);
} elseif ($attribute->lazy && ($value instanceof Reference ? !$this->container->has($value) || !$this->container->findDefinition($value)->isLazy() : null === $attribute->value && $type)) {
$this->container->register('.lazy.'.$value ??= $getValue(), $type)
} elseif ($lazy = $attribute->lazy) {
$definition = (new Definition($type))
->setFactory('current')
->setArguments([[$value]])
->setArguments([[$value ??= $getValue()]])
->setLazy(true);
$value = new Reference('.lazy.'.$value);

if (!\is_array($lazy)) {
if (str_contains($type, '|')) {
throw new AutowiringFailedException($this->currentId, sprintf('Cannot use #[Autowire] with option "lazy: true" on union types for service "%s"; set the option to the interface(s) that should be proxied instead.', $this->currentId));
}
$lazy = str_contains($type, '&') ? explode('&', $type) : [];
}

if ($lazy) {
if (!class_exists($type) && !interface_exists($type, false)) {
$definition->setClass('object');
}
foreach ($lazy as $v) {
$definition->addTag('proxy', ['interface' => $v]);
}
}

if ($definition->getClass() !== (string) $value || $definition->getTag('proxy')) {
$value .= '.'.$this->container->hash([$definition->getClass(), $definition->getTag('proxy')]);
}
$this->container->setDefinition($value = '.lazy.'.$value, $definition);
$value = new Reference($value);
}
$arguments[$index] = $value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function instantiateProxy(ContainerInterface $container, Definition $defi
throw new InvalidArgumentException(sprintf('Cannot instantiate lazy proxy for service "%s".', $id));
}

if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject, $class), false)) {
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject), false)) {
eval($dumper->getProxyCode($definition, $id));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public function getProxyCode(Definition $definition, string $id = null): string
if (!interface_exists($tag['interface']) && !class_exists($tag['interface'], false)) {
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": several "proxy" tags found but "%s" is not an interface.', $id ?? $definition->getClass(), $tag['interface']));
}
if (!is_a($class->name, $tag['interface'], true)) {
if ('object' !== $definition->getClass() && !is_a($class->name, $tag['interface'], true)) {
throw new InvalidArgumentException(sprintf('Invalid "proxy" tag for service "%s": class "%s" doesn\'t implement "%s".', $id ?? $definition->getClass(), $definition->getClass(), $tag['interface']));
}
$interfaces[] = new \ReflectionClass($tag['interface']);
Expand All @@ -141,10 +141,11 @@ public function getProxyCode(Definition $definition, string $id = null): string

public function getProxyClass(Definition $definition, bool $asGhostObject, \ReflectionClass &$class = null): string
{
$class = new \ReflectionClass($definition->getClass());
$class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass';
$class = new \ReflectionClass($class);

return preg_replace('/^.*\\\\/', '', $class->name)
return preg_replace('/^.*\\\\/', '', $definition->getClass())
.($asGhostObject ? 'Ghost' : 'Proxy')
.ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name), -7));
.ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer;
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\IInterface;
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
use Symfony\Component\DependencyInjection\Tests\Compiler\WitherAnnotation;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
Expand Down Expand Up @@ -1769,6 +1772,29 @@ public function testLazyAutowireAttribute()
$this->assertInstanceOf(LazyObjectInterface::class, $container->get('bar')->foo);
$this->assertSame($container->get('foo'), $container->get('bar')->foo->initializeLazyObject());
}

public function testLazyAutowireAttributeWithIntersection()
{
$container = new ContainerBuilder();
$container->register('foo', AAndIInterfaceConsumer::class)
->setPublic('true')
->setAutowired(true);

$container->compile();

$lazyId = \array_slice(array_keys($container->getDefinitions()), -1)[0];
$this->assertStringStartsWith('.lazy.foo.', $lazyId);
$definition = $container->getDefinition($lazyId);
$this->assertSame('object', $definition->getClass());
$this->assertSame([
['interface' => AInterface::class],
['interface' => IInterface::class],
], $definition->getTag('proxy'));

$dumper = new PhpDumper($container);

$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute_with_intersection.php', $dumper->dump());
}
}

class Rot13EnvVarProcessor implements EnvVarProcessorInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Compiler;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Service\Attribute\Required;

require __DIR__.'/uniontype_classes.php';
Expand Down Expand Up @@ -526,3 +527,12 @@ public function __construct(NotExisting $notExisting)
{
}
}

class AAndIInterfaceConsumer
{
public function __construct(
#[Autowire(service: 'foo', lazy: true)]
AInterface&IInterface $logger,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ protected static function getFoo2Service($container, $lazyLoad = true)
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxy9f41ec7', static fn () => \FooProxy9f41ec7::createLazyProxy(static fn () => self::getFoo2Service($containerRef->get(), false)));
return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxy4048957', static fn () => \FooProxy4048957::createLazyProxy(static fn () => self::getFoo2Service($containerRef->get(), false)));
}

return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo());
}
}

class FooProxy9f41ec7 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface
class FooProxy4048957 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface
{
use \Symfony\Component\VarExporter\LazyProxyTrait;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

/**
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
*/
class ProjectServiceContainer extends Container
{
protected $parameters = [];
protected readonly \WeakReference $ref;

public function __construct()
{
$this->ref = \WeakReference::create($this);
$this->services = $this->privates = [];
$this->methodMap = [
'foo' => 'getFooService',
];

$this->aliases = [];
}

public function compile(): void
{
throw new LogicException('You cannot compile a dumped container that was already compiled.');
}

public function isCompiled(): bool
{
return true;
}

public function getRemovedIds(): array
{
return [
'.lazy.foo.gDmfket' => true,
];
}

protected function createProxy($class, \Closure $factory)
{
return $factory();
}

/**
* Gets the public 'foo' shared autowired service.
*
* @return \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer
*/
protected static function getFooService($container)
{
$a = ($container->privates['.lazy.foo.gDmfket'] ?? self::get_Lazy_Foo_GDmfketService($container));

if (isset($container->services['foo'])) {
return $container->services['foo'];
}

return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer($a);
}

/**
* Gets the private '.lazy.foo.gDmfket' shared service.
*
* @return \object
*/
protected static function get_Lazy_Foo_GDmfketService($container, $lazyLoad = true)
{
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->privates['.lazy.foo.gDmfket'] = $container->createProxy('objectProxy8ac8e9a', static fn () => \objectProxy8ac8e9a::createLazyProxy(static fn () => self::get_Lazy_Foo_GDmfketService($containerRef->get(), false)));
}

return ($container->services['foo'] ?? self::getFooService($container));
}
}

class objectProxy8ac8e9a implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface
{
use \Symfony\Component\VarExporter\LazyProxyTrait;

private const LAZY_OBJECT_PROPERTY_SCOPES = [];

public function initializeLazyObject(): \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface&\Symfony\Component\DependencyInjection\Tests\Compiler\IInterface
{
if ($state = $this->lazyObjectState ?? null) {
return $state->realInstance ??= ($state->initializer)();
}

return $this;
}
}

// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ namespace Container%s;

include_once $container->targetDir.''.'/Fixtures/includes/foo.php';

class FooClassGhost2b16075 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface
class FooClassGhostEe53b95 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface
%A

if (!\class_exists('FooClassGhost2b16075', false)) {
\class_alias(__NAMESPACE__.'\\FooClassGhost2b16075', 'FooClassGhost2b16075', false);
if (!\class_exists('FooClassGhostEe53b95', false)) {
\class_alias(__NAMESPACE__.'\\FooClassGhostEe53b95', 'FooClassGhostEe53b95', false);
}

[Container%s/ProjectServiceContainer.php] => <?php
Expand Down Expand Up @@ -78,7 +78,7 @@ class ProjectServiceContainer extends Container
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->services['lazy_foo'] = $container->createProxy('FooClassGhost2b16075', static fn () => \FooClassGhost2b16075::createLazyGhost(static fn ($proxy) => self::getLazyFooService($containerRef->get(), $proxy)));
return $container->services['lazy_foo'] = $container->createProxy('FooClassGhostEe53b95', static fn () => \FooClassGhostEe53b95::createLazyGhost(static fn ($proxy) => self::getLazyFooService($containerRef->get(), $proxy)));
}

include_once $container->targetDir.''.'/Fixtures/includes/foo_lazy.php';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ protected static function getBarService($container, $lazyLoad = true)
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->services['bar'] = $container->createProxy('stdClassGhost5a8a5eb', static fn () => \stdClassGhost5a8a5eb::createLazyGhost(static fn ($proxy) => self::getBarService($containerRef->get(), $proxy)));
return $container->services['bar'] = $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getBarService($containerRef->get(), $proxy)));
}

return $lazyLoad;
Expand All @@ -72,7 +72,7 @@ protected static function getBazService($container, $lazyLoad = true)
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->services['baz'] = $container->createProxy('stdClassProxy5a8a5eb', static fn () => \stdClassProxy5a8a5eb::createLazyProxy(static fn () => self::getBazService($containerRef->get(), false)));
return $container->services['baz'] = $container->createProxy('stdClassProxy2fc7938', static fn () => \stdClassProxy2fc7938::createLazyProxy(static fn () => self::getBazService($containerRef->get(), false)));
}

return \foo_bar();
Expand All @@ -88,7 +88,7 @@ protected static function getBuzService($container, $lazyLoad = true)
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->services['buz'] = $container->createProxy('stdClassProxy5a8a5eb', static fn () => \stdClassProxy5a8a5eb::createLazyProxy(static fn () => self::getBuzService($containerRef->get(), false)));
return $container->services['buz'] = $container->createProxy('stdClassProxy2fc7938', static fn () => \stdClassProxy2fc7938::createLazyProxy(static fn () => self::getBuzService($containerRef->get(), false)));
}

return \foo_bar();
Expand All @@ -104,14 +104,14 @@ protected static function getFooService($container, $lazyLoad = true)
$containerRef = $container->ref;

if (true === $lazyLoad) {
return $container->services['foo'] = $container->createProxy('stdClassGhost5a8a5eb', static fn () => \stdClassGhost5a8a5eb::createLazyGhost(static fn ($proxy) => self::getFooService($containerRef->get(), $proxy)));
return $container->services['foo'] = $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getFooService($containerRef->get(), $proxy)));
}

return $lazyLoad;
}
}

class stdClassGhost5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
class stdClassGhost2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
{
use \Symfony\Component\VarExporter\LazyGhostTrait;

Expand All @@ -123,7 +123,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);

class stdClassProxy5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
class stdClassProxy2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
{
use \Symfony\Component\VarExporter\LazyProxyTrait;

Expand Down
Loading

0 comments on commit 99081f9

Please sign in to comment.