Skip to content
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

Autowire Drupal container params and plain values #6061

Open
wants to merge 2 commits into
base: 13.x
Choose a base branch
from
Open
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
65 changes: 56 additions & 9 deletions src/Commands/AutowireTrait.php
Copy link
Member

Choose a reason for hiding this comment

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

This file is based on https://github.com/drupal/drupal/blob/11.x/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php. Looks like this PR wants to expand its duties considerably. I'm not opposed to that, but I fear that this may get tricky to maintain as Symfony evolves.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Drush\Commands;

use Drupal\Component\DependencyInjection\ContainerInterface as DrupalContainer;
use Drush\Drush;
use League\Container\Container as DrushContainer;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
Expand All @@ -16,31 +19,75 @@
*/
trait AutowireTrait
{
/**
/**
* Limit to service and param or plain value.
*
* @see \Symfony\Component\DependencyInjection\Attribute\Autowire::__construct
*/
private const ACCEPTED_AUTOWIRE_ARGUMENTS = [
0 => 'value',
1 => 'service',
4 => 'param',
];

/**
* Instantiates a new instance of the implementing class using autowiring.
*
* @param ContainerInterface $container
* The service container this instance should use.
*
* @return static
*/
public static function create(ContainerInterface $container)
public static function create(ContainerInterface $container, ?ContainerInterface $drushContainer = null): self
{
$args = [];

if (method_exists(static::class, '__construct')) {
$drushContainer = $container instanceof DrushContainer ? $container : ($drushContainer instanceof DrushContainer ? $drushContainer : Drush::getContainer());
$drupalContainer = $container instanceof DrupalContainer ? $container : null;

$constructor = new \ReflectionMethod(static::class, '__construct');
foreach ($constructor->getParameters() as $parameter) {
$service = ltrim((string) $parameter->getType(), '?');
foreach ($parameter->getAttributes(Autowire::class) as $attribute) {
$service = (string) $attribute->newInstance()->value;
if (!$attributes = $parameter->getAttributes(Autowire::class)) {
// No #[Autowire()] attribute.
$service = ltrim((string) $parameter->getType(), '?');
if (!$drushContainer->has($service)) {
throw new AutowiringFailedException($service, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s::_construct()", you should configure its value explicitly.', $service, $parameter->getName(), static::class));
}
$args[] = $drushContainer->get($service);
continue;
}

if (!$container->has($service)) {
throw new AutowiringFailedException($service, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s::_construct()", you should configure its value explicitly.', $service, $parameter->getName(), static::class));
}
// This parameter has an #[Autowire()] attribute.
[$attribute] = $attributes;
$value = null;
foreach ($attribute->getArguments() as $key => $argument) {
// Resolve argument name when arguments are passed as list.
if (is_int($key)) {
if ($argument === null || !isset(self::ACCEPTED_AUTOWIRE_ARGUMENTS[$key])) {
continue;
}
$key = self::ACCEPTED_AUTOWIRE_ARGUMENTS[$key];
}

if (!in_array($key, self::ACCEPTED_AUTOWIRE_ARGUMENTS, true)) {
continue;
}

$args[] = $container->get($service);
$value = $attribute->newInstance()->value;
$valueAsString = (string) $value;
$value = match ($key) {
'service' => $drushContainer->has($valueAsString) ? $drushContainer->get($valueAsString) : throw new AutowiringFailedException($valueAsString, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s::_construct()", you should configure its value explicitly.', $valueAsString, $parameter->getName(), static::class)),
// Container param comes as %foo.bar.param%.
'param' => $drupalContainer ? $drupalContainer->getParameter(trim($valueAsString, '%')) : $valueAsString,
default => $value,
};
// Done as Autowire::__construct() only needs one argument.
break;
}
if ($value !== null) {
$args[] = $value;
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Commands/generate/GenerateCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Drush\Commands\core\DocsCommands;
use Drush\Commands\DrushCommands;
use Drush\Commands\help\ListCommands;
use Psr\Container\ContainerInterface as DrushContainer;
use League\Container\DefinitionContainerInterface as DrushContainer;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
Expand All @@ -21,6 +21,7 @@ final class GenerateCommands extends DrushCommands
protected function __construct(
private readonly DrushContainer $drush_container,
) {
parent::__construct();
}

public static function create(DrushContainer $container): self
Expand Down
64 changes: 51 additions & 13 deletions src/Runtime/ServiceManager.php
Copy link
Member

Choose a reason for hiding this comment

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

I'm hoping that @greg-1-anderson can take a look at changes in this file.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
use Grasmash\YamlCli\Command\UnsetKeyCommand;
use Grasmash\YamlCli\Command\UpdateKeyCommand;
use Grasmash\YamlCli\Command\UpdateValueCommand;
use League\Container\Container as DrushContainer;
use League\Container\Container;
use League\Container\DefinitionContainerInterface as DrushContainer;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Robo\ClassDiscovery\RelativeNamespaceDiscovery;
Expand Down Expand Up @@ -342,18 +344,16 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain

// Prevent duplicate calls to delegate() by checking for state.
if ($container && !$drushContainer->has('state')) {
assert($drushContainer instanceof Container);
// Combine the two containers.
$drushContainer->delegate($container);
}
foreach ($bootstrapCommandClasses as $class) {
$commandHandler = null;

try {
if ($this->hasStaticCreateFactory($class) && $this->supportsCompoundContainer($class, $drushContainer)) {
// Hurray, this class is compatible with the container with delegate.
$commandHandler = $class::create($drushContainer);
} elseif ($container && $this->hasStaticCreateFactory($class)) {
$commandHandler = $class::create($container, $drushContainer);
if ($staticCreateFactoryArguments = $this->getStaticCreateFactoryArguments($class, $drushContainer, $container)) {
$commandHandler = $class::create(...$staticCreateFactoryArguments);
} elseif (!$container && $this->hasStaticCreateEarlyFactory($class)) {
$commandHandler = $class::createEarly($drushContainer);
} else {
Expand All @@ -372,22 +372,60 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain
return $commandHandlers;
}

/**
* Determine if the first parameter of the create method supports our container with delegate.
*/
protected function supportsCompoundContainer($class, $drush_container): bool
protected function getStaticCreateFactoryArguments(string $class, DrushContainer $drushContainer, ?DrupalContainer $drupalContainer = null): ?array
{
if (!method_exists($class, 'create')) {
return null;
}

$reflection = new \ReflectionMethod($class, 'create');
$hint = (string)$reflection->getParameters()[0]->getType();
return is_a($drush_container, $hint);
if (!$reflection->isStatic()) {
return null;
}

$params = $reflection->getParameters();

$args = [];
$type = ltrim((string) $params[0]->getType(), '?');
if ($drupalContainer && is_a($type, DrupalContainer::class, true)) {
// The factory create() method explicitly expects a Drupal container as 1st argument.
$args[] = $drupalContainer;
} elseif (is_a($type, DrushContainer::class, true)) {
// The factory create() method explicitly expects a Drush container as 1st argument. Don't add a 2nd
// argument. If the Drupal container has been initialized, it's already a delegate.
return [$drushContainer];
} elseif ($drupalContainer && is_a($type, ContainerInterface::class, true)) {
// The factory create() method expects a container of any type as st argument and the Drupal container has
Copy link
Member

Choose a reason for hiding this comment

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

what do we mean "has already been initialized"?

// been initialized. Pass it as 1st argument.
$args[] = $drupalContainer;
}

// Add Drush container as 2nd argument if the method expects one.
if (isset($params[1])) {
$type = ltrim((string) $params[1]->getType(), '?');
if (is_a($type, ContainerInterface::class, true)) {
$args[] = $drushContainer;
}
}
Comment on lines +403 to +409
Copy link
Member

Choose a reason for hiding this comment

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

I dont like commands and AutowireTrait having multiple numbers of params and type hints in create(). How about if we ask commands going forward to always type hint to Psr\Container\ContainerInterface. That receives the compound container. AutowireTrait could pull the drupalContainer out and get container params as needed. We need to declare a service for the drupalContainer in the league container. We already store a reference to the league container inside itself. So this would be a similar service.


return $args;
}

/**
* Check to see if the provided class has a static `create` method.
*/
protected function hasStaticCreateFactory(string $class): bool
{
return static::hasStaticMethod($class, 'create');
if (!$hasStaticCreateFactory = static::hasStaticMethod($class, 'create')) {
return false;
}
$reflection = new \ReflectionMethod($class, 'create');
// Check first two param typings.
foreach (array_slice($reflection->getParameters(), 0, 2) as $param) {
$typing = ltrim((string) $param->getType(), '?');
$hasStaticCreateFactory = $hasStaticCreateFactory && is_a($typing, ContainerInterface::class, true);
}
return $hasStaticCreateFactory;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions sut/modules/unish/woot/src/AutowireTestService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Drupal\woot;

final class AutowireTestService implements AutowireTestServiceInterface
{
public function __toString(): string
{
return 'Hello World!';
}
}
7 changes: 7 additions & 0 deletions sut/modules/unish/woot/src/AutowireTestServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace Drupal\woot;

interface AutowireTestServiceInterface extends \Stringable {}
6 changes: 6 additions & 0 deletions sut/modules/unish/woot/woot.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ services:
class: Drupal\woot\EventSubscriber\PreRowDeleteTestSubscriber
tags:
- { name: event_subscriber }
woot.autowire_test:
class: Drupal\woot\AutowireTestService
Drupal\woot\AutowireTestServiceInterface: '@woot.autowire_test'

parameters:
foo: bar
23 changes: 23 additions & 0 deletions tests/fixtures/lib/CreateExpectsDrupalContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Custom\Library;

use Drupal\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drush\Commands\DrushCommands;
use Psr\Log\LoggerInterface;

final class CreateFactoryExpectsDrupalContainer extends DrushCommands
{
public function __construct(
public readonly string $string,
public readonly RedirectDestinationInterface $redirectDestination,
) {
parent::__construct();
}

public static function create(ContainerInterface $container): self
{
return new self('a string as it is', $container->get('redirect.destination'));
}
}
22 changes: 22 additions & 0 deletions tests/fixtures/lib/CreateExpectsDrushContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Custom\Library;

use Drush\Commands\DrushCommands;
use League\Container\DefinitionContainerInterface;
use Psr\Log\LoggerInterface;

final class CreateExpectsDrushContainer extends DrushCommands
{
public function __construct(
public readonly string $string,
public readonly LoggerInterface $log,
) {
parent::__construct();
}

public static function create(DefinitionContainerInterface $container): self
{
return new self('a string as it is', $container->get('logger'));
}
}
64 changes: 64 additions & 0 deletions tests/fixtures/lib/Drush/Commands/AutowireTestCommands.php
Copy link
Member

Choose a reason for hiding this comment

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

Could not instantiate Custom\Library\Drush\Commands\AutowireTestCommands: Cannot autowire service "woot.autowire_test": argument "$argListContainerService" of method "Custom\Library\Drush\Commands\AutowireTestCommands::_construct()", you should configure its value explicitly. [1.06 sec, 8.13 MB]

I am seeing an error when I run drush st -vvv. I'm not sure why this commandfile loaded when I just ddev start, and am not running tests.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Custom\Library\Drush\Commands;

use Drupal\woot\AutowireTestService;
use Drupal\woot\AutowireTestServiceInterface;
use Drush\Attributes as CLI;
use Drush\Boot\DrupalBootLevels;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
use League\Container\ContainerAwareInterface;
use League\Container\ContainerAwareTrait;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class AutowireTestCommands extends DrushCommands
{
use AutowireTrait;

public function __construct(
#[Autowire('a string as it is')]
private readonly string $argListStringValue,
#[Autowire(null, 'woot.autowire_test')]
private readonly AutowireTestService $argListContainerService,
#[Autowire(null, null, null, null, 'foo')]
private readonly string $argListContainerParam,
#[Autowire(value: 'a string as it is')]
private readonly string $namedArgStringValue,
#[Autowire(service: 'woot.autowire_test')]
private readonly AutowireTestService $namedArgContainerService,
#[Autowire(param: 'foo')]
private readonly string $namedArgContainerParam,
private readonly AutowireTestServiceInterface $noAutowireAttributeContainerService,
) {
parent::__construct();
}

#[CLI\Command(name: 'test_autowire:drupal-container')]
#[CLI\Bootstrap(level: DrupalBootLevels::FULL)]
#[CLI\Help(hidden: true)]
public function drupal(): string
{
$values = [];
$constructor = new \ReflectionMethod($this, '__construct');
foreach ($constructor->getParameters() as $param) {
$values[] = (string) $this->{$param->getName()};
}
return implode("\n", $values);
}

#[CLI\Command(name: 'test_autowire:drush-container')]
#[CLI\Bootstrap(level: DrupalBootLevels::NONE)]
#[CLI\Help(hidden: true)]
public function drush(): string
{
$values = [];
$constructor = new \ReflectionMethod($this, '__construct');
foreach ($constructor->getParameters() as $param) {
$values[] = (string) $this->{$param->getName()};
}
return implode("\n", $values);
}
}
Loading