From 301cd2428616973dec1eb5aab7282de07b2f2827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20Kipin=C3=A4?= Date: Wed, 28 Jun 2023 17:16:02 +0300 Subject: [PATCH 01/21] Make query example work by using the database service (#5680) Without this change the error message is Error: Non-static method Drupal\Core\Database\Connection::query() cannot be called statically in eval(). --- src/Commands/sql/SqlCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/sql/SqlCommands.php b/src/Commands/sql/SqlCommands.php index 89ad9c1df9..136398b46c 100644 --- a/src/Commands/sql/SqlCommands.php +++ b/src/Commands/sql/SqlCommands.php @@ -160,7 +160,7 @@ public function cli(InputInterface $input, $options = ['extra' => self::REQ]): v #[CLI\Usage(name: 'drush sql:query --db-prefix "SELECT * FROM {users}"', description: 'Browse user record. Table prefixes are honored. Caution: All curly-braces will be stripped.')] #[CLI\Usage(name: '$(drush sql:connect) < example.sql', description: 'Import sql statements from a file into the current database.')] #[CLI\Usage(name: 'drush sql:query --file=example.sql', description: 'Alternate way to import sql statements from a file.')] - #[CLI\Usage(name: 'drush php:eval --format=json "return \Drupal\Core\Database\Connection::query(\'SELECT * FROM users LIMIT 5\')->fetchAll()"', description: 'Get data back in JSON format. See https://github.com/drush-ops/drush/issues/3071#issuecomment-347929777.')] + #[CLI\Usage(name: 'drush php:eval --format=json "return \Drupal::service(\'database\')->query(\'SELECT * FROM users LIMIT 5\')->fetchAll()"', description: 'Get data back in JSON format. See https://github.com/drush-ops/drush/issues/3071#issuecomment-347929777.')] #[CLI\Usage(name: '$(drush sql:connect) -e "SELECT * FROM users LIMIT 5;"', description: 'Results are formatted in a pretty table with borders and column headers.')] #[CLI\ValidateFileExists(argName: 'file')] public function query($query = '', $options = ['result-file' => null, 'file' => self::REQ, 'file-delete' => false, 'extra' => self::REQ, 'db-prefix' => false]): bool From 02d96d65995747f3b3a714f331447738923c6333 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sun, 2 Jul 2023 07:47:16 -0600 Subject: [PATCH 02/21] Fix #5672. TypeError when setting config value to '[]' (#5682) --- src/Commands/config/ConfigCommands.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Commands/config/ConfigCommands.php b/src/Commands/config/ConfigCommands.php index 1cbef80eaf..db62e4e118 100644 --- a/src/Commands/config/ConfigCommands.php +++ b/src/Commands/config/ConfigCommands.php @@ -166,16 +166,16 @@ public function set($config_name, $key, $value, $options = ['input-format' => 's $data = $this->stdin()->contents(); } - // Special handling for empty array. - if ($data == '[]') { - $data = []; - } - // Special handling for null. if (strtolower($data) == 'null') { $data = null; } + // Special handling for empty array. + if ($data == '[]') { + $data = []; + } + if ($options['input-format'] === 'yaml') { $parser = new Parser(); $data = $parser->parse($data); From 3285e33454b1571c9e6a7955a4c7545e94d6b1dc Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sun, 2 Jul 2023 07:51:26 -0600 Subject: [PATCH 03/21] --editor must have a value (#5683) --- src/Attributes/OptionsetGetEditor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Attributes/OptionsetGetEditor.php b/src/Attributes/OptionsetGetEditor.php index 5fdf613c64..644cff776d 100644 --- a/src/Attributes/OptionsetGetEditor.php +++ b/src/Attributes/OptionsetGetEditor.php @@ -6,13 +6,14 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; +use Drush\Commands\DrushCommands; #[Attribute(Attribute::TARGET_METHOD)] class OptionsetGetEditor { public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) { - $commandInfo->addOption('editor', 'A string of bash which launches user\'s preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.', [], ''); + $commandInfo->addOption('editor', 'A string of bash which launches user\'s preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.', [], DrushCommands::REQ); $commandInfo->addOption('bg', 'Launch editor in background process.', [], false); } } From 2dd7b48cd9ea07973687055a48903dfe5aba28e4 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sun, 2 Jul 2023 20:52:15 -0600 Subject: [PATCH 04/21] Fix #5651. drush sec could give better guidance when using drupal/core-recommended (#5684) --- src/Commands/pm/SecurityUpdateCommands.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Commands/pm/SecurityUpdateCommands.php b/src/Commands/pm/SecurityUpdateCommands.php index eaae5561c4..18dc634601 100644 --- a/src/Commands/pm/SecurityUpdateCommands.php +++ b/src/Commands/pm/SecurityUpdateCommands.php @@ -57,7 +57,7 @@ public function security(array $options = ['no-dev' => false]): RowsOfFields|Com $composer_lock_data = $this->loadSiteComposerLock(); $updates = $this->calculateSecurityUpdates($composer_lock_data, $security_advisories_composer_json, $options['no-dev']); if ($updates) { - $this->suggestComposerCommand($updates); + $this->suggestComposerCommand($updates, $composer_lock_data['packages']); return CommandResult::dataWithExitCode(new RowsOfFields($updates), self::EXIT_FAILURE_WITH_CLARITY); } $this->logger()->success("There are no outstanding security updates for Drupal projects."); @@ -70,10 +70,14 @@ public function security(array $options = ['no-dev' => false]): RowsOfFields|Com /** * Emit suggested Composer command for security updates. */ - public function suggestComposerCommand($updates): void + public function suggestComposerCommand($updates, array $composer_lock_packages): void { $suggested_command = 'composer require '; foreach ($updates as $package) { + // Improve guidance for 'recommended' users. + if ($package['name'] == 'drupal/core' && isset($composer_lock_packages['drupal/core-recommended'])) { + $package['name'] = 'drupal/core-recommended'; + } $suggested_command .= $package['name'] . ' '; } $suggested_command .= '--update-with-dependencies'; From 7aa30e34a778fe9d98386a0f83ad7703742877df Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Thu, 6 Jul 2023 11:56:56 +0200 Subject: [PATCH 05/21] Fix 'TypeError: SymfonyStyle::ask(): Argument #2 ($default) must be of type ?string, int given' (#5690) --- src/Commands/field/FieldCreateCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/field/FieldCreateCommands.php b/src/Commands/field/FieldCreateCommands.php index 1dc687c8e4..8f7ff3778f 100644 --- a/src/Commands/field/FieldCreateCommands.php +++ b/src/Commands/field/FieldCreateCommands.php @@ -335,7 +335,7 @@ protected function askCardinality(): int $limit = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED; while ($cardinality === 'Limited' && $limit < 1) { - $limit = (int) $this->io()->ask('Allowed number of values', 1); + $limit = (int) $this->io()->ask('Allowed number of values', '1'); } return $limit; From 4e71e538d6383092ea03c79d47f20c8a596ba5a7 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 6 Jul 2023 07:44:44 -0400 Subject: [PATCH 06/21] Add an EntityToArray simplifier to FormatterManager (#5685) * Add an EntityToArray simplifier to FormatterManager * Use var_dump as default formatter for php:eval command * Bump ouput-formatters dependancy --- composer.json | 1 + composer.lock | 14 +++++------ src/Commands/core/PhpCommands.php | 2 +- src/Formatters/EntityToArraySimplifier.php | 28 ++++++++++++++++++++++ src/Runtime/DependencyInjection.php | 4 +++- src/Runtime/ServiceManager.php | 2 +- 6 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 src/Formatters/EntityToArraySimplifier.php diff --git a/composer.json b/composer.json index 37c231cca9..af14bb6a47 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "consolidation/annotated-command": "^4.9.1", "consolidation/config": "^2.1.2", "consolidation/filter-via-dot-access-data": "^2.0.2", + "consolidation/output-formatters": "^4.3.2", "consolidation/robo": "^4.0.6", "consolidation/site-alias": "^4", "consolidation/site-process": "^5.2.0", diff --git a/composer.lock b/composer.lock index a90f36f9fa..b27197a138 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f66675b89de48af07b27b469322a9e8b", + "content-hash": "338ce4b80d57c0a055bd5edd55611dca", "packages": [ { "name": "chi-teck/drupal-code-generator", @@ -368,16 +368,16 @@ }, { "name": "consolidation/output-formatters", - "version": "4.3.1", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "f65524e9ecd2bd0021c4b18710005caaa6dcbd86" + "reference": "06711568b4cd169700ff7e8075db0a9a341ceb58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/f65524e9ecd2bd0021c4b18710005caaa6dcbd86", - "reference": "f65524e9ecd2bd0021c4b18710005caaa6dcbd86", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/06711568b4cd169700ff7e8075db0a9a341ceb58", + "reference": "06711568b4cd169700ff7e8075db0a9a341ceb58", "shasum": "" }, "require": { @@ -416,9 +416,9 @@ "description": "Format text by applying transformations provided by plug-in formatters.", "support": { "issues": "https://github.com/consolidation/output-formatters/issues", - "source": "https://github.com/consolidation/output-formatters/tree/4.3.1" + "source": "https://github.com/consolidation/output-formatters/tree/4.3.2" }, - "time": "2023-05-20T03:23:06+00:00" + "time": "2023-07-06T04:45:41+00:00" }, { "name": "consolidation/robo", diff --git a/src/Commands/core/PhpCommands.php b/src/Commands/core/PhpCommands.php index d7d4791f45..12db6346b7 100644 --- a/src/Commands/core/PhpCommands.php +++ b/src/Commands/core/PhpCommands.php @@ -27,7 +27,7 @@ final class PhpCommands extends DrushCommands implements StdinAwareInterface #[CLI\Usage(name: 'drush php:eval "\Drupal::service(\'file_system\')->copy(\'$HOME/Pictures/image.jpg\', \'public://image.jpg\');"', description: 'Copies a file whose path is determined by an environment\'s variable. Use of double quotes so the variable $HOME gets replaced by its value.')] #[CLI\Usage(name: 'drush php:eval "node_access_rebuild();"', description: 'Rebuild node access permissions.')] #[CLI\Bootstrap(level: DrupalBootLevels::MAX)] - public function evaluate($code, $options = ['format' => 'var_export']) + public function evaluate($code, $options = ['format' => 'var_dump']) { return eval($code . ';'); } diff --git a/src/Formatters/EntityToArraySimplifier.php b/src/Formatters/EntityToArraySimplifier.php new file mode 100644 index 0000000000..b5291199be --- /dev/null +++ b/src/Formatters/EntityToArraySimplifier.php @@ -0,0 +1,28 @@ +implementsInterface('\Drupal\Core\Entity\EntityInterface'); + } + + public function simplifyToArray($structuredData, FormatterOptions $options): array + { + return $structuredData->toArray(); + } +} diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 6228882252..997c7500bc 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -4,6 +4,7 @@ namespace Drush\Runtime; +use Drush\Formatters\EntityToArraySimplifier; use Drush\Log\Logger; use League\Container\Container; use Symfony\Component\Console\Input\StringInput; @@ -113,7 +114,8 @@ protected function addDrushServices($container, ClassLoader $loader, DrushDrupal // @todo not sure that we'll use this. Maybe remove it. Robo::addShared($container, 'formatterManager', DrushFormatterManager::class) ->addMethodCall('addDefaultFormatters', []) - ->addMethodCall('addDefaultSimplifiers', []); + ->addMethodCall('addDefaultSimplifiers', []) + ->addMethodCall('addSimplifier', [new EntityToArraySimplifier()]); // Add some of our own objects to the container Robo::addShared($container, 'service.manager', 'Drush\Runtime\ServiceManager') diff --git a/src/Runtime/ServiceManager.php b/src/Runtime/ServiceManager.php index 6f08bd86fc..5893b11b20 100644 --- a/src/Runtime/ServiceManager.php +++ b/src/Runtime/ServiceManager.php @@ -239,7 +239,7 @@ public function discoverModuleCommands(array $directoryList, string $baseNamespa * * @param string[] $directoryList List of directories to search * @param string $baseNamespace The namespace to use at the base of each - * search diretory. Namespace components mirror directory structure. + * search directory. Namespace components mirror directory structure. * * @return string[] */ From a263508318d8665aa6dde2990d73691d4eee5391 Mon Sep 17 00:00:00 2001 From: Parkle Lee Date: Thu, 6 Jul 2023 19:48:37 +0800 Subject: [PATCH 07/21] Fix #5688. sql:query fails when given a Gzip file with path starting with ../ (#5689) --- src/Sql/SqlBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sql/SqlBase.php b/src/Sql/SqlBase.php index 28b7a0d66d..d6a75b0512 100644 --- a/src/Sql/SqlBase.php +++ b/src/Sql/SqlBase.php @@ -323,7 +323,7 @@ public function alwaysQuery(string $query, $input_file = null, ?string $result_f $process->run(); $this->setProcess($process); if ($process->isSuccessful()) { - $input_file = trim($input_file, '.gz'); + $input_file = preg_replace('/\.gz$/i', '', $input_file); } else { Drush::logger()->error(dt('Failed to decompress input file.')); return false; From 8664a4f9d380f62df99b142b19475a65a042c57e Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 6 Jul 2023 19:28:30 +0500 Subject: [PATCH 08/21] Twig debug (#5668) * Require stable DCG version * Add twig-debug command * Change error message * Clean-up --- src/Commands/core/TwigCommands.php | 38 ++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Commands/core/TwigCommands.php b/src/Commands/core/TwigCommands.php index e8a176873d..14d247700b 100644 --- a/src/Commands/core/TwigCommands.php +++ b/src/Commands/core/TwigCommands.php @@ -5,8 +5,10 @@ namespace Drush\Commands\core; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; +use Drupal\Core\DrupalKernelInterface; use Drupal\Core\Extension\ExtensionList; use Drupal\Core\PhpStorage\PhpStorageFactory; +use Drupal\Core\State\StateInterface; use Drupal\Core\Template\TwigEnvironment; use Drush\Attributes as CLI; use Drush\Commands\DrushCommands; @@ -21,8 +23,9 @@ final class TwigCommands extends DrushCommands { const UNUSED = 'twig:unused'; const COMPILE = 'twig:compile'; + const DEBUG = 'twig:debug'; - public function __construct(protected TwigEnvironment $twig, protected ModuleHandlerInterface $moduleHandler, private ExtensionList $extensionList) + public function __construct(protected TwigEnvironment $twig, protected ModuleHandlerInterface $moduleHandler, private ExtensionList $extensionList, private StateInterface $state, private DrupalKernelInterface $kernel) { } @@ -31,7 +34,9 @@ public static function create(ContainerInterface $container): self $commandHandler = new static( $container->get('twig'), $container->get('module_handler'), - $container->get('extension.list.module') + $container->get('extension.list.module'), + $container->get('state'), + $container->get('kernel'), ); return $commandHandler; @@ -117,4 +122,33 @@ public function twigCompile(): void $this->logger()->success(dt('Compiled twig template !path', ['!path' => $relative])); } } + + /** + * Enables Twig debug and disables caching Twig templates. + * + * @see \Drupal\system\Form\DevelopmentSettingsForm::submitForm() + */ + #[CLI\Command(name: self::DEBUG, aliases: ['twig-debug'])] + #[CLI\Argument(name: 'mode', description: 'Debug mode. Recognized values: on, off.', suggestedValues: ['on', 'off'])] + public function twigDebug(string $mode): void + { + // @todo Remove this condition once Drush drops support for Drupal 10.0. + if (version_compare(\Drupal::VERSION, '10.1.0') < 0) { + throw new \Exception('Twig debug command requires Drupal 10.1.0 and above.'); + } + $mode = match ($mode) { + 'on' => true, + 'off' => false, + default => throw new \Exception('Twig debug mode must be either "on" or "off".'), + }; + $twig_development = [ + 'twig_debug' => $mode, + 'twig_cache_disable' => $mode, + ]; + $this->state->setMultiple($twig_development); + $this->kernel->invalidateContainer(); + $this->io()->success( + dt('{operation} twig debug.', ['operation' => $mode ? 'Enabled' : 'Disabled']), + ); + } } From ddc733f02595671f75c28d473151c3ab839e012f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristofer=20Tengstr=C3=B6m?= Date: Fri, 7 Jul 2023 22:36:39 +0200 Subject: [PATCH 09/21] =?UTF-8?q?#5686=20Fix=20issue=20"Drush=20installati?= =?UTF-8?q?on=20is=20not=20detected=20for=20other=20site=20al=E2=80=A6=20(?= =?UTF-8?q?#5687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #5686 Fix issue "Drush installation is not detected for other site alias when having multiple local Drupal installations with aliases" * Change return type to array * Use phpstan array shapes to define complex return type * Use constant for exit status --------- Co-authored-by: Kristofer Tengström --- src/Preflight/Preflight.php | 13 ++++++++----- src/Preflight/RedispatchToSiteLocal.php | 15 ++++++++------- src/Runtime/Runtime.php | 6 +++--- tests/unish/Controllers/RuntimeController.php | 6 +++--- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Preflight/Preflight.php b/src/Preflight/Preflight.php index ccd44d4dba..c22ed3f735 100644 --- a/src/Preflight/Preflight.php +++ b/src/Preflight/Preflight.php @@ -7,6 +7,7 @@ use Composer\Autoload\ClassLoader; use Consolidation\SiteAlias\SiteAliasManager; use DrupalFinder\DrupalFinder; +use Drush\Commands\DrushCommands; use Drush\DrupalFinder\DrushDrupalFinder; use Drush\Config\ConfigLocator; use Drush\Config\DrushConfig; @@ -242,8 +243,10 @@ public function config(): DrushConfig /** * @param $argv * True if the request was successfully redispatched remotely. False if the request should proceed. + * + * @return array{preflightDidRedispatch: bool, exitStatus: int} */ - public function preflight($argv): bool + public function preflight($argv): array { // Fail fast if there is anything in our environment that does not check out $this->verify->verify($this->environment); @@ -313,9 +316,9 @@ public function preflight($argv): bool // a site-local Drush. If there is, we will redispatch to it. // NOTE: termination handlers have not been set yet, so it is okay // to exit early without taking special action. - $status = RedispatchToSiteLocal::redispatchIfSiteLocalDrush($argv, $alteredRoot, $this->environment->vendorPath(), $this->logger()); - if ($status) { - return $status; + [$preflightDidRedispatch, $exitStatus] = RedispatchToSiteLocal::redispatchIfSiteLocalDrush($argv, $alteredRoot, $this->environment->vendorPath(), $this->logger()); + if ($preflightDidRedispatch) { + return [$preflightDidRedispatch, $exitStatus]; } // If the Drupal site changed, and the alternate site does not @@ -337,7 +340,7 @@ public function preflight($argv): bool // has set it to something higher in one of the config files we loaded. $this->verify->confirmPhpVersion($config->get('drush.php.minimum-version')); - return false; + return [false, DrushCommands::EXIT_SUCCESS]; } /** diff --git a/src/Preflight/RedispatchToSiteLocal.php b/src/Preflight/RedispatchToSiteLocal.php index 115bc2a0a7..ed5e65da02 100644 --- a/src/Preflight/RedispatchToSiteLocal.php +++ b/src/Preflight/RedispatchToSiteLocal.php @@ -4,6 +4,7 @@ namespace Drush\Preflight; +use Drush\Commands\DrushCommands; use Symfony\Component\Filesystem\Path; /** @@ -23,27 +24,26 @@ class RedispatchToSiteLocal * @param string $vendor The path to the vendor directory * @param PreflightLog $preflightLog A basic logger. * - * @return bool - * True if redispatch occurred, and was returned successfully. + * @return array{preflightDidRedispatch: bool, exitStatus: int} */ - public static function redispatchIfSiteLocalDrush(array $argv, string $root, string $vendor, PreflightLog $preflightLog) + public static function redispatchIfSiteLocalDrush(array $argv, string $root, string $vendor, PreflightLog $preflightLog): array { // Try to find the site-local Drush. If there is none, we are done. $siteLocalDrush = static::findSiteLocalDrush($root); if (!$siteLocalDrush) { - return false; + return [false, DrushCommands::EXIT_SUCCESS]; } // If the site-local Drush is us, then we do not need to redispatch. if (Path::isBasePath($vendor, $siteLocalDrush)) { - return false; + return [false, DrushCommands::EXIT_SUCCESS]; } // Do another special check to detect symlinked Drush folder similar // to what the SUT sets up for Drush functional tests. if (dirname($vendor) === dirname($siteLocalDrush)) { - return false; + return [false, DrushCommands::EXIT_SUCCESS]; } // Redispatch! @@ -58,7 +58,8 @@ function ($item) { ); $command .= ' ' . implode(' ', $args); passthru($command, $status); - return $status; + + return [true, $status]; } /** diff --git a/src/Runtime/Runtime.php b/src/Runtime/Runtime.php index 6f199f7b85..288a9cff04 100644 --- a/src/Runtime/Runtime.php +++ b/src/Runtime/Runtime.php @@ -55,11 +55,11 @@ public function run($argv): int protected function doRun($argv, $output): int { // Do the preflight steps - $status = $this->preflight->preflight($argv); + [$preflightDidRedispatch, $exitStatus] = $this->preflight->preflight($argv); // If preflight signals that we are done, then exit early. - if ($status) { - return $status; + if ($preflightDidRedispatch) { + return $exitStatus; } $commandfileSearchpath = $this->preflight->getCommandFilePaths(); diff --git a/tests/unish/Controllers/RuntimeController.php b/tests/unish/Controllers/RuntimeController.php index 1b024e581d..3d3d0569fa 100644 --- a/tests/unish/Controllers/RuntimeController.php +++ b/tests/unish/Controllers/RuntimeController.php @@ -111,11 +111,11 @@ protected function initializeRuntime($root, $argv) $di = new DependencyInjection(); // Begin our version of Runtime::doRun - $status = $this->preflight->preflight($argv); + [$preflightDidRedispatch, $exitStatus] = $this->preflight->preflight($argv); // If preflight signals that we are done, then exit early. - if ($status) { - return $status; + if ($preflightDidRedispatch) { + return $exitStatus; } $commandfileSearchpath = $this->preflight->getCommandFilePaths(); From 3d0d136ed3ceeb41d3a25e1e155b2bd655095c16 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 8 Jul 2023 09:48:18 -0400 Subject: [PATCH 10/21] Update CommandInfoAlterers docs for Drush 12. Also update Woot's CommandInfoAlterer. (#5695) * Update command info alterer docs * Also update Woot's CommandInfoAlterer --- docs/commands.md | 11 ++++++----- sut/modules/unish/woot/drush.services.yml | 5 ----- .../CommandInfoAlterers}/WootCommandInfoAlterer.php | 12 +++++++++++- tests/functional/CommandInfoAlterTest.php | 4 ++-- 4 files changed, 19 insertions(+), 13 deletions(-) rename sut/modules/unish/woot/src/{ => Drush/CommandInfoAlterers}/WootCommandInfoAlterer.php (73%) diff --git a/docs/commands.md b/docs/commands.md index e425adb3cc..cfa933dd7c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -58,13 +58,14 @@ The following are both valid ways to declare a command: - [See Attributes provided by Drush core](https://www.drush.org/api/Drush/Attributes.html). Custom code can add additional attributes. ## Altering Command Info -Drush command info (annotations/attributes) can be altered from other modules. This is done by creating and registering 'command info alterers'. Alterers are class services that are able to intercept and manipulate an existing command annotation. +Drush command info (annotations/attributes) can be altered from other modules. This is done by creating and registering _command info alterers_. Alterers are classes that are able to intercept and manipulate an existing command annotation. -In order to alter an existing command info, follow the steps below: +In the module that wants to alter a command info, add a class that: -1. In the module that wants to alter a command info, add a service class that implements the `\Consolidation\AnnotatedCommand\CommandInfoAltererInterface`. -1. In the module `drush.services.yml` declare a service pointing to this class and tag the service with the `drush.command_info_alterer` tag. -1. In that class, implement the alteration logic in the `alterCommandInfo()` method. +1. The generator class namespace, relative to base namespace, should be `Drupal\\Drush\CommandInfoAlterers` and the class file should be located under the `src/Drush/CommandInfoAlterers` directory. +1. The filename must have a name like FooCommandInfoAlterer.php. The prefix `Foo` can be whatever string you want. The file must end in `CommandInfoAlterer.php`. +1. The class must implement the `\Consolidation\AnnotatedCommand\CommandInfoAltererInterface`. +1. Implement the alteration logic in the `alterCommandInfo()` method. 1. Along with the alter code, it's strongly recommended to log a debug message explaining what exactly was altered. This makes things easier on others who may need to debug the interaction of the alter code with other modules. Also it's a good practice to inject the the logger in the class constructor. For an example, see [WootCommandInfoAlterer](https://github.com/drush-ops/drush/blob/12.x/sut/modules/unish/woot/src/WootCommandInfoAlterer.php) provided by the testing 'woot' module. diff --git a/sut/modules/unish/woot/drush.services.yml b/sut/modules/unish/woot/drush.services.yml index 2a4a09d703..e69d762857 100644 --- a/sut/modules/unish/woot/drush.services.yml +++ b/sut/modules/unish/woot/drush.services.yml @@ -17,8 +17,3 @@ services: class: Drupal\woot\Commands\AnnotatedGreetCommand tags: - { name: console.command } - woot.command_info_alter: - class: Drupal\woot\WootCommandInfoAlterer - arguments: ['@logger.factory'] - tags: - - { name: drush.command_info_alterer } diff --git a/sut/modules/unish/woot/src/WootCommandInfoAlterer.php b/sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php similarity index 73% rename from sut/modules/unish/woot/src/WootCommandInfoAlterer.php rename to sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php index fd095cc3de..1014ac8214 100644 --- a/sut/modules/unish/woot/src/WootCommandInfoAlterer.php +++ b/sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace Drupal\woot; +namespace Drupal\woot\Drush\CommandInfoAlterers; use Drupal\Core\Logger\LoggerChannelInterface; use Consolidation\AnnotatedCommand\CommandInfoAltererInterface; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; class WootCommandInfoAlterer implements CommandInfoAltererInterface { @@ -18,6 +19,15 @@ public function __construct(LoggerChannelFactoryInterface $loggerFactory) $this->logger = $loggerFactory->get('drush'); } + public static function create(ContainerInterface $container): self + { + $commandHandler = new static( + $container->get('logger.factory') + ); + + return $commandHandler; + } + public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance) { if ($commandInfo->getName() === 'woot:altered') { diff --git a/tests/functional/CommandInfoAlterTest.php b/tests/functional/CommandInfoAlterTest.php index 711d9efead..ae24b32fde 100644 --- a/tests/functional/CommandInfoAlterTest.php +++ b/tests/functional/CommandInfoAlterTest.php @@ -27,8 +27,8 @@ public function testCommandInfoAlter() $this->assertStringContainsString('woot-new-alias', $this->getOutput()); // Check the debug messages. - $this->assertStringContainsString('[debug] Commands are potentially altered in Drupal\woot\WootCommandInfoAlterer.', $this->getErrorOutput()); - $this->assertStringContainsString("[debug] Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in Drupal\woot\WootCommandInfoAlterer::alterCommandInfo().", $this->getErrorOutput()); + $this->assertStringContainsString('[debug] Commands are potentially altered in Drupal\woot\Drush\CommandInfoAlterers\WootCommandInfoAlterer.', $this->getErrorOutput()); + $this->assertStringContainsString("[debug] Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in Drupal\woot\Drush\CommandInfoAlterers\WootCommandInfoAlterer::alterCommandInfo().", $this->getErrorOutput()); // Try to run the command with the initial alias. $this->drush('woot-initial-alias', [], [], null, null, self::EXIT_ERROR); From ae6e89f06087bffdbe547483b3dbb34fcbdf9cca Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 8 Jul 2023 10:58:14 -0400 Subject: [PATCH 11/21] Add note about dir change in Drush 12 (#5696) * Add note about dir change in Drush 12 * indent --- docs/commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index cfa933dd7c..f11168bdc9 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2,7 +2,8 @@ !!! 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. See [create() method](dependency-injection.md#create-method). + 1. 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. See [create() method](dependency-injection.md#create-method). + 1. Drush 12 expects all commandfiles in the /Drush/ directory. The `Drush` subdirectory is a new requirement. Creating a new Drush command is easy. Follow the steps below. From e041f608df5e1c345db668bef71aa1bfc84e50ae Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 8 Jul 2023 11:01:02 -0400 Subject: [PATCH 12/21] Doc tweak --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index f11168bdc9..3a5f925356 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -3,7 +3,7 @@ !!! tip 1. 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. See [create() method](dependency-injection.md#create-method). - 1. Drush 12 expects all commandfiles in the /Drush/ directory. The `Drush` subdirectory is a new requirement. + 1. Drush 12 expects all commandfiles in the `/Drush/` directory. The `Drush` subdirectory is a new requirement. Creating a new Drush command is easy. Follow the steps below. From 41b0b8493019a49feed97cb75a366daebc09bec1 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 8 Jul 2023 11:03:55 -0400 Subject: [PATCH 13/21] Clarify porting changes --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 3a5f925356..39991558d3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2,7 +2,7 @@ !!! tip - 1. 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. See [create() method](dependency-injection.md#create-method). + 1. Drush 12 expects commandfiles to use a [create() method](dependency-injection.md#create-method) to inject Drupal and Drush dependencies. Prior versions used a drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files) which is now deprecated and will be removed in Drush 13. 1. Drush 12 expects all commandfiles in the `/Drush/` directory. The `Drush` subdirectory is a new requirement. Creating a new Drush command is easy. Follow the steps below. From 39ca0175c83a5f8ae4c9696e648ef67b370b97c1 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 8 Jul 2023 11:07:57 -0400 Subject: [PATCH 14/21] Fix link --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 39991558d3..dff48279e4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2,7 +2,7 @@ !!! tip - 1. Drush 12 expects commandfiles to use a [create() method](dependency-injection.md#create-method) to inject Drupal and Drush dependencies. Prior versions used a drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files) which is now deprecated and will be removed in Drush 13. + 1. Drush 12 expects commandfiles to use a [create() method](dependency-injection.md#create-method) to inject Drupal and Drush dependencies. Prior versions used a [drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files) which is now deprecated and will be removed in Drush 13. 1. Drush 12 expects all commandfiles in the `/Drush/` directory. The `Drush` subdirectory is a new requirement. Creating a new Drush command is easy. Follow the steps below. From af3d6c304cc25cbb54836a3f2c17d463adeb7a37 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 8 Jul 2023 11:34:12 -0400 Subject: [PATCH 15/21] Note Version for new twig:debug command (#5697) --- src/Commands/core/TwigCommands.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Commands/core/TwigCommands.php b/src/Commands/core/TwigCommands.php index 14d247700b..81e4fdb977 100644 --- a/src/Commands/core/TwigCommands.php +++ b/src/Commands/core/TwigCommands.php @@ -130,6 +130,7 @@ public function twigCompile(): void */ #[CLI\Command(name: self::DEBUG, aliases: ['twig-debug'])] #[CLI\Argument(name: 'mode', description: 'Debug mode. Recognized values: on, off.', suggestedValues: ['on', 'off'])] + #[CLI\Version(version: '12.1')] public function twigDebug(string $mode): void { // @todo Remove this condition once Drush drops support for Drupal 10.0. From 28d4077f7a778d466c1744bc1dfdcef663cbad56 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 10 Jul 2023 10:47:07 +0200 Subject: [PATCH 16/21] Add FieldTextHooks (#5693) --- src/Commands/field/FieldTextHooks.php | 128 ++++++++++++++++++++++++++ tests/functional/FieldTest.php | 10 ++ 2 files changed, 138 insertions(+) create mode 100644 src/Commands/field/FieldTextHooks.php diff --git a/src/Commands/field/FieldTextHooks.php b/src/Commands/field/FieldTextHooks.php new file mode 100644 index 0000000000..366cafbc3e --- /dev/null +++ b/src/Commands/field/FieldTextHooks.php @@ -0,0 +1,128 @@ +get('entity_type.manager'), + $container->get('plugin.manager.field.field_type'), + ); + } + + + #[CLI\Hook(type: HookManager::OPTION_HOOK, target: FieldCreateCommands::CREATE)] + public function hookOption(Command $command, AnnotationData $annotationData): void + { + if (!$this->hasAllowedFormats($this->input()->getOption('field-type'))) { + return; + } + + $command->addOption( + 'allowed-formats', + '', + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'Restrict which text formats are allowed, given the user has the required permissions.' + ); + } + + #[CLI\Hook(type: HookManager::ARGUMENT_VALIDATOR, target: FieldCreateCommands::CREATE)] + public function hookValidate(CommandData $commandData): void + { + if (!$this->hasAllowedFormats($commandData->input()->getOption('field-type'))) { + return; + } + + $allFormats = filter_formats(); + $allowedFormats = $this->input->getOption('allowed-formats') ?? []; + + $missingFormats = array_diff($allowedFormats, array_keys($allFormats)); + if ($missingFormats !== []) { + throw new \InvalidArgumentException(sprintf( + 'The following text formats do not exist: %s', + implode(', ', $missingFormats) + )); + } + } + + #[CLI\Hook(type: HookManager::ON_EVENT, target: 'field-create-set-options')] + public function hookSetOptions(InputInterface $input): void + { + if (!$this->hasAllowedFormats($input->getOption('field-type'))) { + return; + } + + $input->setOption( + 'allowed-formats', + $this->input->getOption('allowed-formats') ?? $this->askAllowedFormats() + ); + } + + #[CLI\Hook(type: HookManager::ON_EVENT, target: 'field-create-field-config')] + public function hookFieldConfig(array $values, InputInterface $input): array + { + if (!$this->hasAllowedFormats($values['field_type'])) { + return $values; + } + + $allowedFormats = $this->input->getOption('allowed-formats') ?? []; + $values['settings']['allowed_formats'] = $allowedFormats; + + return $values; + } + + protected function hasAllowedFormats(?string $fieldType = null): bool + { + if ($fieldType === null) { + $defaultFieldSettings = TextItemBase::defaultFieldSettings(); + } else { + $defaultFieldSettings = $this->fieldTypePluginManager->getDefaultFieldSettings($fieldType); + } + + return isset($defaultFieldSettings['allowed_formats']); + } + + /** + * Ask for the allowed formats. Only used in case the command is run interactively. + */ + protected function askAllowedFormats(): array + { + $formats = filter_formats(); + $choices = ['_none' => '- None -']; + + foreach ($formats as $format) { + $choices[$format->id()] = $format->label(); + } + + $question = (new ChoiceQuestion('Allowed formats', $choices, '_none')) + ->setMultiselect(true); + + return array_filter( + $this->io()->askQuestion($question) + ); + } +} diff --git a/tests/functional/FieldTest.php b/tests/functional/FieldTest.php index 321681a7b9..e8235ae167 100644 --- a/tests/functional/FieldTest.php +++ b/tests/functional/FieldTest.php @@ -59,6 +59,16 @@ public function testFieldCreate() $this->assertStringContainsString('Success', $this->getErrorOutputRaw()); $this->drush('field:create', ['unish_article', 'beta'], ['existing-field-name' => 'field_test3', 'field-label' => 'Body', 'field-widget' => 'text_textarea_with_summary'], null, null, self::EXIT_ERROR); $this->assertStringContainsString('Field with name \'field_test3\' already exists on bundle \'beta\'', $this->getErrorOutputRaw()); + if (version_compare(\Drupal::VERSION, '10.1.0') < 0) { + $this->markTestSkipped('Allowed formats available since Drupal 10.1.0'); + } + // Allowed formats + $this->drush('field:create', ['unish_article', 'alpha'], ['field-name' => 'field_test_allowed_formats', 'field-label' => 'Text', 'field-type' => 'string', 'allowed-formats' => 'minimal'], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('The "--allowed-formats" option does not exist.', $this->getSimplifiedErrorOutput()); + $this->drush('field:create', ['unish_article', 'alpha'], ['field-name' => 'field_test_allowed_formats', 'field-label' => 'Text', 'field-type' => 'text_long', 'cardinality' => 1, 'allowed-formats' => 'baz'], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('The following text formats do not exist: baz', $this->getSimplifiedErrorOutput()); + $this->drush('field:create', ['unish_article', 'alpha'], ['field-name' => 'field_test_allowed_formats', 'field-label' => 'Text', 'field-type' => 'text_long', 'cardinality' => 1, 'allowed-formats' => 'plain_text']); + $this->assertStringContainsString("Successfully created field 'field_test_allowed_formats' on unish_article type with bundle 'alpha'", $this->getErrorOutputRaw()); } public function testFieldInfo() From 0dce0a899b514f2e1bcbaf408c2ea6387e0437fb Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 10 Jul 2023 11:54:48 +0200 Subject: [PATCH 17/21] Fix 'TypeError: FieldBaseOverrideCreateCommands::askFieldDescription(): Argument #1 ($default) must be of type ?string, TranslatableMarkup given' (#5699) --- src/Commands/field/FieldBaseOverrideCreateCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/field/FieldBaseOverrideCreateCommands.php b/src/Commands/field/FieldBaseOverrideCreateCommands.php index 3f337b7db9..c286fe3522 100644 --- a/src/Commands/field/FieldBaseOverrideCreateCommands.php +++ b/src/Commands/field/FieldBaseOverrideCreateCommands.php @@ -101,7 +101,7 @@ public function baseOverrideCreateField(?string $entityType = null, ?string $bun ); $this->input->setOption( 'field-description', - $this->input->getOption('field-description') ?? $this->askFieldDescription($definition->getDescription()) + $this->input->getOption('field-description') ?? $this->askFieldDescription((string) $definition->getDescription()) ); $this->input->setOption( 'is-required', From e82a8fd0ea69195c8505df72be795ee0005f9b8b Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 10 Jul 2023 12:01:03 +0200 Subject: [PATCH 18/21] Fix allowed formats not being asked in interactive context --- src/Commands/field/FieldTextHooks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/field/FieldTextHooks.php b/src/Commands/field/FieldTextHooks.php index 366cafbc3e..c558d29a81 100644 --- a/src/Commands/field/FieldTextHooks.php +++ b/src/Commands/field/FieldTextHooks.php @@ -78,7 +78,7 @@ public function hookSetOptions(InputInterface $input): void $input->setOption( 'allowed-formats', - $this->input->getOption('allowed-formats') ?? $this->askAllowedFormats() + $this->input->getOption('allowed-formats') ?: $this->askAllowedFormats() ); } From 33939f18a0651bb16424760bb91683dbaf2478e5 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 10 Jul 2023 08:51:29 -0400 Subject: [PATCH 19/21] Defensive code in options hook for field:create (#5700) --- src/Commands/field/FieldTextHooks.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Commands/field/FieldTextHooks.php b/src/Commands/field/FieldTextHooks.php index c558d29a81..ed6070c980 100644 --- a/src/Commands/field/FieldTextHooks.php +++ b/src/Commands/field/FieldTextHooks.php @@ -38,6 +38,11 @@ public static function create(ContainerInterface $container): static #[CLI\Hook(type: HookManager::OPTION_HOOK, target: FieldCreateCommands::CREATE)] public function hookOption(Command $command, AnnotationData $annotationData): void { + // The options hook is called by mk:docs - avoid an exception. + if (!$this->input()->hasOption('field-type')) { + return; + } + if (!$this->hasAllowedFormats($this->input()->getOption('field-type'))) { return; } From bb619469b9bfceef87340c387cc37d3a2820598e Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 10 Jul 2023 16:16:18 -0400 Subject: [PATCH 20/21] Fix image:flush --all (#5702) * Fix image:flush --all * Actually use postInit --- src/Commands/core/ImageCommands.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Commands/core/ImageCommands.php b/src/Commands/core/ImageCommands.php index 7211402125..7ddd69e9b7 100644 --- a/src/Commands/core/ImageCommands.php +++ b/src/Commands/core/ImageCommands.php @@ -39,7 +39,7 @@ public function flush($style_names, $options = ['all' => false]): void } #[CLI\Hook(type: HookManager::INTERACT, target: self::FLUSH)] - public function interactFlush($input, $output): void + public function interactFlush(InputInterface $input, $output): void { $styles = array_keys(ImageStyle::loadMultiple()); $style_names = $input->getArgument('style_names'); @@ -56,10 +56,10 @@ public function interactFlush($input, $output): void } } - #[CLI\Hook(type: HookManager::INITIALIZE, target: self::FLUSH)] - public function initFlush(InputInterface $input, AnnotationData $annotationData): void + #[CLI\Hook(type: HookManager::POST_INITIALIZE, target: self::FLUSH)] + public function postInit(InputInterface $input, AnnotationData $annotationData): void { - // Needed for non-interactive calls. + // Needed for non-interactive calls.We use post-init phase because interact() methods run early if ($input->getOption('all')) { $styles = array_keys(ImageStyle::loadMultiple()); $input->setArgument('style_names', implode(",", $styles)); From 185ebd0d15bd8d2647821cc20eee0b82bf00ff4b Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 11 Jul 2023 10:22:11 -0400 Subject: [PATCH 21/21] Default to render cache clear (#5704) --- src/Commands/core/CacheCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/core/CacheCommands.php b/src/Commands/core/CacheCommands.php index 313b780965..165cb3b68b 100644 --- a/src/Commands/core/CacheCommands.php +++ b/src/Commands/core/CacheCommands.php @@ -141,7 +141,7 @@ public function interact($input, $output): void if (empty($input->getArgument('type'))) { $types = $this->getTypes($this->bootstrapManager->hasBootstrapped(DrupalBootLevels::FULL)); $choices = array_combine(array_keys($types), array_keys($types)); - $type = $this->io()->choice(dt("Choose a cache to clear"), $choices, 'all'); + $type = $this->io()->choice(dt("Choose a cache to clear"), $choices, 'render'); $input->setArgument('type', $type); }