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

Create a service adapter for drush.services.yml #5553

Merged
merged 31 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8b5789f
Add a basic Drush service adapter. Feed it from the existing drush.se…
greg-1-anderson May 4, 2023
fb89bd4
Use our own service discovery class instead of Symfony DI container c…
greg-1-anderson May 5, 2023
6b47912
Code style
greg-1-anderson May 5, 2023
62a9682
Fix module command discovery
greg-1-anderson May 6, 2023
705f2fd
Fix typo in Console command handling
greg-1-anderson May 6, 2023
9e02f9a
Rename DrushServiceFinder to LegacyServiceFinder and add ServiceManag…
greg-1-anderson May 8, 2023
51834e0
Code style
greg-1-anderson May 8, 2023
f993888
Move ModuleGeneratorTest to the functional tests, because integration…
greg-1-anderson May 8, 2023
dc97db7
Merge branch '12.x' into drush-service-adapter
greg-1-anderson May 8, 2023
aa661d3
Skip failing archive:restore test.
greg-1-anderson May 8, 2023
ece54ea
Simplify LegacyServiceInstantiator; remove unnecessary containers.
greg-1-anderson May 8, 2023
674207b
Code style
greg-1-anderson May 8, 2023
2025668
Remove unused class DrushServiceModifier, and also remove some unused…
greg-1-anderson May 9, 2023
36cc20f
Remove container test, because Drush is no longer involved with conta…
greg-1-anderson May 9, 2023
dbcdbbb
Move PSR-4 command discovery out of Generate command and into the Ser…
greg-1-anderson May 9, 2023
da8f93b
Remove unused FindCommandsCompilerPass
greg-1-anderson May 9, 2023
170b5ad
Move command discovery to the service manager.
greg-1-anderson May 9, 2023
c74ec7e
Code style
greg-1-anderson May 9, 2023
077f123
Fix typo in LegacyServiceFinder
greg-1-anderson May 9, 2023
971aff9
Use module handler instead of container.modules in DrupalBoot8.
greg-1-anderson May 9, 2023
e5fad4e
Move module discovery methods to the service manager
greg-1-anderson May 9, 2023
c9428ba
Move discovery code out of DrupalKernelTrait and into LegacyServiceIn…
greg-1-anderson May 10, 2023
c979e3c
Code style
greg-1-anderson May 10, 2023
ebfa31f
Merge branch '12.x' into drush-service-adapter
greg-1-anderson May 11, 2023
503fa6f
Move bootstrap classes from Application to ServiceManager
greg-1-anderson May 11, 2023
d59b343
Declare FilterHooks in ServiceManager
greg-1-anderson May 11, 2023
f003f14
Explicitly instantiate command info alterers
greg-1-anderson May 11, 2023
b092149
Add some docblock comments
greg-1-anderson May 11, 2023
ce2ce5d
Throw Symfony ParameterNotFoundException when service cannot be initi…
greg-1-anderson May 11, 2023
d3998b7
Merge branch '12.x' into drush-service-adapter
greg-1-anderson May 11, 2023
c598710
Typehints and docblock comments for service manager
greg-1-anderson May 11, 2023
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
16 changes: 15 additions & 1 deletion src/Boot/DrupalBoot8.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\HttpFoundation\Response;
use Consolidation\AnnotatedCommand\CommandFileDiscovery;
use Robo\Robo;
use Drush\Runtime\ServiceInstantiator;

class DrupalBoot8 extends DrupalBoot implements AutoloaderAwareInterface
{
Expand Down Expand Up @@ -247,13 +248,22 @@ public function bootstrapDrupalFull(BootstrapManager $manager): void
public function addDrupalModuleDrushCommands($manager): void
{
$application = Drush::getApplication();
$drushContainer = Drush::getContainer();

$this->logger->debug(dt("Loading drupal module drush commands & etc.", []));

// We have to get the service command list from the container, because
// it is constructed in an indirect way during the container initialization.
// The upshot is that the list of console commands is not available
// until after $kernel->boot() is called.
$container = \Drupal::getContainer();

// Drush services adapter
$drushServiceFiles = $this->kernel->getDrushServiceFiles();
$serviceInstantiator = new ServiceInstantiator($container, $drushContainer);
$serviceInstantiator->loadServiceFiles($drushServiceFiles);
$drushCommandHandlers = $serviceInstantiator->taggedServices('drush.command');

// Find the containerless commands, generators and command info alterers
$bootstrapCommandClasses = $application->bootstrapCommandClasses();
$commandInfoAlterers = [];
Expand Down Expand Up @@ -309,7 +319,7 @@ public function addDrupalModuleDrushCommands($manager): void
// of double-instantiating Drush service commands, if anyone decided
// to put those in the same namespace (\Drupal\modulename\Drush\Commands)
if ($this->hasStaticCreateFactory($class)) {
$commandHandler = $class::create($container);
$commandHandler = $class::create($container, $drushContainer);
}
} catch (\Exception $e) {
}
Expand All @@ -321,6 +331,10 @@ public function addDrupalModuleDrushCommands($manager): void
Robo::register($application, $commandHandler);
}
}
foreach ($drushCommandHandlers as $commandHandler) {
$manager->inflect($commandHandler);
Robo::register($application, $commandHandler);
}
}

protected function hasStaticCreateFactory($class)
Expand Down
13 changes: 12 additions & 1 deletion src/Drupal/DrupalKernelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ trait DrupalKernelTrait
{
/** @var ServiceModifierInterface[] */
protected $serviceModifiers = [];
protected $serviceFinder;
protected $drushServiceYamls = [];

public function getDrushServiceFiles()
{
return $this->drushServiceYamls;
}

/**
* Add a service modifier to the container builder.
Expand Down Expand Up @@ -209,7 +216,11 @@ protected function findAppropriateServicesFile($module, $services, $dir)
protected function addDrushServiceProvider($serviceProviderName, $serviceYmlPath = '')
{
if (($serviceYmlPath !== null) && file_exists($serviceYmlPath)) {
$this->serviceYamls['app'][$serviceProviderName] = $serviceYmlPath;
// Keep our own list of service files
$this->drushServiceYamls[$serviceProviderName] = $serviceYmlPath;
// This is how we used to add our drush.services.yml file
// to the Drush service container. This is no longer necessary.
//$this->serviceYamls['app'][$serviceProviderName] = $serviceYmlPath;
}
}
}
24 changes: 24 additions & 0 deletions src/Runtime/DrushServiceFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Drush\Runtime;

use Drush\Log\Logger;
use League\Container\Container;
use Drush\Drush;
use Symfony\Component\Console\Application;
use Composer\Autoload\ClassLoader;
use League\Container\ContainerInterface;
use Drush\Command\DrushCommandInfoAlterer;

use Psr\Container\ContainerInterface;
use League\Container\Container as DrushContainer;

/**
* Find drush.services.yml files.
*/
class DrushServiceFinder
{

Copy link
Member Author

Choose a reason for hiding this comment

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

Committed by mistake, but might end up using eventually. Intended as the home for the drush.services.yml discovery methods, but I put that work off and just used the existing code in the Drush Drupal Kernel trait for the initial PoC.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe LegacyServiceFinder instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good suggestion; this code vanishes if support for drush.services.yml is ever removed, and we are going to deprecate that even if it sticks around for b/c.

}
204 changes: 204 additions & 0 deletions src/Runtime/ServiceInstantiator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

declare(strict_types=1);

namespace Drush\Runtime;

use Drush\Log\Logger;
use League\Container\Container;
use Drush\Drush;
use Symfony\Component\Console\Application;
use Composer\Autoload\ClassLoader;
use Drush\Command\DrushCommandInfoAlterer;

use Psr\Container\ContainerInterface;
use League\Container\Container as DrushContainer;
use Symfony\Component\Yaml\Yaml;
use Robo\Robo;

/**
* Use Dependency Injection Container to instantiate services.
*/
class ServiceInstantiator
{
protected ContainerInterface $drushServicesContainer;
protected array $tags = [];

public function __construct(protected ContainerInterface $container, protected ContainerInterface $drushContainer)
{
$this->drushServicesContainer = new DrushContainer();
Copy link
Member Author

Choose a reason for hiding this comment

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

Services created in a drush.services.yml file are stored in this container so that they can be referenced as needed e.g. as the parameter to some other service or command also in a drush.services.yml file. Usually, only services in the same file can reference each other, as there is no other way to ensure the initialization order of Drush services.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe with a little extra work, we could use the features of league container to lazy-initialize the commandfiles. I'm not sure if it's necessary to allow cross-Drush-service-file references, though. Since Drush services can reference Drupal services, it would be preferable to just make a Drupal service for any shared service needs a Drush command might have.

Copy link
Member

Choose a reason for hiding this comment

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

Is this a third container? I have no idea if there is a better way but its at least a thing that will raise an eyebrow.

  1. Original Drush container
  2. New container containing Drupal service data
  3. Drupal container.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could re-use the drupal container or the drush container or just put it in an array. The scope is only during instantiation; it is not needed thereafter.

}

public function loadServiceFiles(array $serviceFiles)
{
foreach ($serviceFiles as $serviceFile) {
$serviceFileContents = '';
$serviceFileData = [];

if(file_exists($serviceFile)) {
$serviceFileContents = file_get_contents($serviceFile);
}
if (!empty($serviceFileContents)) {
$serviceFileData = Yaml::parse($serviceFileContents);
}

if (isset($serviceFileData['services'])) {
$this->instantiateServices($serviceFileData['services']);
}
}
}

/**
* Given a drush.services.yml file (parsed into an array),
* instantiate all of the services referenced therein, and
* cache them in our dynamic service container.
Copy link
Member

Choose a reason for hiding this comment

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

"cache them" - the drush containers are rebuilt from scratch on every request, right? maybe we can say 'collect them' instead. if we are dumping containers to disk then we a similar cache rebuild issue that plagues the current module defined commands.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yeah, there is no persistent cache here.

*/
public function instantiateServices(array $services)
{
foreach ($services as $serviceName => $info) {
$service = $this->create(
$info['class'],
$info['arguments'] ?? [],
$info['calls'] ?? []
);

Robo::addShared($this->drushServicesContainer, $serviceName, $service);

// If `tags` to contains an item with `name: drush.command`,
// then we should do something special with it

if (isset($info['tags'])) {
foreach ($info['tags'] as $tag) {
if (isset($tag['name'])) {
$this->tags[$tag['name']][$serviceName] = $service;
}
}
}
}
}

public function taggedServices($tagName)
{
return $this->tags[$tagName] ?? [];
}

/**
* Create one named service.
*/
public function create($class, array $arguments, array $calls)
{
$instance = $this->instantiateObject($class, $arguments);
foreach ($calls as $callInfo) {
$this->call($instance, $callInfo[0], $callInfo[1]);
}
return $instance;
}

/**
* Instantiate an object with the given arguments.
* Arguments are first looked up from the Drupal container
* or from our dynamic service container if they begin
* with an `@`. Items from the Drush container may be
* retreived by prepending the Drush service name with `*`.
*/
public function instantiateObject($class, array $arguments)
{
$refl = new \ReflectionClass($class);
return $refl->newInstanceArgs($this->resolveArguments($arguments));
}

/**
* Call a method of an object with the provided arguments.
* Arguments are resolved against the container first.
*/
public function call($object, $method, array $arguments)
{
$resolved = $this->resolveArguments($arguments);
if ($this->atLeastOneValue($resolved)) {
call_user_func_array([$object, $method], $resolved);
}
}

/**
* Resolve arguments against our containers. Arguments that
* do not map to one of our containers are returned unchanged.
*/
protected function resolveArguments(array $arguments)
{
return array_map([$this, 'resolveArgument'], $arguments);
}

/**
* Look up one argument in the appropriate container, or
* return it as-is.
*/
protected function resolveArgument($arg)
{
if (!is_string($arg)) {
return $arg;
}

if ($arg[0] == '@') {
// Check to see if a previous drush.services.yml instantiated
// this service; return any service found.
$result = $this->resolveFromContainer($this->drushServicesContainer, substr($arg, 1), false);
if ($result) {
return $result;
}

// If the service is not found in the dynamic container
return $this->resolveFromContainer($this->container, substr($arg, 1));
}

// Use '*' instead of '@' to pull from the Drush container.
if ($arg[0] == '*') {
return $this->resolveFromContainer($this->drushContainer, substr($arg, 1));
}

return $arg;
}

/**
* Look up in the provided container; throw an exception if
* not found, unless the service name begins with `?` (e.g.
* `@?drupal.service` or `*?drush.service`).
*/
protected function resolveFromContainer($container, string $arg, bool $checkRequired = true)
{
[$required, $arg] = $this->isRequired($arg);

// Exit early if the container does not have the service
if (!$container->has($arg)) {
if ($checkRequired && $required) {
throw new \Exception("Big badda boom! This should be the same thing that the Drupal / Symfony DI container throws.");
}

return null;
}

return $container->get($arg);
}

/**
* Check to see if the provided argument begins with a `?`;
* those that do not are required.
*/
protected function isRequired(string $arg)
{
if ($arg[0] == '?') {
return [false, substr($arg, 1)];
}

return [true, $arg];
}

protected function atLeastOneValue($args)
{
foreach ($args as $arg) {
if ($arg) {
return true;
}
}
return false;
}
}