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

Backport static create factory commands for Drush 11.x #5565

Merged
merged 7 commits into from
May 18, 2023
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ api
sut/*
!sut/drush
sut/drush/sites/*test.site.yml
!sut/modules
sut/modules/contrib
/sandbox/
.env
# Test fixtures
Expand Down
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ install:
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))

test_script:
- composer info phpunit/phpunit
- vendor/bin/phpunit --colors=always --configuration tests --testsuite functional --debug
- vendor/bin/phpunit --colors=always --configuration tests --testsuite integration --debug
- vendor/bin/phpunit --colors=always --configuration tests --testsuite unit --debug
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"ext-dom": "*",
"chi-teck/drupal-code-generator": "^2.4",
"composer/semver": "^1.4 || ^3",
"consolidation/annotated-command": "^4.7.0",
"consolidation/annotated-command": "^4.8.2",
"consolidation/config": "^2",
"consolidation/filter-via-dot-access-data": "^2",
"consolidation/robo": "^3.0.9 || ^4.0.1",
Expand Down
705 changes: 348 additions & 357 deletions composer.lock

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
Creating a new Drush command or porting a legacy command is easy. Follow the steps below.

1. Run `drush generate drush-command-file`.
2. Drush will prompt for the machine name of the module that should "own" the file.
1. (optional) Drush will also prompt for the path to a legacy command file to port. See [tips on porting commands to Drush 11](https://weitzman.github.io/blog/port-to-drush9)
1. The module selected must already exist and be enabled. Use `drush generate module-standard` to create a new module.
3. Drush will then report that it created a commandfile, a drush.services.yml file and a composer.json file. Edit those files as needed.
4. Use the classes for the core Drush commands at [/src/Drupal/Commands](https://github.com/drush-ops/drush/tree/11.x/src/Drupal/Commands) as inspiration and documentation.
5. See the [dependency injection docs](dependency-injection.md) for interfaces you can implement to gain access to Drush config, Drupal site aliases, etc.
6. Write PHPUnit tests based on [Drush Test Traits](https://github.com/drush-ops/drush/blob/11.x/docs/contribute/unish.md#drush-test-traits).
7. Once your drush.services.yml files is ready, run `drush cr` to get your command recognized by the Drupal container.
2. Drush will prompt for the machine name of the module that should "own" the file. The module selected must already exist and be enabled. Use `drush generate module-standard` to create a new module.
3. Drush will then report that it created a commandfile. Edit as needed.
4. Use the classes for the core Drush commands at [/src/Commands](https://github.com/drush-ops/drush/tree/12.x/src/Commands) as inspiration and documentation.
5. See the [dependency injection docs](dependency-injection.md) for interfaces you can implement to gain access to Drush config, Drupal site aliases, etc. Also note the [create() method](dependency-injection.md#create-method) for injecting Drupal or Drush dependencies.
6. Write PHPUnit tests based on [Drush Test Traits](https://github.com/drush-ops/drush/blob/12.x/docs/contribute/unish.md#drush-test-traits).

## Attributes or Annotations
The following are both valid ways to declare a command:
Expand Down
27 changes: 26 additions & 1 deletion docs/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,35 @@ There are two ways that a class can receive its dependencies. One is called “c
```
A class should use one or the other of these methods. The code that is responsible for providing the dependencies a class need is usually an object called the dependency injection container.

create() method
------------------

!!! tip

Drush 11 and prior required [dependency injection via a drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files). This approach is deprecated in Drush 12 and will be removed in Drush 13.

Drush command files can inject services by adding a create() method to the commandfile. See [creating commands](commands.md) for instructions on how to use the Drupal Code Generator to create a simple command file starter. A create() method and a constructor will look something like this:
```php
class WootStaticFactoryCommands extends DrushCommands
{
protected $configFactory;

protected function __construct($configFactory)
{
$this->configFactory = $configFactory;
}

public static function create(ContainerInterface $container): self
{
return new static($container->get('config.factory'));
}
```
See the [Drupal Documentation](https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/services-and-dependency-injection-in-drupal-8#s-injecting-dependencies-into-controllers-forms-and-blocks) for details on how to inject Drupal services into your command file. Drush's approach mimics Drupal's blocks, forms, and controllers.

Services Files
------------------

Drush command files can request that Drupal inject services by using a drush.services.yml file. See [creating commands](commands.md) for instructions on how to use the Drupal Code Generator to create a simple command file starter with a drush.services.yml file. An initial services file will look something like this:
Drush command files can request that Drupal inject services by using a drush.services.yml file. This used to be the preferred method to do dependency injection for Drush commands, but is being phased out in favor of the create() method, described above. An example services file might look something like this:
```yaml
services:
my_module.commands:
Expand Down
22 changes: 22 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class Application extends SymfonyApplication implements LoggerAwareInterface, Co
/** @var TildeExpansionHook */
protected $tildeExpansionHook;

/** @var string[] */
protected array $bootstrapCommandClasses = [];

/**
* Add global options to the Application and their default values to Config.
*/
Expand Down Expand Up @@ -178,6 +181,11 @@ public function selectUri($cwd)
return $uri;
}

public function bootstrapCommandClasses(): array
{
return $this->bootstrapCommandClasses;
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -320,6 +328,20 @@ public function configureAndRegisterCommands(InputInterface $input, OutputInterf
[FilterHooks::class]
));

// If a command class has a static `create` method, then we will
// postpone instantiating it until after we bootstrap Drupal.
$this->bootstrapCommandClasses = array_filter($commandClasses, function (string $class): bool {
if (!method_exists($class, 'create')) {
return false;
}

$reflectionMethod = new \ReflectionMethod($class, 'create');
return $reflectionMethod->isStatic();
});

// Remove the command classes that we put into the bootstrap command classes.
$commandClasses = array_diff($commandClasses, $this->bootstrapCommandClasses);

// Uncomment the lines below to use Console's built in help and list commands.
// unset($commandClasses[__DIR__ . '/Commands/help/HelpCommands.php']);
// unset($commandClasses[__DIR__ . '/Commands/help/ListCommands.php']);
Expand Down
94 changes: 89 additions & 5 deletions src/Boot/DrupalBoot8.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\Filesystem\Path;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Consolidation\AnnotatedCommand\CommandFileDiscovery;
use Robo\Robo;

class DrupalBoot8 extends DrupalBoot implements AutoloaderAwareInterface
{
Expand Down Expand Up @@ -265,16 +267,31 @@ public function addDrupalModuleDrushCommands($manager): void
// until after $kernel->boot() is called.
$container = \Drupal::getContainer();

// Set the command info alterers.
// Find the containerless commands, generators and command info alterers
$bootstrapCommandClasses = $application->bootstrapCommandClasses();
$commandInfoAlterers = [];
foreach ($container->getParameter('container.modules') as $moduleId => $moduleInfo) {
$path = dirname(DRUPAL_ROOT . '/' . $moduleInfo['pathname']) . '/src/Drush/';
$commandsInThisModule = $this->discoverModuleCommands([$path], "\\Drupal\\" . $moduleId . "\\Drush");
$bootstrapCommandClasses = array_merge($bootstrapCommandClasses, $commandsInThisModule);
$commandInfoAlterersInThisModule = $this->discoverCommandInfoAlterers([$path], "\\Drupal\\" . $moduleId . "\\Drush");
$commandInfoAlterers = array_merge($commandInfoAlterers, $commandInfoAlterersInThisModule);
}

// Find the command info alterers in Drush services.
if ($container->has(DrushServiceModifier::DRUSH_COMMAND_INFO_ALTERER_SERVICES)) {
$serviceCommandInfoAltererList = $container->get(DrushServiceModifier::DRUSH_COMMAND_INFO_ALTERER_SERVICES);
$commandFactory = Drush::commandFactory();
foreach ($serviceCommandInfoAltererList->getCommandList() as $altererHandler) {
$commandFactory->addCommandInfoAlterer($altererHandler);
$this->logger->debug(dt('Commands are potentially altered in !class.', ['!class' => get_class($altererHandler)]));
}
$commandInfoAlterers = array_merge($commandInfoAlterers, $serviceCommandInfoAltererList->getCommandList());
}

// Set the command info alterers.
foreach ($serviceCommandInfoAltererList->getCommandList() as $altererHandler) {
$commandFactory->addCommandInfoAlterer($altererHandler);
$this->logger->debug(dt('Commands are potentially altered in !class.', ['!class' => get_class($altererHandler)]));
}

// Register the Drush Symfony Console commands found in Drush services
if ($container->has(DrushServiceModifier::DRUSH_CONSOLE_SERVICES)) {
$serviceCommandList = $container->get(DrushServiceModifier::DRUSH_CONSOLE_SERVICES);
foreach ($serviceCommandList->getCommandList() as $command) {
Expand All @@ -283,6 +300,7 @@ public function addDrupalModuleDrushCommands($manager): void
$application->add($command);
}
}

// Do the same thing with the annotation commands.
if ($container->has(DrushServiceModifier::DRUSH_COMMAND_SERVICES)) {
$serviceCommandList = $container->get(DrushServiceModifier::DRUSH_COMMAND_SERVICES);
Expand All @@ -292,6 +310,72 @@ public function addDrupalModuleDrushCommands($manager): void
$runner->registerCommandClass($application, $commandHandler);
}
}

// Finally, instantiate all of the classes we discovered in
// configureAndRegisterCommands, and all of the classes we find
// via 'discoverModuleCommands' that have static create factory methods.
foreach ($bootstrapCommandClasses as $class) {
$commandHandler = null;
try {
// We insist that the command class have a static 'create' method.
// We could make this optional, but doing so would run the risk
// 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);
}
} catch (\Exception $e) {
}
// Fail silently if the command handler could not be
// instantiated, e.g. if it tries to fetch services from
// a module that has not been enabled.
if ($commandHandler) {
$manager->inflect($commandHandler);
$runner->registerCommandClass($application, $commandHandler);
}
}
}

protected function hasStaticCreateFactory($class)
{
if (!method_exists($class, 'create')) {
return false;
}

$reflectionMethod = new \ReflectionMethod($class, 'create');
return $reflectionMethod->isStatic();
}

/**
* Discover module commands. This is the preferred way to find module
* commands in Drush 12+.
*/
protected function discoverModuleCommands(array $directoryList, string $baseNamespace): array
{
$discovery = new CommandFileDiscovery();
$discovery
->setIncludeFilesAtBase(true)
->setSearchDepth(1)
->ignoreNamespacePart('src')
->setSearchLocations(['Commands', 'Hooks', 'Generators'])
->setSearchPattern('#.*(Command|Hook|Generator)s?.php$#');
$baseNamespace = ltrim($baseNamespace, '\\');
$commandClasses = $discovery->discover($directoryList, $baseNamespace);
return array_values($commandClasses);
}

protected function discoverCommandInfoAlterers(array $directoryList, string $baseNamespace): array
{
$discovery = new CommandFileDiscovery();
$discovery
->setIncludeFilesAtBase(true)
->setSearchDepth(1)
->ignoreNamespacePart('src')
->setSearchLocations(['CommandInfoAlterers'])
->setSearchPattern('#.*CommandInfoAlterer.php$#');
$baseNamespace = ltrim($baseNamespace, '\\');
$commandClasses = $discovery->discover($directoryList, $baseNamespace);
return array_values($commandClasses);
}

/**
Expand Down
32 changes: 1 addition & 31 deletions src/Commands/generate/Generators/Drush/DrushCommandFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,37 +43,7 @@ protected function generate(array &$vars): void
$vars['commands'] = $this->adjustCommands($commands);
}

$this->addFile('src/Commands/{class}.php', 'drush-command-file.php');

$json = $this->getComposerJson($vars);
$content = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$this->addFile('composer.json')
->content($content)
->replaceIfExists();

$this->addFile('drush.services.yml', 'drush.services.yml');
}

protected function getComposerJson(array $vars): array
{
$composer_json_template_path = __DIR__ . '/dcf-composer.json';
// TODO: look up the path of the 'machine_name' module.
$composer_json_existing_path = DRUPAL_ROOT . '/modules/' . $vars['machine_name'] . '/composer.json';
$composer_json_path = file_exists($composer_json_existing_path) ? $composer_json_existing_path : $composer_json_template_path;
$composer_json_contents = file_get_contents($composer_json_path);
$composer_json_data = json_decode($composer_json_contents, true);

// If there is no name, fill something in
if (empty($composer_json_data['name'])) {
$composer_json_data['name'] = 'org/' . $vars['machine_name'];
}

// Add an entry for the Drush services file.
$composer_json_data['extra']['drush']['services'] = [
'drush.services.yml' => '^' . Drush::getMajorVersion(),
];

return $composer_json_data;
$this->addFile('src/Drush/Commands/{class}.php', 'drush-command-file.php');
}

protected function getOwningModulePath(array $vars): string
Expand Down
4 changes: 0 additions & 4 deletions src/Commands/generate/Generators/Drush/dcf-composer.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
<?php

namespace Drupal\{{ machine_name }}\Commands;
namespace Drupal\{{ machine_name }}\Drush\Commands;

{% if not source %}
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
{% endif %}
use Drush\Commands\DrushCommands;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* A Drush commandfile.
*
* In addition to this file, you need a drush.services.yml
* in root of your module, and a composer.json file that provides the name
* of the services file to use.
*
* See these files for an example of injecting Drupal services:
* - http://cgit.drupalcode.org/devel/tree/src/Commands/DevelCommands.php
* - http://cgit.drupalcode.org/devel/tree/drush.services.yml
*/
class {{ class }} extends DrushCommands {

/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {
parent::__construct();
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
{{ di.container(services) }}
);
}

{% if source %}
{% include 'ported-methods.php.twig' %}
{% else %}
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion src/Drupal/DrupalKernelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ protected function findDefaultServicesFile($module, $dir)
if (!file_exists($result)) {
return;
}
Drush::logger()->info(dt("!module should have an extra.drush.services section in its composer.json. See https://www.drush.org/latest/commands/#specifying-the-services-file.", ['!module' => $module]));
return $result;
}

Expand Down
Loading