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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.