Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

@chalasr chalasr Mar 20, 2024

Choose a reason for hiding this comment

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

AutowireInline looks pretty cryptic, any newcomer wouldn't be able to get what the attribute is for by just looking at its name nor its description as it currently is. I think we either need to find a super self-explanatory name (I've no clue) or extend the description so that it tells what's the purpose of the attribute and when it should be used (inline service definition only means something to advanced Symfony's DIC hackers)

Copy link
Member

Choose a reason for hiding this comment

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

Naming things... :)
"inline" refers to "inline_service()" in the PHP-DSL

AutowireInlineService? but verbose
AutowireNew? AutowireObject? AutowireInstance? or keep AutowireInline?

of course, a top notch description is also desirable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a better description here feel free to request further suggestments to it 👍.

{
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,
nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved
'calls' => $calls,
'properties' => $properties,
'parent' => $parent,
nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved
], 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 1 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 11 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] ?? '-')) {
Copy link
Member

Choose a reason for hiding this comment

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

I added this rule so that we don't use internal service ids in error messages. This matters to this PR because the added code creates new removed-ids, which means fixtures have to be updated with (what I consider as) noise.

$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
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.