-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from 1 commit
8b5789f
fb89bd4
6b47912
62a9682
705f2fd
9e02f9a
51834e0
f993888
dc97db7
aa661d3
ece54ea
674207b
2025668
36cc20f
dbcdbbb
da8f93b
170b5ad
c74ec7e
077f123
971aff9
e5fad4e
c9428ba
c979e3c
ebfa31f
503fa6f
d59b343
f003f14
b092149
ce2ce5d
d3998b7
c598710
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
{ | ||
|
||
} |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
LegacyServiceFinder
instead?There was a problem hiding this comment.
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.