Skip to content

Commit

Permalink
feature #52820 [DependencyInjection] Add #[AutowireInline] attribut…
Browse files Browse the repository at this point in the history
…e to allow service definition at the class level (DaDeather, nicolas-grekas)

This PR was merged into the 7.1 branch.

Discussion
----------

[DependencyInjection] Add `#[AutowireInline]` attribute to allow service definition at the class level

| Q             | A
| ------------- | ---
| Branch       | 7.1
| Bug fix      | no
| New feature  | yes
| Deprecations | no
| Issues        | Fix #52819
| License       | MIT

For the idea behind this feature see the issue that contains examples #52819

### Example usage:
```php
class SomeSourceAwareLogger
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly string $someSource,
    ) {
    }
}

class SomeSourceAwareLoggerFactory
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {
    }

    public function create(string $someSource): SomeSourceAwareLogger
    {
        return new SomeSourceAwareLogger($this->logger, $someSource);
    }

    public static function staticCreate(LoggerInterface $logger, string $someSource): SomeSourceAwareLogger
    {
        return new SomeSourceAwareLogger($logger, $someSource);
    }
}

// -----------

class SomeClass1
{
    public function __construct(
        #[AutowireInline(class: SomeSourceAwareLogger::class, args: [new Reference(LoggerInterface::class), 'bar'])]
        public SomeSourceAwareLogger $someSourceAwareLogger,
    ) {
    }
}

// AND/OR

class SomeClass2
{
    public function __construct(
        #[AutowireInline(
            class: SomeSourceAwareLogger::class,
            factory: [SomeSourceAwareLoggerFactory::class, 'staticCreate'],
            args: [new Reference(LoggerInterface::class), 'someParam'],
        )]
        public SomeSourceAwareLogger $someSourceAwareLogger,
    ) {
    }
}

// AND/OR

class SomeClass3
{
    public function __construct(
        #[AutowireInline(
            class: SomeSourceAwareLogger::class,
            factory: [new Reference(SomeSourceAwareLoggerFactory::class), 'create'],
            args: ['someParam'],
        )]
        public SomeSourceAwareLogger $someSourceAwareLogger,
    ) {
    }
}
```

Commits
-------

b9a838e Finish implementing AutowireInline attribute
a596142 [DependencyInjection] Add `#[AutowireInline]` attribute to allow service definition at the class level
  • Loading branch information
fabpot committed May 2, 2024
2 parents 15956b2 + b9a838e commit 66faca6
Show file tree
Hide file tree
Showing 23 changed files with 860 additions and 40 deletions.
Expand Up @@ -19,7 +19,7 @@
* Attribute to tell which callable to give to an argument of type Closure.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireCallable extends Autowire
class AutowireCallable extends AutowireInline
{
/**
* @param string|array|null $callable The callable to autowire
Expand All @@ -40,7 +40,7 @@ public function __construct(
throw new LogicException('#[AutowireCallable] attribute cannot have a $method without a $service.');
}

parent::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
Autowire::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
}

public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
Expand Down
@@ -0,0 +1,64 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
* Allows inline service definition for an argument.
*
* Using this attribute on a class autowires a new instance
* which is not shared between different services.
*
* $class a FQCN, or an array to define a factory.
* Use the "@" prefix to reference a service.
*
* @author Ismail Özgün Turan <oezguen.turan@dadadev.com>
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireInline extends Autowire
{
public function __construct(string|array|null $class = null, array $arguments = [], array $calls = [], array $properties = [], ?string $parent = null, bool|string $lazy = false)
{
if (null === $class && null === $parent) {
throw new LogicException('#[AutowireInline] attribute should declare either $class or $parent.');
}

parent::__construct([
\is_array($class) ? 'factory' : 'class' => $class,
'arguments' => $arguments,
'calls' => $calls,
'properties' => $properties,
'parent' => $parent,
], lazy: $lazy);
}

public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
{
static $parseDefinition;
static $yamlLoader;

$parseDefinition ??= new \ReflectionMethod(YamlFileLoader::class, 'parseDefinition');
$yamlLoader ??= $parseDefinition->getDeclaringClass()->newInstanceWithoutConstructor();

if (isset($value['factory'])) {
$value['class'] = $type;
$value['factory'][0] ??= $type;
$value['factory'][1] ??= '__invoke';
}
$class = $parameter->getDeclaringClass();

return $parseDefinition->invoke($yamlLoader, $class->name, $value, $class->getFileName(), ['autowire' => true], true);
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -13,6 +13,7 @@ CHANGELOG
* Add argument `$prepend` to `FileLoader::construct()` to prepend loaded configuration instead of appending it
* [BC BREAK] When used in the `prependExtension()` method, the `ContainerConfigurator::import()` method now prepends the configuration instead of appending it
* Cast env vars to null or bool when referencing them using `#[Autowire(env: '...')]` depending on the signature of the corresponding parameter
* Add `#[AutowireInline]` attribute to allow service definition at the class level

7.0
---
Expand Down
Expand Up @@ -13,8 +13,8 @@

use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
use Symfony\Component\DependencyInjection\Attribute\Lazy;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -331,7 +331,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
continue 2;
}

if ($attribute instanceof AutowireCallable) {
if ($attribute instanceof AutowireInline) {
$value = $attribute->buildDefinition($value, $type, $parameter);
$value = $this->doProcessValue($value);
} elseif ($lazy = $attribute->lazy) {
Expand Down
Expand Up @@ -166,6 +166,9 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
*/
private function isInlineableDefinition(string $id, Definition $definition): bool
{
if (str_starts_with($id, '.autowire_inline.')) {
return true;
}
if ($definition->hasErrors() || $definition->isDeprecated() || $definition->isLazy() || $definition->isSynthetic() || $definition->hasTag('container.do_not_inline')) {
return false;
}
Expand Down
Expand Up @@ -55,6 +55,7 @@ public function __construct()
new AutoAliasServicePass(),
new ValidateEnvPlaceholdersPass(),
new ResolveDecoratorStackPass(),
new ResolveAutowireInlineAttributesPass(),
new ResolveChildDefinitionsPass(),
new RegisterServiceSubscribersPass(),
new ResolveParameterPlaceHoldersPass(false, false),
Expand Down
@@ -0,0 +1,141 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\VarExporter\ProxyHelper;

/**
* Inspects existing autowired services for {@see AutowireInline} attributes and registers the definitions for reuse.
*
* @author Ismail Özgün Turan <oezguen.turan@dadadev.com>
*/
class ResolveAutowireInlineAttributesPass extends AbstractRecursivePass
{
protected bool $skipScalars = true;

protected function processValue(mixed $value, bool $isRoot = false): mixed
{
$value = parent::processValue($value, $isRoot);

if (!$value instanceof Definition || !$value->isAutowired() || !$value->getClass() || $value->hasTag('container.ignore_attributes')) {
return $value;
}

$isChildDefinition = $value instanceof ChildDefinition;

try {
$constructor = $this->getConstructor($value, false);
} catch (RuntimeException) {
return $value;
}

if ($constructor) {
$arguments = $this->registerAutowireInlineAttributes($constructor, $value->getArguments(), $isChildDefinition);

if ($arguments !== $value->getArguments()) {
$value->setArguments($arguments);
}
}

$dummy = $value;
while (null === $dummy->getClass() && $dummy instanceof ChildDefinition) {
$dummy = $this->container->findDefinition($dummy->getParent());
}

$methodCalls = $value->getMethodCalls();

foreach ($methodCalls as $i => $call) {
[$method, $arguments] = $call;

try {
$method = $this->getReflectionMethod($dummy, $method);
} catch (RuntimeException) {
continue;
}

$arguments = $this->registerAutowireInlineAttributes($method, $arguments, $isChildDefinition);

if ($arguments !== $call[1]) {
$methodCalls[$i][1] = $arguments;
}
}

if ($methodCalls !== $value->getMethodCalls()) {
$value->setMethodCalls($methodCalls);
}

return $value;
}

private function registerAutowireInlineAttributes(\ReflectionFunctionAbstract $method, array $arguments, bool $isChildDefinition): array
{
$parameters = $method->getParameters();

if ($method->isVariadic()) {
array_pop($parameters);
}
$dummyContainer = new ContainerBuilder($this->container->getParameterBag());

foreach ($parameters as $index => $parameter) {
if ($isChildDefinition) {
$index = 'index_'.$index;
}

$name = '$'.$parameter->name;
if (\array_key_exists($name, $arguments)) {
$arguments[$index] = $arguments[$name];
unset($arguments[$name]);
}
if (\array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
continue;
}
if (!$attribute = $parameter->getAttributes(AutowireInline::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
continue;
}

$type = ProxyHelper::exportType($parameter, true);

if (!$type && isset($arguments[$index])) {
continue;
}

$attribute = $attribute->newInstance();
$definition = $attribute->buildDefinition($attribute->value, $type, $parameter);

$dummyContainer->setDefinition('.autowire_inline', $definition);
(new ResolveParameterPlaceHoldersPass(false, false))->process($dummyContainer);

$id = '.autowire_inline.'.ContainerBuilder::hash([$this->currentId, $method->class ?? null, $method->name, (string) $parameter]);

$this->container->setDefinition($id, $definition);
$arguments[$index] = new Reference($id);

if ($definition->isAutowired()) {
$currentId = $this->currentId;
try {
$this->currentId = $id;
$this->processValue($definition, true);
} finally {
$this->currentId = $currentId;
}
}
}

return $arguments;
}
}
11 changes: 9 additions & 2 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Expand Up @@ -494,7 +494,9 @@ public function removeDefinition(string $id): void
{
if (isset($this->definitions[$id])) {
unset($this->definitions[$id]);
$this->removedIds[$id] = true;
if ('.' !== ($id[0] ?? '-')) {
$this->removedIds[$id] = true;
}
}
}

Expand Down Expand Up @@ -768,6 +770,9 @@ public function compile(bool $resolveEnvPlaceholders = false): void
parent::compile();

foreach ($this->definitions + $this->aliasDefinitions as $id => $definition) {
if ('.' === ($id[0] ?? '-')) {
continue;
}
if (!$definition->isPublic() || $definition->isPrivate()) {
$this->removedIds[$id] = true;
}
Expand Down Expand Up @@ -841,7 +846,9 @@ public function removeAlias(string $alias): void
{
if (isset($this->aliasDefinitions[$alias])) {
unset($this->aliasDefinitions[$alias]);
$this->removedIds[$alias] = true;
if ('.' !== ($alias[0] ?? '-')) {
$this->removedIds[$alias] = true;
}
}
}

Expand Down
Expand Up @@ -257,7 +257,7 @@ class %s extends {$options['class']}
$preloadedFiles = [];
$ids = $this->container->getRemovedIds();
foreach ($this->container->getDefinitions() as $id => $definition) {
if (!$definition->isPublic()) {
if (!$definition->isPublic() && '.' !== ($id[0] ?? '-')) {
$ids[$id] = true;
}
}
Expand Down Expand Up @@ -1380,7 +1380,7 @@ private function addRemovedIds(): string
{
$ids = $this->container->getRemovedIds();
foreach ($this->container->getDefinitions() as $id => $definition) {
if (!$definition->isPublic()) {
if (!$definition->isPublic() && '.' !== ($id[0] ?? '-')) {
$ids[$id] = true;
}
}
Expand Down

0 comments on commit 66faca6

Please sign in to comment.