From 2f63ffcef88c57d2d4aef9c8e351beb304254beb Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 10:15:35 -0400 Subject: [PATCH 01/48] Deprecate AnnotatedCommands in favor of native Symfony Console commands --- src/Application.php | 14 +++ src/Attributes/DefaultFields.php | 21 +++- src/Attributes/DefaultTableFields.php | 20 +++- src/Attributes/FieldLabels.php | 24 ++++- src/Attributes/FilterDefaultField.php | 23 +++- src/Attributes/Format.php | 1 - src/Attributes/ValidateModulesEnabled.php | 2 +- src/Boot/DrupalBoot8.php | 24 ++++- src/Commands/core/CoreCommands.php | 4 +- src/Commands/core/TwigCommands.php | 49 --------- src/Commands/core/TwigUnusedCommand.php | 100 ++++++++++++++++++ src/Commands/help/HelpCLIFormatter.php | 6 +- src/Event/ConsoleDefinitionsEvent.php | 28 +++++ .../ValidateModulesEnabledListener.php | 45 ++++++++ ...tterConfigurationItemProviderInterface.php | 11 ++ src/Formatters/FormatterTrait.php | 67 ++++++++++++ src/Runtime/DependencyInjection.php | 13 ++- src/Runtime/ServiceManager.php | 89 +++++++++++++++- .../WootCommandInfoAlterer.php | 31 ------ .../EventListener/WootDefinitionListener.php | 31 ++++++ 20 files changed, 493 insertions(+), 110 deletions(-) create mode 100644 src/Commands/core/TwigUnusedCommand.php create mode 100644 src/Event/ConsoleDefinitionsEvent.php create mode 100644 src/EventListener/ValidateModulesEnabledListener.php create mode 100644 src/Formatters/FormatterConfigurationItemProviderInterface.php create mode 100644 src/Formatters/FormatterTrait.php delete mode 100644 sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php create mode 100644 sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php diff --git a/src/Application.php b/src/Application.php index cf9170ab36..c686f9a11e 100644 --- a/src/Application.php +++ b/src/Application.php @@ -11,6 +11,7 @@ use Drush\Boot\DrupalBootLevels; use Drush\Command\RemoteCommandProxy; use Drush\Config\ConfigAwareTrait; +use Drush\Event\ConsoleDefinitionsEvent; use Drush\Runtime\RedispatchHook; use Drush\Runtime\ServiceManager; use Drush\Runtime\TildeExpansionHook; @@ -309,6 +310,8 @@ public function configureAndRegisterCommands(InputInterface $input, OutputInterf // any of the configuration steps we do here. $this->configureIO($input, $output); + $this->addListeners($commandfileSearchpath); + // Directly add the yaml-cli commands. $this->addCommands($this->serviceManager->instantiateYamlCliCommands()); @@ -327,6 +330,9 @@ public function configureAndRegisterCommands(InputInterface $input, OutputInterf // Note that Robo::register can accept either Annotated Command // command handlers or Symfony Console Command objects. Robo::register($this, $commandInstances); + + // Dispatch our custom event. It also fires later in \Drush\Boot\DrupalBoot8::bootstrapDrupalFull. + Drush::getContainer()->get('eventDispatcher')->dispatch(new ConsoleDefinitionsEvent($this), ConsoleDefinitionsEvent::class); } /** @@ -338,4 +344,12 @@ public function renderThrowable(\Throwable $e, OutputInterface $output): void $this->doRenderThrowable($e, $output); } + + // Discover event listeners, and add those that do not require bootstrap. + protected function addListeners($commandfileSearchpath): void + { + $listenerClasses = $this->serviceManager->discoverListeners($commandfileSearchpath, '\Drush'); + $listenerClasses = $this->serviceManager->filterListeners($listenerClasses); + $this->serviceManager->addListeners($listenerClasses, Drush::getContainer()); + } } diff --git a/src/Attributes/DefaultFields.php b/src/Attributes/DefaultFields.php index 58b0e02a9f..7c09c7cada 100644 --- a/src/Attributes/DefaultFields.php +++ b/src/Attributes/DefaultFields.php @@ -5,8 +5,25 @@ namespace Drush\Attributes; use Attribute; +use Drush\Formatters\FormatterConfigurationItemProviderInterface; -#[Attribute(Attribute::TARGET_METHOD)] -class DefaultFields extends \Consolidation\AnnotatedCommand\Attributes\DefaultFields +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +class DefaultFields implements FormatterConfigurationItemProviderInterface { + const KEY = 'default-fields'; + + /** + * @param $fields + * An array of field names to show by default. + */ + public function __construct(public array $fields) + { + } + + public function getConfigurationItem(\ReflectionAttribute $attribute): array + { + $args = $attribute->getArguments(); + return [self::KEY => $args['fields']]; + } + } diff --git a/src/Attributes/DefaultTableFields.php b/src/Attributes/DefaultTableFields.php index 7e340474bf..90baf35226 100644 --- a/src/Attributes/DefaultTableFields.php +++ b/src/Attributes/DefaultTableFields.php @@ -5,8 +5,24 @@ namespace Drush\Attributes; use Attribute; +use Drush\Formatters\FormatterConfigurationItemProviderInterface; -#[Attribute(Attribute::TARGET_METHOD)] -class DefaultTableFields extends \Consolidation\AnnotatedCommand\Attributes\DefaultTableFields +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +class DefaultTableFields implements FormatterConfigurationItemProviderInterface { + const KEY = 'default-table-fields'; + + /** + * @param $fields + * An array of field names to show by default when using table formatter. + */ + public function __construct(public array $fields) + { + } + + public function getConfigurationItem(\ReflectionAttribute $attribute): array + { + $args = $attribute->getArguments(); + return [self::KEY => $args['fields']]; + } } diff --git a/src/Attributes/FieldLabels.php b/src/Attributes/FieldLabels.php index 6e7a26a1b2..0ed2a9b06d 100644 --- a/src/Attributes/FieldLabels.php +++ b/src/Attributes/FieldLabels.php @@ -1,12 +1,28 @@ getArguments(); + return [self::KEY => $args['labels']]; + } } diff --git a/src/Attributes/FilterDefaultField.php b/src/Attributes/FilterDefaultField.php index 70d2749cc0..fe639f07df 100644 --- a/src/Attributes/FilterDefaultField.php +++ b/src/Attributes/FilterDefaultField.php @@ -1,12 +1,27 @@ getArguments(); + return [self::KEY => $args['field']]; + } } diff --git a/src/Attributes/Format.php b/src/Attributes/Format.php index 0c121d59c1..b2f16bc01e 100644 --- a/src/Attributes/Format.php +++ b/src/Attributes/Format.php @@ -7,7 +7,6 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Consolidation\OutputFormatters\Options\FormatterOptions; -use Drush\Boot\Kernels; use JetBrains\PhpStorm\ExpectedValues; #[Attribute(Attribute::TARGET_METHOD)] diff --git a/src/Attributes/ValidateModulesEnabled.php b/src/Attributes/ValidateModulesEnabled.php index d3cd6b1267..a9d5751e49 100644 --- a/src/Attributes/ValidateModulesEnabled.php +++ b/src/Attributes/ValidateModulesEnabled.php @@ -8,7 +8,7 @@ use Consolidation\AnnotatedCommand\CommandData; use Consolidation\AnnotatedCommand\CommandError; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class ValidateModulesEnabled extends ValidatorBase implements ValidatorInterface { /** diff --git a/src/Boot/DrupalBoot8.php b/src/Boot/DrupalBoot8.php index 496deb9625..bf7aabc747 100644 --- a/src/Boot/DrupalBoot8.php +++ b/src/Boot/DrupalBoot8.php @@ -13,11 +13,10 @@ use Drush\Config\ConfigLocator; use Drush\Drupal\DrushLoggerServiceProvider; use Drush\Drush; +use Drush\Event\ConsoleDefinitionsEvent; use Drush\Runtime\LegacyServiceFinder; use Drush\Runtime\LegacyServiceInstantiator; use Drush\Runtime\ServiceManager; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerInterface; use Robo\Robo; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -215,12 +214,20 @@ public function bootstrapDrupalFull(BootstrapManager $manager): void // Directly add the Drupal core bootstrapped commands. Drush::getApplication()->addCommands($this->serviceManager->instantiateDrupalCoreBootstrappedCommands()); + $this->addBootstrapListeners(); + $this->addDrupalModuleDrushCommands($manager); + // Dispatch our custom event. It also fires earlier in \Drush\Application::configureAndRegisterCommands. + Drush::getContainer()->get('eventDispatcher')->dispatch(new ConsoleDefinitionsEvent(Drush::getApplication()), ConsoleDefinitionsEvent::class); + // Set a default account to make sure the correct timezone is set $this->kernel->getContainer()->get('current_user')->setAccount(new AnonymousUserSession()); } + /** + * Adds module supplied commands, as well as Drush Console commands that require bootstrap. + */ public function addDrupalModuleDrushCommands(BootstrapManager $manager): void { $application = Drush::getApplication(); @@ -323,4 +330,17 @@ public function bootstrapDrupalSite(BootstrapManager $manager) { $this->bootstrapDoDrupalSite($manager); } + + // Add the Listeners that require bootstrap. + public function addBootstrapListeners(): void + { + $listenersInThisModule = []; + $moduleHandler = \Drupal::moduleHandler(); + foreach ($moduleHandler->getModuleList() as $moduleId => $extension) { + $path = DRUPAL_ROOT . '/' . $extension->getPath() . '/src/Drush'; + $listenersInThisModule = array_merge($listenersInThisModule, $this->serviceManager->discoverListeners([$path], "\Drupal\\$moduleId\Drush")); + } + $classes = $this->serviceManager->bootstrapListenerClasses(); + $this->serviceManager->addListeners(array_merge($listenersInThisModule, $classes), Drush::getContainer(), \Drupal::getContainer()); + } } diff --git a/src/Commands/core/CoreCommands.php b/src/Commands/core/CoreCommands.php index 792a5726e0..9fa9d11db9 100644 --- a/src/Commands/core/CoreCommands.php +++ b/src/Commands/core/CoreCommands.php @@ -4,12 +4,12 @@ namespace Drush\Commands\core; +use Consolidation\OutputFormatters\Options\FormatterOptions; use Consolidation\OutputFormatters\StructuredData\PropertyList; +use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drush\Attributes as CLI; use Drush\Commands\DrushCommands; use Drush\Drush; -use Consolidation\OutputFormatters\StructuredData\RowsOfFields; -use Consolidation\OutputFormatters\Options\FormatterOptions; final class CoreCommands extends DrushCommands { diff --git a/src/Commands/core/TwigCommands.php b/src/Commands/core/TwigCommands.php index 001ff11ddb..2df9a90a65 100644 --- a/src/Commands/core/TwigCommands.php +++ b/src/Commands/core/TwigCommands.php @@ -4,18 +4,15 @@ namespace Drush\Commands\core; -use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drupal\Core\DrupalKernelInterface; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\State\StateInterface; use Drupal\Core\Template\TwigEnvironment; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; use Drush\Commands\DrushCommands; use Drush\Drush; -use Drush\Utils\StringUtils; use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; @@ -23,7 +20,6 @@ final class TwigCommands extends DrushCommands { use AutowireTrait; - const UNUSED = 'twig:unused'; const COMPILE = 'twig:compile'; const DEBUG = 'twig:debug'; @@ -36,51 +32,6 @@ public function __construct( ) { } - /** - * Find potentially unused Twig templates. - * - * Immediately before running this command, web crawl your entire web site. Or - * use your Production PHPStorage dir for comparison. - */ - #[CLI\Command(name: self::UNUSED, aliases: [])] - #[CLI\Argument(name: 'searchpaths', description: 'A comma delimited list of paths to recursively search')] - #[CLI\Usage(name: 'drush twig:unused --field=template /var/www/mass.local/docroot/modules/custom,/var/www/mass.local/docroot/themes/custom', description: 'Output a simple list of potentially unused templates.')] - #[CLI\FieldLabels(labels: ['template' => 'Template', 'compiled' => 'Compiled'])] - #[CLI\DefaultTableFields(fields: ['template', 'compiled'])] - public function unused($searchpaths): RowsOfFields - { - $unused = []; - $phpstorage = PhpStorageFactory::get('twig'); - - // Find all templates in the codebase. - $files = Finder::create() - ->files() - ->name('*.html.twig') - ->exclude('tests') - ->in(StringUtils::csvToArray($searchpaths)); - $this->logger()->notice(dt('Found !count templates', ['!count' => count($files)])); - - // Check to see if a compiled equivalent exists in PHPStorage - foreach ($files as $file) { - $relative = Path::makeRelative($file->getRealPath(), Drush::bootstrapManager()->getRoot()); - $mainCls = $this->twig->getTemplateClass($relative); - $cache = $this->twig->getCache(); - if ($cache) { - $key = $cache->generateKey($relative, $mainCls); - if (!$phpstorage->exists($key)) { - $unused[$key] = [ - 'template' => $relative, - 'compiled' => $key, - ]; - } - } else { - throw new \Exception('There was a problem, please ensure your twig cache is enabled.'); - } - } - $this->logger()->notice(dt('Found !count unused', ['!count' => count($unused)])); - return new RowsOfFields($unused); - } - /** * Compile all Twig template(s). */ diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php new file mode 100644 index 0000000000..565f839e49 --- /dev/null +++ b/src/Commands/core/TwigUnusedCommand.php @@ -0,0 +1,100 @@ + 'Template', 'compiled' => 'Compiled'])] +#[CLI\DefaultTableFields(fields: ['template', 'compiled'])] +final class TwigUnusedCommand extends Command +{ + use AutowireTrait; + use FormatterTrait; + + const UNUSED = 'twig:unused'; + + public function __construct( + protected readonly FormatterManager $formatterManager, + protected readonly BootstrapManager $bootstrapManager, + protected readonly TwigEnvironment $twig, + protected readonly ModuleHandlerInterface $moduleHandler, + private readonly ModuleExtensionList $extensionList, + private readonly StateInterface $state, + private readonly DrupalKernelInterface $kernel, + private readonly LoggerInterface $logger + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('searchpaths', InputArgument::REQUIRED, 'A comma delimited list of paths to recursively search') + // Usages can't have a description with plain Console :(. Use setHelp() if desired as per https://github.com/symfony/symfony/issues/45050 + ->addUsage('twig:unused /var/www/mass.local/docroot/modules/custom,/var/www/mass.local/docroot/themes/custom') + ->setHelp('Immediately before running this command, web crawl your entire web site. Or use your Production PHPStorage dir for comparison.'); + $this->addFormatterOptions(); + } + + public function doExecute(InputInterface $input, OutputInterface $output): RowsOfFields + { + $searchpaths = $input->getArgument('searchpaths'); + $unused = []; + $phpstorage = PhpStorageFactory::get('twig'); + + // Find all templates in the codebase. + $files = Finder::create() + ->files() + ->name('*.html.twig') + ->exclude('tests') + ->in(StringUtils::csvToArray($searchpaths)); + $this->logger->notice(dt('Found !count templates', ['!count' => count($files)])); + + // Check to see if a compiled equivalent exists in PHPStorage + foreach ($files as $file) { + $relative = Path::makeRelative($file->getRealPath(), $this->bootstrapManager->getRoot()); + $mainCls = $this->twig->getTemplateClass($relative); + $cache = $this->twig->getCache(); + if ($cache) { + $key = $cache->generateKey($relative, $mainCls); + if (!$phpstorage->exists($key)) { + $unused[$key] = [ + 'template' => $relative, + 'compiled' => $key, + ]; + } + } else { + throw new \Exception('There was a problem, please ensure your twig cache is enabled.'); + } + } + $this->logger->notice(dt('Found !count unused', ['!count' => count($unused)])); + return new RowsOfFields($unused); + } +} diff --git a/src/Commands/help/HelpCLIFormatter.php b/src/Commands/help/HelpCLIFormatter.php index fbc4a48982..20ba0e0ac6 100644 --- a/src/Commands/help/HelpCLIFormatter.php +++ b/src/Commands/help/HelpCLIFormatter.php @@ -42,9 +42,9 @@ public function write(OutputInterface $output, $data, FormatterOptions $options) } } elseif (array_key_exists('usages', $data)) { // Usages come from Console commands. - // Don't show the last two Usages which come from synopsis and alias. See \Symfony\Component\Console\Descriptor\XmlDescriptor::getCommandDocument. - array_pop($data['usages']); - array_pop($data['usages']); + // Don't show the first two Usages which come from synopsis and alias. See \Symfony\Component\Console\Descriptor\XmlDescriptor::getCommandDocument. + array_shift($data['usages']); + array_shift($data['usages']); if ($data['usages']) { $output->writeln(''); $output->writeln('Examples:'); diff --git a/src/Event/ConsoleDefinitionsEvent.php b/src/Event/ConsoleDefinitionsEvent.php new file mode 100644 index 0000000000..63811117cc --- /dev/null +++ b/src/Event/ConsoleDefinitionsEvent.php @@ -0,0 +1,28 @@ +application = $application; + } + + public function getApplication(): Application + { + return $this->application; + } +} diff --git a/src/EventListener/ValidateModulesEnabledListener.php b/src/EventListener/ValidateModulesEnabledListener.php new file mode 100644 index 0000000000..1d4dc600e6 --- /dev/null +++ b/src/EventListener/ValidateModulesEnabledListener.php @@ -0,0 +1,45 @@ +getCommand(); + $reflection = new \ReflectionObject($command); + $attributes = $reflection->getAttributes(ValidateModulesEnabled::class); + if (empty($attributes)) { + return; + } + $instance = $attributes[0]->newInstance(); + $missing = array_filter($instance->modules, fn($module) => !$this->moduleHandler->moduleExists($module)); + if ($missing) { + $message = dt('The following modules are required: !modules', ['!modules' => implode(', ', $missing)]); + $this->logger->error($message); + $event->disableCommand(); + } + } +} diff --git a/src/Formatters/FormatterConfigurationItemProviderInterface.php b/src/Formatters/FormatterConfigurationItemProviderInterface.php new file mode 100644 index 0000000000..35810c7504 --- /dev/null +++ b/src/Formatters/FormatterConfigurationItemProviderInterface.php @@ -0,0 +1,11 @@ +getConfigurationData(), []); + $reflection = new \ReflectionMethod($this, 'doExecute'); + $inputOptions = $this->formatterManager->automaticOptions($formatterOptions, $reflection->getReturnType()->getName()); + foreach ($inputOptions as $inputOption) { + $mode = $this->getPrivatePropValue($inputOption, 'mode'); + $suggestedValues = $this->getPrivatePropValue($inputOption, 'suggestedValues'); + $this->addOption($inputOption->getName(), $inputOption->getShortcut(), $mode, $inputOption->getDescription(), $inputOption->getDefault(), $suggestedValues); + } + } + + /** + * Format the structured data as per user input and the command definition. + */ + public function execute(InputInterface $input, OutputInterface $output): int + { + $configurationData = $this->getConfigurationData($this); + $formatterOptions = new FormatterOptions($configurationData, $input->getOptions()); + $formatterOptions->setInput($input); + $data = $this->doExecute($input, $output); + $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); + return Command::SUCCESS; + } + + protected function getPrivatePropValue(mixed $object, $name): mixed + { + $rc = new \ReflectionClass($object); + $prop = $rc->getProperty($name); + return $prop->getValue($object); + } + + /** + * Build the formatter configuration from the command's attributes + */ + protected function getConfigurationData(): array + { + $configurationData = []; + $reflection = new \ReflectionObject($this); + $attributes = $reflection->getAttributes(); + foreach ($attributes as $attribute) { + $instance = $attribute->newInstance(); + if ($instance instanceof FormatterConfigurationItemProviderInterface) { + $configurationData = array_merge($configurationData, $instance->getConfigurationItem($attribute)); + } + } + return $configurationData; + } + + /** + * Override this method with the actual command logic. Type hint the return value + * to help the formatter know what to expect. + */ + abstract protected function doExecute(InputInterface $input, OutputInterface $output); +} diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 71d6f13eb0..8de4048cb0 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -7,6 +7,7 @@ use Composer\Autoload\ClassLoader; use Consolidation\Config\ConfigInterface; use Consolidation\Config\Util\ConfigOverlay; +use Consolidation\OutputFormatters\FormatterManager; use Consolidation\SiteAlias\SiteAliasManager; use Consolidation\SiteAlias\SiteAliasManagerAwareInterface; use Consolidation\SiteAlias\SiteAliasManagerInterface; @@ -26,6 +27,7 @@ use Drush\Symfony\DrushStyleInjector; use League\Container\Container; use League\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use Robo\Robo; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -35,6 +37,7 @@ */ class DependencyInjection { + const FORMATTER_MANAGER = 'formatterManager'; const SITE_ALIAS_MANAGER = 'site.alias.manager'; const BOOTSTRAP_MANAGER = 'bootstrap.manager'; const LOADER = 'loader'; @@ -92,12 +95,13 @@ public function installHandlers($container): void } // Add Drush Services to league/container 3.x - protected function addDrushServices($container, ClassLoader $loader, DrushDrupalFinder $drupalFinder, SiteAliasManager $aliasManager, DrushConfig $config, OutputInterface $output): void + protected function addDrushServices(Container $container, ClassLoader $loader, DrushDrupalFinder $drupalFinder, SiteAliasManager $aliasManager, DrushConfig $config, OutputInterface $output): void { // Override Robo's logger with a LoggerManager that delegates to the Drush logger. Robo::addShared($container, 'logger', '\Drush\Log\DrushLoggerManager') - ->addMethodCall('setLogOutputStyler', ['logStyler']) - ->addMethodCall('add', ['drush', new Logger($output)]); + ->addMethodCall('setLogOutputStyler', ['logStyler']) + ->addMethodCall('add', ['drush', new Logger($output)]); + Robo::addShared($container, LoggerInterface::class, 'logger'); // For autowiring Robo::addShared($container, self::LOADER, $loader); Robo::addShared($container, ClassLoader::class, self::LOADER); // For autowiring @@ -110,10 +114,11 @@ protected function addDrushServices($container, ClassLoader $loader, DrushDrupal // Override Robo's formatter manager with our own // @todo not sure that we'll use this. Maybe remove it. - Robo::addShared($container, 'formatterManager', DrushFormatterManager::class) + Robo::addShared($container, self::FORMATTER_MANAGER, DrushFormatterManager::class) ->addMethodCall('addDefaultFormatters', []) ->addMethodCall('addDefaultSimplifiers', []) ->addMethodCall('addSimplifier', [new EntityToArraySimplifier()]); + Robo::addShared($container, FormatterManager::class, self::FORMATTER_MANAGER); // For autowiring // 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 894fe5bf8b..b67b27db44 100644 --- a/src/Runtime/ServiceManager.php +++ b/src/Runtime/ServiceManager.php @@ -18,19 +18,24 @@ use Drush\Boot\DrupalBootLevels; use Drush\Commands\DrushCommands; use Drush\Config\DrushConfig; +use Drush\Drush; use Grasmash\YamlCli\Command\GetValueCommand; use Grasmash\YamlCli\Command\LintCommand; use Grasmash\YamlCli\Command\UnsetKeyCommand; use Grasmash\YamlCli\Command\UpdateKeyCommand; use Grasmash\YamlCli\Command\UpdateValueCommand; use League\Container\Container as DrushContainer; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Robo\ClassDiscovery\RelativeNamespaceDiscovery; use Robo\Contract\ConfigAwareInterface; use Robo\Contract\OutputAwareInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** * Manage Drush services. @@ -54,6 +59,9 @@ class ServiceManager /** @var string[] */ protected array $bootstrapCommandClasses = []; + /** @var string[] */ + protected array $bootstrapListenerClasses = []; + public function __construct( protected ClassLoader $autoloader, protected DrushConfig $config, @@ -87,6 +95,17 @@ public function bootstrapCommandClasses(): array return $this->bootstrapCommandClasses; } + /** + * Return cached of deferred scubscriber objects. + * + * @return string[] + * List of class names to instantiate at bootstrap time. + */ + public function bootstrapListenerClasses(): array + { + return $this->bootstrapListenerClasses; + } + /** * Discover all of the different kinds of command handler objects * in the places where Drush can find them. Called during preflight; @@ -95,7 +114,7 @@ public function bootstrapCommandClasses(): array * * @param string[] $commandfileSearchpath 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[] * List of command classes @@ -261,6 +280,32 @@ public function discoverModuleCommandInfoAlterers(array $directoryList, string $ return array_values($commandClasses); } + /** + * Discovers Listener classes from a provided search path. + * + * @param string[] $directoryList List of directories to search + * @param string $baseNamespace The namespace to use at the base of each + * search directory. Namespace components mirror directory structure. + * + * @return string[] + * List Listeners. + */ + public function discoverListeners(array $directoryList, string $baseNamespace): array + { + $discovery = new CommandFileDiscovery(); + $discovery + ->setIncludeFilesAtBase(true) + ->setSearchDepth(3) + ->ignoreNamespacePart('contrib', 'Listeners') + ->ignoreNamespacePart('custom', 'Listeners') + ->ignoreNamespacePart('src') + ->setSearchLocations(['EventListener']) + ->setSearchPattern('#.*(Listener)s?.php$#'); + $baseNamespace = ltrim($baseNamespace, '\\'); + $listenerClasses = $discovery->discover($directoryList, $baseNamespace); + return array_values($listenerClasses); + } + /** * Instantiate commands from Grasmash\YamlCli that we want to expose * as Drush commands. @@ -318,12 +363,12 @@ public function instantiateDrupalCoreBootstrappedCommands(): array * Drupal and Drush DI containers. If there is no static factory, then * instantiate it via 'new $class' * - * @param string[] $bootstrapCommandClasses Classes to instantiate. + * @param string[] $serviceClasses Classes to instantiate. * * @return object[] * List of instantiated service objects */ - public function instantiateServices(array $bootstrapCommandClasses, DrushContainer $drushContainer, ?DrupalContainer $container = null): array + public function instantiateServices(array $serviceClasses, DrushContainer $drushContainer, ?DrupalContainer $container = null): array { $commandHandlers = []; @@ -331,7 +376,7 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain // particularly DrushCommands (our abstract base class). // n.b. we cannot simply use 'isInstantiable' here because // the constructor is typically protected when using a static create method - $bootstrapCommandClasses = array_filter($bootstrapCommandClasses, function ($class) { + $serviceClasses = array_filter($serviceClasses, function ($class) { try { $reflection = new \ReflectionClass($class); } catch (\Throwable $e) { @@ -345,7 +390,7 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain // Combine the two containers. $drushContainer->delegate($container); } - foreach ($bootstrapCommandClasses as $class) { + foreach ($serviceClasses as $class) { $commandHandler = null; try { @@ -372,6 +417,30 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain return $commandHandlers; } + /** + * Add listeners to Drush's event dispatcher. + */ + public function addListeners(iterable $classes, ContainerInterface $drushContainer, ?ContainerInterface $drupalContainer = null): void + { + $instances = $this->instantiateServices($classes, $drushContainer, $drupalContainer); + foreach ($instances as $instance) { + $reflectionObject = new \ReflectionObject($instance); + $attributes = $reflectionObject->getAttributes(AsEventListener::class); + foreach ($attributes as $attribute) { + $attributeInstance = $attribute->newInstance(); + $method = $attributeInstance->method ?? '__invoke'; + $priority = $attributeInstance->priority ?? 0; + $reflectionMethod = $reflectionObject->getMethod($method); + $reflectionParameters = $reflectionMethod->getParameters(); + $eventName = $reflectionParameters[0]->getType()->getName(); + if ($eventName == ConsoleCommandEvent::class) { + $eventName = ConsoleEvents::COMMAND; + } + Drush::getContainer()->get('eventDispatcher')->addListener($eventName, $instance->$method(...), $priority); + } + } + } + /** * Determine if the first parameter of the create method supports our container with delegate. */ @@ -406,6 +475,16 @@ protected function bootStrapAttributeValue(string $class): ?int return null; } + // If a command class has a Bootstrap Attribute or static `create` method, we + // postpone instantiating it until after we bootstrap Drupal. + public function filterListeners($listenClasses): array + { + $this->bootstrapListenerClasses = array_filter($listenClasses, [$this, 'requiresBootstrap']); + + // Remove the listener classes that we put into the bootstrap listener classes. + return array_diff($listenClasses, $this->bootstrapListenerClasses); + } + /** * Check whether a command class requires Drupal bootstrap. */ diff --git a/sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php b/sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php deleted file mode 100644 index e2cfc6a495..0000000000 --- a/sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php +++ /dev/null @@ -1,31 +0,0 @@ -logger = $loggerFactory->get('drush'); - } - - public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance) - { - if ($commandInfo->getName() === 'woot:altered') { - $commandInfo->setAliases('woot-new-alias'); - $this->logger->debug(dt("Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in " . __METHOD__ . '().')); - } - } -} diff --git a/sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php b/sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php new file mode 100644 index 0000000000..8ec647f193 --- /dev/null +++ b/sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php @@ -0,0 +1,31 @@ +getApplication()->all() as $id => $command) { + if ($command->getName() === 'woot:altered') { + $command->setAliases(['woot-new-alias']); + $this->logger->debug(dt("Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in " . __METHOD__ . '().')); + } + } + } +} From ec1f51289d406b72ef00955d9ff2b18d677bb312 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 12:46:09 -0400 Subject: [PATCH 02/48] PHPCS and rename directory to just Listeners (was EventListener) --- src/Attributes/DefaultFields.php | 1 - src/Attributes/FieldLabels.php | 1 - .../FormatterConfigurationItemProviderInterface.php | 5 ++--- src/Formatters/FormatterTrait.php | 1 - .../ValidateModulesEnabledListener.php | 4 ++-- src/Runtime/ServiceManager.php | 2 +- .../{EventListener => Listeners}/WootDefinitionListener.php | 2 +- tests/functional/CommandInfoAlterTest.php | 3 +-- 8 files changed, 7 insertions(+), 12 deletions(-) rename src/{EventListener => Listeners}/ValidateModulesEnabledListener.php (96%) rename sut/modules/unish/woot/src/Drush/{EventListener => Listeners}/WootDefinitionListener.php (94%) diff --git a/src/Attributes/DefaultFields.php b/src/Attributes/DefaultFields.php index 7c09c7cada..0dc23ac7d8 100644 --- a/src/Attributes/DefaultFields.php +++ b/src/Attributes/DefaultFields.php @@ -25,5 +25,4 @@ public function getConfigurationItem(\ReflectionAttribute $attribute): array $args = $attribute->getArguments(); return [self::KEY => $args['fields']]; } - } diff --git a/src/Attributes/FieldLabels.php b/src/Attributes/FieldLabels.php index 0ed2a9b06d..8e0962ccca 100644 --- a/src/Attributes/FieldLabels.php +++ b/src/Attributes/FieldLabels.php @@ -8,7 +8,6 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class FieldLabels implements FormatterConfigurationItemProviderInterface { - const KEY = 'field-labels'; /** diff --git a/src/Formatters/FormatterConfigurationItemProviderInterface.php b/src/Formatters/FormatterConfigurationItemProviderInterface.php index 35810c7504..78314cad85 100644 --- a/src/Formatters/FormatterConfigurationItemProviderInterface.php +++ b/src/Formatters/FormatterConfigurationItemProviderInterface.php @@ -2,10 +2,9 @@ namespace Drush\Formatters; -interface FormatterConfigurationItemProviderInterface { - +interface FormatterConfigurationItemProviderInterface +{ const KEY = ''; public function getConfigurationItem(\ReflectionAttribute $attribute): array; - } diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index 29f3edbb6f..70cd8f380f 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -7,7 +7,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - trait FormatterTrait { public function addFormatterOptions() diff --git a/src/EventListener/ValidateModulesEnabledListener.php b/src/Listeners/ValidateModulesEnabledListener.php similarity index 96% rename from src/EventListener/ValidateModulesEnabledListener.php rename to src/Listeners/ValidateModulesEnabledListener.php index 1d4dc600e6..50a0f8ea69 100644 --- a/src/EventListener/ValidateModulesEnabledListener.php +++ b/src/Listeners/ValidateModulesEnabledListener.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drush\EventListener; +namespace Drush\Listeners; use Drupal\Core\Extension\ModuleHandlerInterface; use Drush\Attributes\ValidateModulesEnabled; @@ -23,7 +23,7 @@ public function __construct( } /** - * This subscriber affects commands which put #[ValidateModulesEnabled] on the class. + * This subscriber affects commands which put #[ValidateModulesEnabled] on the *class*. * Method usages are enforced by Annotated Command still. */ public function __invoke(ConsoleCommandEvent $event): void diff --git a/src/Runtime/ServiceManager.php b/src/Runtime/ServiceManager.php index b67b27db44..f20b3bde20 100644 --- a/src/Runtime/ServiceManager.php +++ b/src/Runtime/ServiceManager.php @@ -299,7 +299,7 @@ public function discoverListeners(array $directoryList, string $baseNamespace): ->ignoreNamespacePart('contrib', 'Listeners') ->ignoreNamespacePart('custom', 'Listeners') ->ignoreNamespacePart('src') - ->setSearchLocations(['EventListener']) + ->setSearchLocations(['Listeners']) ->setSearchPattern('#.*(Listener)s?.php$#'); $baseNamespace = ltrim($baseNamespace, '\\'); $listenerClasses = $discovery->discover($directoryList, $baseNamespace); diff --git a/sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php similarity index 94% rename from sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php rename to sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php index 8ec647f193..1b6090eae9 100644 --- a/sut/modules/unish/woot/src/Drush/EventListener/WootDefinitionListener.php +++ b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drupal\woot\Drush\EventListener; +namespace Drupal\woot\Drush\Listeners; use Drush\Commands\AutowireTrait; use Drush\Event\ConsoleDefinitionsEvent; diff --git a/tests/functional/CommandInfoAlterTest.php b/tests/functional/CommandInfoAlterTest.php index ae24b32fde..a80154318f 100644 --- a/tests/functional/CommandInfoAlterTest.php +++ b/tests/functional/CommandInfoAlterTest.php @@ -5,7 +5,6 @@ namespace Unish; use Drush\Commands\pm\PmCommands; -use Symfony\Component\Filesystem\Path; /** * @group commands @@ -27,7 +26,7 @@ public function testCommandInfoAlter() $this->assertStringContainsString('woot-new-alias', $this->getOutput()); // Check the debug messages. - $this->assertStringContainsString('[debug] Commands are potentially altered in Drupal\woot\Drush\CommandInfoAlterers\WootCommandInfoAlterer.', $this->getErrorOutput()); + $this->assertStringContainsString('[debug] Commands are potentially altered in Drupal\woot\Drush\Listeners.', $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. From 47bf610a2e8d7ddfc789af6406ee5700e61f9d1f Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 13:28:51 -0400 Subject: [PATCH 03/48] PHPStan --- src/Commands/AutowireTrait.php | 2 -- src/Commands/core/TwigUnusedCommand.php | 8 -------- src/Commands/pm/PmCommands.php | 2 +- src/Formatters/FormatterTrait.php | 10 ++++++++-- src/Runtime/ServiceManager.php | 7 ++++++- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Commands/AutowireTrait.php b/src/Commands/AutowireTrait.php index d4d77ef095..f34f82425c 100644 --- a/src/Commands/AutowireTrait.php +++ b/src/Commands/AutowireTrait.php @@ -21,8 +21,6 @@ trait AutowireTrait * * @param ContainerInterface $container * The service container this instance should use. - * - * @return static */ public static function create(ContainerInterface $container) { diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 565f839e49..62e4b69201 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -6,11 +6,7 @@ use Consolidation\OutputFormatters\FormatterManager; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; -use Drupal\Core\DrupalKernelInterface; -use Drupal\Core\Extension\ModuleExtensionList; -use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\PhpStorage\PhpStorageFactory; -use Drupal\Core\State\StateInterface; use Drupal\Core\Template\TwigEnvironment; use Drush\Attributes as CLI; use Drush\Boot\BootstrapManager; @@ -44,10 +40,6 @@ public function __construct( protected readonly FormatterManager $formatterManager, protected readonly BootstrapManager $bootstrapManager, protected readonly TwigEnvironment $twig, - protected readonly ModuleHandlerInterface $moduleHandler, - private readonly ModuleExtensionList $extensionList, - private readonly StateInterface $state, - private readonly DrupalKernelInterface $kernel, private readonly LoggerInterface $logger ) { parent::__construct(); diff --git a/src/Commands/pm/PmCommands.php b/src/Commands/pm/PmCommands.php index 82100f89dc..f8d27fa90a 100644 --- a/src/Commands/pm/PmCommands.php +++ b/src/Commands/pm/PmCommands.php @@ -116,7 +116,7 @@ public function validateEnableModules(CommandData $commandData): void // Note: we can't just call the API ($moduleHandler->loadInclude($module, 'install')), // because the API ignores modules that haven't been installed yet. We have // to do it the same way the `function drupal_check_module($module)` does. - $file = DRUPAL_ROOT . '/' . $this->extensionListModule->getPath($module) . "/$module.install"; + $file = DRUPAL_ROOT . '/' . $this->getExtensionListModule()->getPath($module) . "/$module.install"; if (is_file($file)) { require_once $file; } diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index 70cd8f380f..d473b68bc4 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -13,7 +13,13 @@ public function addFormatterOptions() { $formatterOptions = new FormatterOptions($this->getConfigurationData(), []); $reflection = new \ReflectionMethod($this, 'doExecute'); - $inputOptions = $this->formatterManager->automaticOptions($formatterOptions, $reflection->getReturnType()->getName()); + $returnType = $reflection->getReturnType(); + if ($returnType instanceof \ReflectionNamedType) { + $dataType = $returnType->getName(); + } else { + throw new \Exception($reflection->getDeclaringClass() . '::doExecute method must specify a return type.'); + } + $inputOptions = $this->formatterManager->automaticOptions($formatterOptions, $dataType); foreach ($inputOptions as $inputOption) { $mode = $this->getPrivatePropValue($inputOption, 'mode'); $suggestedValues = $this->getPrivatePropValue($inputOption, 'suggestedValues'); @@ -26,7 +32,7 @@ public function addFormatterOptions() */ public function execute(InputInterface $input, OutputInterface $output): int { - $configurationData = $this->getConfigurationData($this); + $configurationData = $this->getConfigurationData(); $formatterOptions = new FormatterOptions($configurationData, $input->getOptions()); $formatterOptions->setInput($input); $data = $this->doExecute($input, $output); diff --git a/src/Runtime/ServiceManager.php b/src/Runtime/ServiceManager.php index f20b3bde20..772790422e 100644 --- a/src/Runtime/ServiceManager.php +++ b/src/Runtime/ServiceManager.php @@ -432,7 +432,12 @@ public function addListeners(iterable $classes, ContainerInterface $drushContain $priority = $attributeInstance->priority ?? 0; $reflectionMethod = $reflectionObject->getMethod($method); $reflectionParameters = $reflectionMethod->getParameters(); - $eventName = $reflectionParameters[0]->getType()->getName(); + $paramType = $reflectionParameters[0]->getType(); + if ($paramType instanceof \ReflectionNamedType) { + $eventName = $paramType->getName(); + } else { + throw new \Exception('Event listener method must have a single parameter with a type hint.'); + } if ($eventName == ConsoleCommandEvent::class) { $eventName = ConsoleEvents::COMMAND; } From a201b1e8ecd238c79017d87eaa4772607a7610f7 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 14:16:08 -0400 Subject: [PATCH 04/48] Restore handle() methods on a few Formatter Attribute classes --- .circleci/config.yml | 4 +++- src/Attributes/DefaultTableFields.php | 10 +++++++++- src/Attributes/FieldLabels.php | 10 +++++++++- src/Attributes/FilterDefaultField.php | 7 +++++++ src/Attributes/Format.php | 12 +++++++++++- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 449ea55487..d392784b64 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,7 @@ defaults: &defaults PHP_EXTENSIONS_DISABLE: xdebug PHP_XDEBUG_MODE: off +#No longer used requires: &requires requires: - check_mergable @@ -173,7 +174,8 @@ workflows: # - test_80_drupal92_security: # <<: *requires - test: - <<: *requires + # Not used, for now. + # <<: *requires <<: *poststeps matrix: parameters: diff --git a/src/Attributes/DefaultTableFields.php b/src/Attributes/DefaultTableFields.php index 90baf35226..1a31ba2ab9 100644 --- a/src/Attributes/DefaultTableFields.php +++ b/src/Attributes/DefaultTableFields.php @@ -5,12 +5,14 @@ namespace Drush\Attributes; use Attribute; +use Consolidation\AnnotatedCommand\Parser\CommandInfo; +use Consolidation\OutputFormatters\Options\FormatterOptions; use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class DefaultTableFields implements FormatterConfigurationItemProviderInterface { - const KEY = 'default-table-fields'; + const KEY = FormatterOptions::DEFAULT_TABLE_FIELDS; /** * @param $fields @@ -25,4 +27,10 @@ public function getConfigurationItem(\ReflectionAttribute $attribute): array $args = $attribute->getArguments(); return [self::KEY => $args['fields']]; } + + public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) + { + $args = $attribute->getArguments(); + $commandInfo->addAnnotation('default-table-fields', $args['fields']); + } } diff --git a/src/Attributes/FieldLabels.php b/src/Attributes/FieldLabels.php index 8e0962ccca..dfa0c607b2 100644 --- a/src/Attributes/FieldLabels.php +++ b/src/Attributes/FieldLabels.php @@ -3,12 +3,14 @@ namespace Drush\Attributes; use Attribute; +use Consolidation\AnnotatedCommand\Parser\CommandInfo; +use Consolidation\OutputFormatters\Options\FormatterOptions; use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class FieldLabels implements FormatterConfigurationItemProviderInterface { - const KEY = 'field-labels'; + const KEY = FormatterOptions::FIELD_LABELS; /** * @param $labels @@ -24,4 +26,10 @@ public function getConfigurationItem(\ReflectionAttribute $attribute): array $args = $attribute->getArguments(); return [self::KEY => $args['labels']]; } + + public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) + { + $args = $attribute->getArguments(); + $commandInfo->addAnnotation('field-labels', $args['labels']); + } } diff --git a/src/Attributes/FilterDefaultField.php b/src/Attributes/FilterDefaultField.php index fe639f07df..adb0eabaa9 100644 --- a/src/Attributes/FilterDefaultField.php +++ b/src/Attributes/FilterDefaultField.php @@ -3,6 +3,7 @@ namespace Drush\Attributes; use Attribute; +use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] @@ -24,4 +25,10 @@ public function getConfigurationItem(\ReflectionAttribute $attribute): array $args = $attribute->getArguments(); return [self::KEY => $args['field']]; } + + public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) + { + $args = $attribute->getArguments(); + $commandInfo->addAnnotation('filter-default-field', $args['field']); + } } diff --git a/src/Attributes/Format.php b/src/Attributes/Format.php index b2f16bc01e..236ef88de6 100644 --- a/src/Attributes/Format.php +++ b/src/Attributes/Format.php @@ -7,10 +7,11 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Consolidation\OutputFormatters\Options\FormatterOptions; +use Drush\Formatters\FormatterConfigurationItemProviderInterface; use JetBrains\PhpStorm\ExpectedValues; #[Attribute(Attribute::TARGET_METHOD)] -class Format +class Format implements FormatterConfigurationItemProviderInterface { /** * @param ?string $listDelimiter @@ -31,4 +32,13 @@ public static function handle(\ReflectionAttribute $attribute, CommandInfo $comm $commandInfo->addAnnotation(FormatterOptions::LIST_DELIMITER, $instance->listDelimiter); $commandInfo->addAnnotation(FormatterOptions::TABLE_STYLE, $instance->tableStyle); } + + public function getConfigurationItem(\ReflectionAttribute $attribute): array + { + $instance = $attribute->newInstance(); + return [ + FormatterOptions::LIST_DELIMITER => $instance->listDelimiter, + FormatterOptions::TABLE_STYLE => $instance->tableStyle + ]; + } } From 756d384271e8901eaba3d849f3929359ef090cb3 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 14:59:09 -0400 Subject: [PATCH 05/48] A few fixes --- src/Attributes/DefaultFields.php | 7 +++++++ .../src/Drush/Listeners/WootDefinitionListener.php | 2 ++ ...foAlterTest.php => CommandDefinitionAlterTest.php} | 11 ++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) rename tests/functional/{CommandInfoAlterTest.php => CommandDefinitionAlterTest.php} (64%) diff --git a/src/Attributes/DefaultFields.php b/src/Attributes/DefaultFields.php index 0dc23ac7d8..62680db5b7 100644 --- a/src/Attributes/DefaultFields.php +++ b/src/Attributes/DefaultFields.php @@ -5,6 +5,7 @@ namespace Drush\Attributes; use Attribute; +use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] @@ -25,4 +26,10 @@ public function getConfigurationItem(\ReflectionAttribute $attribute): array $args = $attribute->getArguments(); return [self::KEY => $args['fields']]; } + + public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) + { + $args = $attribute->getArguments(); + $commandInfo->addAnnotation('default-fields', $args['fields']); + } } diff --git a/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php index 1b6090eae9..c6352d5769 100644 --- a/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php +++ b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php @@ -24,6 +24,8 @@ public function __invoke(ConsoleDefinitionsEvent $event): void foreach ($event->getApplication()->all() as $id => $command) { if ($command->getName() === 'woot:altered') { $command->setAliases(['woot-new-alias']); + // Remove the command keyed with the old alias. + unset($event->getApplication()[$id]); $this->logger->debug(dt("Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in " . __METHOD__ . '().')); } } diff --git a/tests/functional/CommandInfoAlterTest.php b/tests/functional/CommandDefinitionAlterTest.php similarity index 64% rename from tests/functional/CommandInfoAlterTest.php rename to tests/functional/CommandDefinitionAlterTest.php index a80154318f..8da5b51848 100644 --- a/tests/functional/CommandInfoAlterTest.php +++ b/tests/functional/CommandDefinitionAlterTest.php @@ -10,14 +10,14 @@ * @group commands * */ -class CommandInfoAlterTest extends CommandUnishTestCase +class CommandDefinitionAlterTest extends CommandUnishTestCase { use TestModuleHelperTrait; /** - * Tests command info alter. + * Tests Console Definition Event Listener. */ - public function testCommandInfoAlter() + public function testCommandDefinitionAlter() { $this->setUpDrupal(1, true); $this->drush(PmCommands::INSTALL, ['woot']); @@ -26,8 +26,9 @@ public function testCommandInfoAlter() $this->assertStringContainsString('woot-new-alias', $this->getOutput()); // Check the debug messages. - $this->assertStringContainsString('[debug] Commands are potentially altered in Drupal\woot\Drush\Listeners.', $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()); + $this->assertStringContainsString("[debug] Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in Drupal\woot\Drush\Listeners\WootDefinitionListener::__invoke().", $this->getErrorOutput()); + // Listeners dispatch mostly outside of Drush so no longer able to asset this message. + // $this->assertStringContainsString('[debug] Commands are potentially altered in Drupal\woot\Drush\Listeners.', $this->getErrorOutput()); // Try to run the command with the initial alias. $this->drush('woot-initial-alias', [], [], null, null, self::EXIT_ERROR); From 000c215b5d32147c9f837d9ed661d774d57b75c5 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 15:07:32 -0400 Subject: [PATCH 06/48] Remove illegal operation --- .../unish/woot/src/Drush/Listeners/WootDefinitionListener.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php index c6352d5769..ece00abef3 100644 --- a/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php +++ b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php @@ -24,8 +24,7 @@ public function __invoke(ConsoleDefinitionsEvent $event): void foreach ($event->getApplication()->all() as $id => $command) { if ($command->getName() === 'woot:altered') { $command->setAliases(['woot-new-alias']); - // Remove the command keyed with the old alias. - unset($event->getApplication()[$id]); + // @todo Remove the command keyed with the old alias. $this->logger->debug(dt("Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in " . __METHOD__ . '().')); } } From 2eeb287e48b868b101c000c10e480224b8debd08 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 16:13:02 -0400 Subject: [PATCH 07/48] A bit ugly but test has to pass --- src/Application.php | 10 ++++++++++ .../src/Drush/Listeners/WootDefinitionListener.php | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index c686f9a11e..1e44cd1605 100644 --- a/src/Application.php +++ b/src/Application.php @@ -352,4 +352,14 @@ protected function addListeners($commandfileSearchpath): void $listenerClasses = $this->serviceManager->filterListeners($listenerClasses); $this->serviceManager->addListeners($listenerClasses, Drush::getContainer()); } + + // Remove a command. Initially used by WootDefinitionListener and its test. + public function remove(string $id): void + { + $rf = new \ReflectionProperty(\Symfony\Component\Console\Application::class, 'commands'); + $rf->setAccessible(true); + $commands = $rf->getValue($this); + unset($commands[$id]); + $rf->setValue($this, $commands); + } } diff --git a/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php index ece00abef3..8ee1fc5216 100644 --- a/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php +++ b/sut/modules/unish/woot/src/Drush/Listeners/WootDefinitionListener.php @@ -24,7 +24,9 @@ public function __invoke(ConsoleDefinitionsEvent $event): void foreach ($event->getApplication()->all() as $id => $command) { if ($command->getName() === 'woot:altered') { $command->setAliases(['woot-new-alias']); - // @todo Remove the command keyed with the old alias. + if ($id == 'woot-initial-alias') { + $event->getApplication()->remove('woot-initial-alias'); + } $this->logger->debug(dt("Module 'woot' changed the alias of 'woot:altered' command into 'woot-new-alias' in " . __METHOD__ . '().')); } } From ff65ecef2c4584f631aa7539adc8e50280b07596 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 16:56:01 -0400 Subject: [PATCH 08/48] Add --filter option automatically for Console commands. Implementation is in progress --- src/Application.php | 6 ++++- src/Commands/core/TwigUnusedCommand.php | 1 + src/Listeners/FilterOptionListener.php | 30 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/Listeners/FilterOptionListener.php diff --git a/src/Application.php b/src/Application.php index 1e44cd1605..1a543fadeb 100644 --- a/src/Application.php +++ b/src/Application.php @@ -353,7 +353,11 @@ protected function addListeners($commandfileSearchpath): void $this->serviceManager->addListeners($listenerClasses, Drush::getContainer()); } - // Remove a command. Initially used by WootDefinitionListener and its test. + /** + * Remove a command. Initially used by WootDefinitionListener and its test. + * + * An alternative would be console.excluded https://github.com/search?q=repo%3AHuttopia%2Fconsole-bundle%20console.excluded&type=code + */ public function remove(string $id): void { $rf = new \ReflectionProperty(\Symfony\Component\Console\Application::class, 'commands'); diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 62e4b69201..88da364f99 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -29,6 +29,7 @@ )] #[CLI\FieldLabels(labels: ['template' => 'Template', 'compiled' => 'Compiled'])] #[CLI\DefaultTableFields(fields: ['template', 'compiled'])] +#[CLI\FilterDefaultField(field: 'template')] final class TwigUnusedCommand extends Command { use AutowireTrait; diff --git a/src/Listeners/FilterOptionListener.php b/src/Listeners/FilterOptionListener.php new file mode 100644 index 0000000000..d42c5539a3 --- /dev/null +++ b/src/Listeners/FilterOptionListener.php @@ -0,0 +1,30 @@ +getApplication()->all() as $id => $command) { + $reflection = new \ReflectionObject($command); + $attributes = $reflection->getAttributes(FilterDefaultField::class); + if (empty($attributes)) { + continue; + } + $instance = $attributes[0]->newInstance(); + $command->addOption('filter', null, InputOption::VALUE_REQUIRED, 'Filter output based on provided expression. Default field: ' . $instance->field); + } + } +} From f2f179d28eee2ab29197a68bbccc3c6fd60d982a Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 17:11:33 -0400 Subject: [PATCH 09/48] Incorporate into the Trait --- src/Formatters/FormatterTrait.php | 9 ++++++++ src/Listeners/FilterOptionListener.php | 30 -------------------------- 2 files changed, 9 insertions(+), 30 deletions(-) delete mode 100644 src/Listeners/FilterOptionListener.php diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index d473b68bc4..a79704a5dc 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -3,8 +3,10 @@ namespace Drush\Formatters; use Consolidation\OutputFormatters\Options\FormatterOptions; +use Drush\Attributes\FilterDefaultField; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; trait FormatterTrait @@ -25,6 +27,13 @@ public function addFormatterOptions() $suggestedValues = $this->getPrivatePropValue($inputOption, 'suggestedValues'); $this->addOption($inputOption->getName(), $inputOption->getShortcut(), $mode, $inputOption->getDescription(), $inputOption->getDefault(), $suggestedValues); } + + $reflection = new \ReflectionObject($this); + $attributes = $reflection->getAttributes(FilterDefaultField::class); + if (!empty($attributes)) { + $instance = $attributes[0]->newInstance(); + $this->addOption('filter', null, InputOption::VALUE_REQUIRED, 'Filter output based on provided expression. Default field: ' . $instance->field); + } } /** diff --git a/src/Listeners/FilterOptionListener.php b/src/Listeners/FilterOptionListener.php deleted file mode 100644 index d42c5539a3..0000000000 --- a/src/Listeners/FilterOptionListener.php +++ /dev/null @@ -1,30 +0,0 @@ -getApplication()->all() as $id => $command) { - $reflection = new \ReflectionObject($command); - $attributes = $reflection->getAttributes(FilterDefaultField::class); - if (empty($attributes)) { - continue; - } - $instance = $attributes[0]->newInstance(); - $command->addOption('filter', null, InputOption::VALUE_REQUIRED, 'Filter output based on provided expression. Default field: ' . $instance->field); - } - } -} From 41a8aa66a7cae00a3f8175e8f330a638cc32f926 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 20:06:29 -0400 Subject: [PATCH 10/48] Support --filter during execute() of formatter supporting commands --- src/Formatters/FormatterTrait.php | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index a79704a5dc..71a855d3f8 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -2,6 +2,8 @@ namespace Drush\Formatters; +use Consolidation\Filter\FilterOutputData; +use Consolidation\Filter\LogicalOpFactory; use Consolidation\OutputFormatters\Options\FormatterOptions; use Drush\Attributes\FilterDefaultField; use Symfony\Component\Console\Command\Command; @@ -28,6 +30,7 @@ public function addFormatterOptions() $this->addOption($inputOption->getName(), $inputOption->getShortcut(), $mode, $inputOption->getDescription(), $inputOption->getDefault(), $suggestedValues); } + // Add the --filter option if the command has a FilterDefaultField attribute. $reflection = new \ReflectionObject($this); $attributes = $reflection->getAttributes(FilterDefaultField::class); if (!empty($attributes)) { @@ -45,10 +48,40 @@ public function execute(InputInterface $input, OutputInterface $output): int $formatterOptions = new FormatterOptions($configurationData, $input->getOptions()); $formatterOptions->setInput($input); $data = $this->doExecute($input, $output); + $data = $this->alterResult($data, $input); $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); return Command::SUCCESS; } + protected function alterResult($result, InputInterface $input): mixed + { + $reflection = new \ReflectionObject($this); + $expression = $input->getOption('filter'); + if (empty($expression)) { + return $result; + } + $attributes = $reflection->getAttributes(FilterDefaultField::class); + $instance = $attributes[0]->newInstance(); + $factory = LogicalOpFactory::get(); + $op = $factory->evaluate($expression, $instance->field); + $filter = new FilterOutputData(); + return $this->wrapFilteredResult($filter->filter($result, $op), $result); + } + + /** + * If the source data was wrapped in a marker class such + * as RowsOfFields, then re-apply the wrapper. + */ + protected function wrapFilteredResult($data, $source) + { + if (!$source instanceof \ArrayObject) { + return $data; + } + $sourceClass = get_class($source); + + return new $sourceClass($data); + } + protected function getPrivatePropValue(mixed $object, $name): mixed { $rc = new \ReflectionClass($object); From 141fa703a676ab305fb77520b5f0de88f2f6ca23 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 22:50:17 -0400 Subject: [PATCH 11/48] Convert image:flush to a Console command. Add ValidateEntityLoad listener Shows interact() --- src/Attributes/ValidateEntityLoad.php | 2 +- src/Commands/core/ImageCommands.php | 55 +------------ src/Commands/core/ImageFlushCommand.php | 82 ++++++++++++++++++++ src/Listeners/ValidateEntityLoadListener.php | 47 +++++++++++ 4 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 src/Commands/core/ImageFlushCommand.php create mode 100644 src/Listeners/ValidateEntityLoadListener.php diff --git a/src/Attributes/ValidateEntityLoad.php b/src/Attributes/ValidateEntityLoad.php index a8edc12aee..a210b35518 100644 --- a/src/Attributes/ValidateEntityLoad.php +++ b/src/Attributes/ValidateEntityLoad.php @@ -9,7 +9,7 @@ use Consolidation\AnnotatedCommand\CommandError; use Drush\Utils\StringUtils; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class ValidateEntityLoad extends ValidatorBase implements ValidatorInterface { /** diff --git a/src/Commands/core/ImageCommands.php b/src/Commands/core/ImageCommands.php index 87f7636594..6379608835 100644 --- a/src/Commands/core/ImageCommands.php +++ b/src/Commands/core/ImageCommands.php @@ -4,68 +4,15 @@ namespace Drush\Commands\core; -use Consolidation\AnnotatedCommand\AnnotationData; -use Consolidation\AnnotatedCommand\Hooks\HookManager; use Drupal\image\Entity\ImageStyle; use Drush\Attributes as CLI; -use Drush\Commands\DrushCommands; -use Drush\Utils\StringUtils; -use Symfony\Component\Console\Input\InputInterface; use Drush\Boot\DrupalBootLevels; +use Drush\Commands\DrushCommands; final class ImageCommands extends DrushCommands { - const FLUSH = 'image:flush'; const DERIVE = 'image:derive'; - /** - * Flush all derived images for a given style. - */ - #[CLI\Command(name: self::FLUSH, aliases: ['if', 'image-flush'])] - #[CLI\Argument(name: 'style_names', description: 'A comma delimited list of image style machine names. If not provided, user may choose from a list of names.')] - #[CLI\Option(name: 'all', description: 'Flush all derived images')] - #[CLI\Usage(name: 'drush image:flush', description: 'Pick an image style and then delete its derivatives.')] - #[CLI\Usage(name: 'drush image:flush thumbnail,large', description: 'Delete all thumbnail and large derivatives.')] - #[CLI\Usage(name: 'drush image:flush --all', description: 'Flush all derived images. They will be regenerated on demand.')] - #[CLI\ValidateEntityLoad(entityType: 'image_style', argumentName: 'style_names')] - #[CLI\ValidateModulesEnabled(modules: ['image'])] - #[CLI\Bootstrap(level: DrupalBootLevels::FULL)] - public function flush($style_names, $options = ['all' => false]): void - { - foreach (ImageStyle::loadMultiple(StringUtils::csvToArray($style_names)) as $style_name => $style) { - $style->flush(); - $this->logger()->success(dt('Image style !style_name flushed', ['!style_name' => $style_name])); - } - } - - #[CLI\Hook(type: HookManager::INTERACT, target: self::FLUSH)] - public function interactFlush(InputInterface $input, $output): void - { - $styles = array_keys(ImageStyle::loadMultiple()); - $style_names = $input->getArgument('style_names'); - - if (empty($style_names)) { - $styles_all = $styles; - array_unshift($styles_all, 'all'); - $choices = array_combine($styles_all, $styles_all); - $style_names = $this->io()->select(dt("Choose a style to flush"), $choices, 'all', scroll: 20); - if ($style_names == 'all') { - $style_names = implode(',', $styles); - } - $input->setArgument('style_names', $style_names); - } - } - - #[CLI\Hook(type: HookManager::POST_INITIALIZE, target: self::FLUSH)] - public function postInit(InputInterface $input, AnnotationData $annotationData): void - { - // 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)); - } - } - /** * Create an image derivative. */ diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php new file mode 100644 index 0000000000..8ea2a46b5f --- /dev/null +++ b/src/Commands/core/ImageFlushCommand.php @@ -0,0 +1,82 @@ +addArgument('style_names', InputArgument::OPTIONAL, 'A comma delimited list of image style machine names. If not provided, user may choose from a list of names.') + ->addOption('all', null, InputOption::VALUE_NONE, 'Flush all derived images') + ->addUsage('image:flush thumbnail,large') + ->addUsage('image:flush --all') + ->setHelp('Immediately before running this command, web crawl your entire web site. Or use your Production PHPStorage dir for comparison.'); + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + // @todo Inject this or use a trait? + $io = new DrushStyle($input, $output); + + $styles = array_keys(ImageStyle::loadMultiple()); + $style_names = $input->getArgument('style_names'); + + if (empty($style_names)) { + $styles_all = $styles; + array_unshift($styles_all, 'all'); + $choices = array_combine($styles_all, $styles_all); + $style_names = $io->select(dt("Choose a style to flush"), $choices, 'all', scroll: 20); + if ($style_names == 'all') { + $style_names = implode(',', $styles); + } + $input->setArgument('style_names', $style_names); + } + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + // Needed for non-interactive requests. + if ($input->getOption('all')) { + $input->setArgument('style_names', implode(',', array_keys(ImageStyle::loadMultiple()))); + } + + foreach (ImageStyle::loadMultiple(StringUtils::csvToArray($input->getArgument('style_names'))) as $style_name => $style) { + $style->flush(); + $this->logger->success(dt('Image style !style_name flushed', ['!style_name' => $style_name])); + } + return Command::SUCCESS; + } +} diff --git a/src/Listeners/ValidateEntityLoadListener.php b/src/Listeners/ValidateEntityLoadListener.php new file mode 100644 index 0000000000..2d26262c00 --- /dev/null +++ b/src/Listeners/ValidateEntityLoadListener.php @@ -0,0 +1,47 @@ +getCommand(); + $reflection = new \ReflectionObject($command); + $attributes = $reflection->getAttributes(ValidateEntityLoad::class); + if (empty($attributes)) { + return; + } + $instance = $attributes[0]->newInstance(); + $names = StringUtils::csvToArray($event->getInput()->getArgument($instance->argumentName)); + $loaded = $this->entityTypeManager->getStorage($instance->entityType)->loadMultiple($names); + if ($missing = array_diff($names, array_keys($loaded))) { + $msg = dt('Unable to load the !type: !str', ['!type' => $instance->entityType, '!str' => implode(', ', $missing)]); + $this->logger->error($msg); + $event->disableCommand(); + } + } +} From 2eea0de9e1cd74682ebe7a1e3062a6ade8343909 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 14 Oct 2024 23:01:13 -0400 Subject: [PATCH 12/48] Deprecate constants --- src/Commands/core/ImageCommands.php | 3 +++ src/Commands/core/TwigCommands.php | 3 +++ tests/integration/ImageTest.php | 5 +++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Commands/core/ImageCommands.php b/src/Commands/core/ImageCommands.php index 6379608835..561f093ef6 100644 --- a/src/Commands/core/ImageCommands.php +++ b/src/Commands/core/ImageCommands.php @@ -8,10 +8,13 @@ use Drush\Attributes as CLI; use Drush\Boot\DrupalBootLevels; use Drush\Commands\DrushCommands; +use JetBrains\PhpStorm\Deprecated; final class ImageCommands extends DrushCommands { const DERIVE = 'image:derive'; + #[Deprecated('Use ImageFlushCommand::FLUSH instead.')] + const FLUSH = ImageFlushCommand::FLUSH; /** * Create an image derivative. diff --git a/src/Commands/core/TwigCommands.php b/src/Commands/core/TwigCommands.php index 2df9a90a65..460290e1d7 100644 --- a/src/Commands/core/TwigCommands.php +++ b/src/Commands/core/TwigCommands.php @@ -13,6 +13,7 @@ use Drush\Commands\AutowireTrait; use Drush\Commands\DrushCommands; use Drush\Drush; +use JetBrains\PhpStorm\Deprecated; use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; @@ -22,6 +23,8 @@ final class TwigCommands extends DrushCommands const COMPILE = 'twig:compile'; const DEBUG = 'twig:debug'; + #[Deprecated('Use TwigUnusedCommand::UNUSED instead.')] + const UNUSED = 'twig:unused'; public function __construct( protected TwigEnvironment $twig, diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 16fbde81f4..2a2c148d52 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -5,6 +5,7 @@ namespace Unish; use Drush\Commands\core\ImageCommands; +use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; /** @@ -35,7 +36,7 @@ public function testImage() $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. - $this->drush(ImageCommands::FLUSH, [$style_name], ['all' => null]); + $this->drush(ImageFlushCommand::FLUSH, [$style_name], ['all' => null]); $this->assertFileDoesNotExist($thumbnail); // Check that "drush image-flush --all" deletes all image styles by creating two different ones and testing its @@ -44,7 +45,7 @@ public function testImage() $this->assertFileExists($thumbnail); $this->drush(ImageCommands::DERIVE, ['medium', $logo]); $this->assertFileExists($medium); - $this->drush(ImageCommands::FLUSH, [], ['all' => null]); + $this->drush(ImageFlushCommand::FLUSH, [], ['all' => null]); $this->assertFileDoesNotExist($thumbnail); $this->assertFileDoesNotExist($medium); } From 68cad77072b2e3172ad899149bf67d1a1f7365c4 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 03:31:06 -0400 Subject: [PATCH 13/48] Inject a new io service when StyleInterface is the type hint --- src/Commands/core/ImageFlushCommand.php | 12 +++++------- src/Runtime/DependencyInjection.php | 12 ++++++++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 8ea2a46b5f..6ba1643d58 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -7,7 +7,6 @@ use Drupal\image\Entity\ImageStyle; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; -use Drush\Style\DrushStyle; use Drush\Utils\StringUtils; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -16,6 +15,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\StyleInterface; #[AsCommand( name: self::FLUSH, @@ -31,7 +31,8 @@ final class ImageFlushCommand extends Command const FLUSH = 'image:flush'; public function __construct( - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly StyleInterface $io ) { parent::__construct(); } @@ -48,17 +49,14 @@ protected function configure(): void protected function interact(InputInterface $input, OutputInterface $output): void { - // @todo Inject this or use a trait? - $io = new DrushStyle($input, $output); - $styles = array_keys(ImageStyle::loadMultiple()); $style_names = $input->getArgument('style_names'); - if (empty($style_names)) { + if (empty($style_names) && !$input->getOption('all')) { $styles_all = $styles; array_unshift($styles_all, 'all'); $choices = array_combine($styles_all, $styles_all); - $style_names = $io->select(dt("Choose a style to flush"), $choices, 'all', scroll: 20); + $style_names = $this->io->select(dt("Choose a style to flush"), $choices, 'all', scroll: 20); if ($style_names == 'all') { $style_names = implode(',', $styles); } diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 8de4048cb0..b2369e6900 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -24,6 +24,7 @@ use Drush\Formatters\EntityToArraySimplifier; use Drush\Log\Logger; use Drush\SiteAlias\ProcessManager; +use Drush\Style\DrushStyle; use Drush\Symfony\DrushStyleInjector; use League\Container\Container; use League\Container\ContainerInterface; @@ -31,6 +32,7 @@ use Robo\Robo; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\StyleInterface; /** * Prepare our Dependency Injection Container @@ -65,7 +67,7 @@ public function initContainer( $container = new Container(); // With league/container 3.x, first call wins, so add Drush services first. - $this->addDrushServices($container, $loader, $drupalFinder, $aliasManager, $config, $output); + $this->addDrushServices($container, $loader, $drupalFinder, $aliasManager, $config, $output, $input); // Robo has the same signature for configureContainer in 1.x, 2.x and 3.x. Robo::configureContainer($container, $application, $config, $input, $output); @@ -95,7 +97,7 @@ public function installHandlers($container): void } // Add Drush Services to league/container 3.x - protected function addDrushServices(Container $container, ClassLoader $loader, DrushDrupalFinder $drupalFinder, SiteAliasManager $aliasManager, DrushConfig $config, OutputInterface $output): void + protected function addDrushServices(Container $container, ClassLoader $loader, DrushDrupalFinder $drupalFinder, SiteAliasManager $aliasManager, DrushConfig $config, OutputInterface $output, InputInterface $input): void { // Override Robo's logger with a LoggerManager that delegates to the Drush logger. Robo::addShared($container, 'logger', '\Drush\Log\DrushLoggerManager') @@ -103,6 +105,12 @@ protected function addDrushServices(Container $container, ClassLoader $loader, D ->addMethodCall('add', ['drush', new Logger($output)]); Robo::addShared($container, LoggerInterface::class, 'logger'); // For autowiring + Robo::addShared($container, 'io', DrushStyle::class) + ->addArguments([$input, $output]); + // @todo SymfonyStyle is already registered at https://github.com/drush-ops/drush/blob/2f63ffcef88c57d2d4aef9c8e351beb304254beb/src/Runtime/DependencyInjection.php#L173 + // so we expect a different type hint for autowire. + Robo::addShared($container, StyleInterface::class, 'io'); // For autowiring + Robo::addShared($container, self::LOADER, $loader); Robo::addShared($container, ClassLoader::class, self::LOADER); // For autowiring Robo::addShared($container, self::SITE_ALIAS_MANAGER, $aliasManager); From b3fbd3ad8031d414962ab4887efd8014ea2636b5 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 08:58:41 -0400 Subject: [PATCH 14/48] Add visibility and remove use of dt() --- src/Commands/core/ImageFlushCommand.php | 4 ++-- src/Commands/core/TwigUnusedCommand.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 6ba1643d58..15df5f151e 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -28,7 +28,7 @@ final class ImageFlushCommand extends Command { use AutowireTrait; - const FLUSH = 'image:flush'; + public const FLUSH = 'image:flush'; public function __construct( private readonly LoggerInterface $logger, @@ -73,7 +73,7 @@ public function execute(InputInterface $input, OutputInterface $output): int foreach (ImageStyle::loadMultiple(StringUtils::csvToArray($input->getArgument('style_names'))) as $style_name => $style) { $style->flush(); - $this->logger->success(dt('Image style !style_name flushed', ['!style_name' => $style_name])); + $this->logger->success('Image style {style_name} flushed', ['style_name' => $style_name]); } return Command::SUCCESS; } diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 88da364f99..acada4113c 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -35,7 +35,7 @@ final class TwigUnusedCommand extends Command use AutowireTrait; use FormatterTrait; - const UNUSED = 'twig:unused'; + public const UNUSED = 'twig:unused'; public function __construct( protected readonly FormatterManager $formatterManager, @@ -68,7 +68,7 @@ public function doExecute(InputInterface $input, OutputInterface $output): RowsO ->name('*.html.twig') ->exclude('tests') ->in(StringUtils::csvToArray($searchpaths)); - $this->logger->notice(dt('Found !count templates', ['!count' => count($files)])); + $this->logger->notice('Found {count} templates', ['count' => count($files)]); // Check to see if a compiled equivalent exists in PHPStorage foreach ($files as $file) { @@ -87,7 +87,7 @@ public function doExecute(InputInterface $input, OutputInterface $output): RowsO throw new \Exception('There was a problem, please ensure your twig cache is enabled.'); } } - $this->logger->notice(dt('Found !count unused', ['!count' => count($unused)])); + $this->logger->notice('Found {count} unused', ['count' => count($unused)]); return new RowsOfFields($unused); } } From a194c59554249057f5150e8c91b794e10ab2adaa Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 10:59:51 -0400 Subject: [PATCH 15/48] Use io instead of logger for success messages --- src/Commands/core/ImageFlushCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 15df5f151e..641f6cb8a8 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -73,7 +73,7 @@ public function execute(InputInterface $input, OutputInterface $output): int foreach (ImageStyle::loadMultiple(StringUtils::csvToArray($input->getArgument('style_names'))) as $style_name => $style) { $style->flush(); - $this->logger->success('Image style {style_name} flushed', ['style_name' => $style_name]); + $this->io->success("Image style $style_name flushed"); } return Command::SUCCESS; } From 4a9f239ba0f15b378db8a7f6b89fbb82c064cf5d Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 11:24:37 -0400 Subject: [PATCH 16/48] Deal with a PHPStan failure by aliasing SynfonyStyle to DrushStyle in \Drush\Runtime\DependencyInjection::alterServicesForDrush --- src/Commands/core/ImageFlushCommand.php | 6 ++---- src/Runtime/DependencyInjection.php | 17 ++++++++--------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 641f6cb8a8..8f2de2ac59 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -8,14 +8,13 @@ use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; use Drush\Utils\StringUtils; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\StyleInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: self::FLUSH, @@ -31,8 +30,7 @@ final class ImageFlushCommand extends Command public const FLUSH = 'image:flush'; public function __construct( - private readonly LoggerInterface $logger, - private readonly StyleInterface $io + private readonly SymfonyStyle $io ) { parent::__construct(); } diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index b2369e6900..36e5b1b47a 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -32,7 +32,7 @@ use Robo\Robo; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\StyleInterface; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Prepare our Dependency Injection Container @@ -77,7 +77,7 @@ public function initContainer( Drush::setContainer($container); // Change service definitions as needed for our application. - $this->alterServicesForDrush($container, $application); + $this->alterServicesForDrush($container, $application, $input, $output); // Inject needed services into our application object. $this->injectApplicationServices($container, $application); @@ -105,12 +105,6 @@ protected function addDrushServices(Container $container, ClassLoader $loader, D ->addMethodCall('add', ['drush', new Logger($output)]); Robo::addShared($container, LoggerInterface::class, 'logger'); // For autowiring - Robo::addShared($container, 'io', DrushStyle::class) - ->addArguments([$input, $output]); - // @todo SymfonyStyle is already registered at https://github.com/drush-ops/drush/blob/2f63ffcef88c57d2d4aef9c8e351beb304254beb/src/Runtime/DependencyInjection.php#L173 - // so we expect a different type hint for autowire. - Robo::addShared($container, StyleInterface::class, 'io'); // For autowiring - Robo::addShared($container, self::LOADER, $loader); Robo::addShared($container, ClassLoader::class, self::LOADER); // For autowiring Robo::addShared($container, self::SITE_ALIAS_MANAGER, $aliasManager); @@ -169,11 +163,16 @@ protected function addDrushServices(Container $container, ClassLoader $loader, D ->invokeMethod('setProcessManager', ['process.manager']); } - protected function alterServicesForDrush($container, Application $application): void + protected function alterServicesForDrush($container, Application $application, InputInterface $input, OutputInterface $output): void { $paramInjection = $container->get('parameterInjection'); $paramInjection->register('Symfony\Component\Console\Style\SymfonyStyle', new DrushStyleInjector()); + Robo::addShared($container, 'io', DrushStyle::class) + ->addArguments([$input, $output]); + // @todo Does this alias interfere with the paramInjector above? Could we drop that feature? + Robo::addShared($container, SymfonyStyle::class, 'io'); // For autowiring + // Add our own callback to the hook manager $hookManager = $container->get('hookManager'); $hookManager->addCommandEvent(new GlobalOptionsEventListener()); From e78b50a9c6e187803e6204428c01bcf456ae8787 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 11:31:38 -0400 Subject: [PATCH 17/48] in ImageFlushCommand, use choice() and remove the deprecation from it Resolves a PHPStan failure. --- src-symfony-compatibility/v6/Style/DrushStyle.php | 3 +-- src/Commands/core/ImageFlushCommand.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src-symfony-compatibility/v6/Style/DrushStyle.php b/src-symfony-compatibility/v6/Style/DrushStyle.php index 5b35bce38a..0291cbf540 100644 --- a/src-symfony-compatibility/v6/Style/DrushStyle.php +++ b/src-symfony-compatibility/v6/Style/DrushStyle.php @@ -37,8 +37,7 @@ public function confirm(string $question, bool $default = true, string $yes = 'Y return confirm($question, $default, $yes, $no, $required, $validate, $hint); } - #[Deprecated('Use select() or multiselect() instead.')] - public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false, int $scroll = 10, ?\Closure $validate = null, string $hint = '', bool|string $required = true): mixed + public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false, int $scroll = 15, ?\Closure $validate = null, string $hint = '', bool|string $required = true): mixed { if ($multiSelect) { // For backward compat. Deprecated. diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 8f2de2ac59..6784cf5dad 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -54,7 +54,7 @@ protected function interact(InputInterface $input, OutputInterface $output): voi $styles_all = $styles; array_unshift($styles_all, 'all'); $choices = array_combine($styles_all, $styles_all); - $style_names = $this->io->select(dt("Choose a style to flush"), $choices, 'all', scroll: 20); + $style_names = $this->io->choice(dt("Choose a style to flush"), $choices, 'all'); if ($style_names == 'all') { $style_names = implode(',', $styles); } From 2f42025e3a3c7af18c0e586d7278f154c2060f6f Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 18:09:23 -0400 Subject: [PATCH 18/48] Convert sql:dump. Optionsets are provided via a Listener --- src/Attributes/OptionsetSql.php | 2 +- src/Attributes/OptionsetTableSelection.php | 3 +- src/Commands/core/ImageCommands.php | 4 +- src/Commands/core/ImageFlushCommand.php | 4 +- src/Commands/core/TwigUnusedCommand.php | 4 +- src/Commands/sql/SqlCommands.php | 38 +--------- src/Commands/sql/SqlDumpCommand.php | 75 +++++++++++++++++++ src/Formatters/FormatterTrait.php | 9 +-- src/Listeners/OptionsetSqlListener.php | 34 +++++++++ .../OptionsetTableSelectionListener.php | 37 +++++++++ tests/integration/ImageTest.php | 4 +- 11 files changed, 162 insertions(+), 52 deletions(-) create mode 100644 src/Commands/sql/SqlDumpCommand.php create mode 100644 src/Listeners/OptionsetSqlListener.php create mode 100644 src/Listeners/OptionsetTableSelectionListener.php diff --git a/src/Attributes/OptionsetSql.php b/src/Attributes/OptionsetSql.php index 98d0f5d6c4..02e19631f7 100644 --- a/src/Attributes/OptionsetSql.php +++ b/src/Attributes/OptionsetSql.php @@ -8,7 +8,7 @@ use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drush\Commands\DrushCommands; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class OptionsetSql { public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) diff --git a/src/Attributes/OptionsetTableSelection.php b/src/Attributes/OptionsetTableSelection.php index 0cec9996d4..7d84ce06ca 100644 --- a/src/Attributes/OptionsetTableSelection.php +++ b/src/Attributes/OptionsetTableSelection.php @@ -8,7 +8,7 @@ use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drush\Commands\DrushCommands; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class OptionsetTableSelection { public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) @@ -18,7 +18,6 @@ public static function handle(\ReflectionAttribute $attribute, CommandInfo $comm $commandInfo->addOption('tables-key', 'A key in the $tables array.', [], DrushCommands::REQ); $commandInfo->addOption('skip-tables-list', 'A comma-separated list of tables to exclude completely.', [], DrushCommands::REQ); $commandInfo->addOption('structure-tables-list', 'A comma-separated list of tables to include for structure, but not data.', [], DrushCommands::REQ); - $commandInfo->addOption('skip-tables-list', 'A comma-separated list of tables to exclude completely.', [], DrushCommands::REQ); $commandInfo->addOption('tables-list', 'A comma-separated list of tables to transfer.', [], DrushCommands::REQ); } } diff --git a/src/Commands/core/ImageCommands.php b/src/Commands/core/ImageCommands.php index 561f093ef6..e544c6210e 100644 --- a/src/Commands/core/ImageCommands.php +++ b/src/Commands/core/ImageCommands.php @@ -13,8 +13,8 @@ final class ImageCommands extends DrushCommands { const DERIVE = 'image:derive'; - #[Deprecated('Use ImageFlushCommand::FLUSH instead.')] - const FLUSH = ImageFlushCommand::FLUSH; + #[Deprecated(replacement: 'ImageFlushCommand::NAME')] + const FLUSH = ImageFlushCommand::NAME; /** * Create an image derivative. diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 6784cf5dad..b7200e446d 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -17,7 +17,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( - name: self::FLUSH, + name: self::NAME, description: 'Flush all derived images for a given style.', aliases: ['if', 'image-flush'] )] @@ -27,7 +27,7 @@ final class ImageFlushCommand extends Command { use AutowireTrait; - public const FLUSH = 'image:flush'; + public const NAME = 'image:flush'; public function __construct( private readonly SymfonyStyle $io diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index acada4113c..d919b40593 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -23,7 +23,7 @@ use Symfony\Component\Finder\Finder; #[AsCommand( - name: self::UNUSED, + name: self::NAME, description: 'Find potentially unused Twig templates.', aliases: ['twu'] )] @@ -35,7 +35,7 @@ final class TwigUnusedCommand extends Command use AutowireTrait; use FormatterTrait; - public const UNUSED = 'twig:unused'; + public const NAME = 'twig:unused'; public function __construct( protected readonly FormatterManager $formatterManager, diff --git a/src/Commands/sql/SqlCommands.php b/src/Commands/sql/SqlCommands.php index f3d91f5356..424958c824 100644 --- a/src/Commands/sql/SqlCommands.php +++ b/src/Commands/sql/SqlCommands.php @@ -8,7 +8,6 @@ use Consolidation\AnnotatedCommand\Hooks\HookManager; use Consolidation\AnnotatedCommand\Input\StdinAwareInterface; use Consolidation\AnnotatedCommand\Input\StdinAwareTrait; -use Consolidation\OutputFormatters\StructuredData\PropertyList; use Consolidation\SiteProcess\Util\Tty; use Drupal\Core\Database\Database; use Drush\Attributes as CLI; @@ -19,6 +18,7 @@ use Drush\Exceptions\UserAbortException; use Drush\Exec\ExecTrait; use Drush\Sql\SqlBase; +use JetBrains\PhpStorm\Deprecated; use Symfony\Component\Console\Input\InputInterface; final class SqlCommands extends DrushCommands implements StdinAwareInterface @@ -32,6 +32,7 @@ final class SqlCommands extends DrushCommands implements StdinAwareInterface const DROP = 'sql:drop'; const CLI = 'sql:cli'; const QUERY = 'sql:query'; + #[Deprecated(reason: 'Moved', replacement: SqlDumpCommand::NAME)] const DUMP = 'sql:dump'; #[CLI\Command(name: self::CONF, aliases: ['sql-conf'])] @@ -187,41 +188,6 @@ public function query($query = '', $options = ['result-file' => null, 'file' => return true; } - /** - * Exports the Drupal DB as SQL using mysqldump or equivalent. - * - * --create-db is used by sql-sync, since including the DROP TABLE statements interferes with the import when the database is created. - */ - #[CLI\Command(name: self::DUMP, aliases: ['sql-dump'])] - #[CLI\Bootstrap(level: DrupalBootLevels::MAX, max_level: DrupalBootLevels::CONFIGURATION)] - #[CLI\OptionsetSql] - #[CLI\OptionsetTableSelection] - #[CLI\Option(name: 'result-file', description: "Save to a file. The file should be relative to Drupal root. If --result-file is provided with the value 'auto', a date-based filename will be created under ~/drush-backups directory.")] - #[CLI\Option(name: 'create-db', description: 'Omit DROP TABLE statements. Used by Postgres and Oracle only.')] - #[CLI\Option(name: 'data-only', description: 'Dump data without statements to create any of the schema.')] - #[CLI\Option(name: 'ordered-dump', description: 'Order by primary key and add line breaks for efficient diffs. Slows down the dump. Mysql only.')] - #[CLI\Option(name: 'gzip', description: 'Compress the dump using the gzip program which must be in your $PATH.')] - #[CLI\Option(name: 'extra', description: 'Add custom arguments/options when connecting to database (used internally to list tables).')] - #[CLI\Option(name: 'extra-dump', description: 'Add custom arguments/options to the dumping of the database (e.g. mysqldump command).')] - #[CLI\Usage(name: 'drush sql:dump --result-file=../18.sql', description: 'Save SQL dump to the directory above Drupal root.')] - #[CLI\Usage(name: 'drush sql:dump --skip-tables-key=common', description: 'Skip standard tables. See [Drush configuration](../../using-drush-configuration)')] - #[CLI\Usage(name: 'drush sql:dump --extra-dump=--no-data', description: 'Pass extra option to mysqldump command.')] - #[CLI\FieldLabels(labels: ['path' => 'Path'])] - public function dump($options = ['result-file' => self::REQ, 'create-db' => false, 'data-only' => false, 'ordered-dump' => false, 'gzip' => false, 'extra' => self::REQ, 'extra-dump' => self::REQ, 'format' => 'null']): PropertyList - { - $sql = SqlBase::create($options); - $return = $sql->dump(); - if ($return === false) { - throw new \Exception('Unable to dump database. Rerun with --debug to see any error message.'); - } - - // SqlBase::dump() returns null if 'result-file' option is empty. - if ($return) { - $this->logger()->success(dt('Database dump saved to !path', ['!path' => $return])); - } - return new PropertyList(['path' => $return]); - } - /** * Assert that `mysql` or similar are on the user's PATH. */ diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php new file mode 100644 index 0000000000..91e35489e5 --- /dev/null +++ b/src/Commands/sql/SqlDumpCommand.php @@ -0,0 +1,75 @@ + 'Path'])] +final class SqlDumpCommand extends Command +{ + use AutowireTrait; + use FormatterTrait; + + public const NAME = 'sql:dump'; + + public function __construct( + protected readonly FormatterManager $formatterManager, + private readonly SymfonyStyle $io + ) { + parent::__construct(); + } + + protected function configure() + { + $this + ->addOption('result-file', null, InputOption::VALUE_REQUIRED, 'Save to a file. The file should be relative to Drupal root. If --result-file is provided with the value \'auto\', a date-based filename will be created under ~/drush-backups directory.') + ->addOption('create-db', null, InputOption::VALUE_NONE, 'Omit DROP TABLE statements. Used by Postgres and Oracle only.') + ->addOption('data-only', null, InputOption::VALUE_NONE, 'Dump data without statements to create any of the schema.') + ->addOption('ordered-dump', null, InputOption::VALUE_NONE, 'Order by primary key and add line breaks for efficient diffs. Slows down the dump. Mysql only.') + ->addOption('gzip', null, InputOption::VALUE_NONE, 'Compress the dump using the gzip program which must be in your $PATH.') + ->addOption('extra', null, InputOption::VALUE_REQUIRED, 'Add custom arguments/options when connecting to database (used internally to list tables).') + ->addOption('extra-dump', null, InputOption::VALUE_REQUIRED, 'Add custom arguments/options to the dumping of the database (e.g. mysqldump command).') + ->addUsage('sql:dump --result-file=../18.sql') + ->addUsage('sql:dump --skip-tables-key=common') + ->addUsage('sql:dump --extra-dump=--no-data') + ->setHelp('--create-db is used by sql:sync, since including the DROP TABLE statements interferes with the import when the database is created.'); + $this->addFormatterOptions(); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): PropertyList + { + $sql = SqlBase::create($input->getOptions()); + $return = $sql->dump(); + if ($return === false) { + throw new \Exception('Unable to dump database. Rerun with --debug to see any error message.'); + } + + // SqlBase::dump() returns null if 'result-file' option is empty. + if ($return) { + $this->io->success(dt('Database dump saved to !path', ['!path' => $return])); + } + return new PropertyList(['path' => $return]); + } +} diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index 71a855d3f8..b9334adeda 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -48,18 +48,17 @@ public function execute(InputInterface $input, OutputInterface $output): int $formatterOptions = new FormatterOptions($configurationData, $input->getOptions()); $formatterOptions->setInput($input); $data = $this->doExecute($input, $output); - $data = $this->alterResult($data, $input); + if ($input->hasOption('filter')) { + $data = $this->alterResult($data, $input); + } $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); return Command::SUCCESS; } protected function alterResult($result, InputInterface $input): mixed { - $reflection = new \ReflectionObject($this); $expression = $input->getOption('filter'); - if (empty($expression)) { - return $result; - } + $reflection = new \ReflectionObject($this); $attributes = $reflection->getAttributes(FilterDefaultField::class); $instance = $attributes[0]->newInstance(); $factory = LogicalOpFactory::get(); diff --git a/src/Listeners/OptionsetSqlListener.php b/src/Listeners/OptionsetSqlListener.php new file mode 100644 index 0000000000..a9641f4bc3 --- /dev/null +++ b/src/Listeners/OptionsetSqlListener.php @@ -0,0 +1,34 @@ +getApplication()->all() as $id => $command) { + $reflection = new \ReflectionObject($command); + $attributes = $reflection->getAttributes(CLI\OptionsetSql::class); + if (empty($attributes)) { + continue; + } + $command->addOption('database', '', InputOption::VALUE_REQUIRED, 'The DB connection key if using multiple connections in settings.php.', 'default'); + $command->addOption('db-url', '', InputOption::VALUE_REQUIRED, 'A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbname'); + $command->addOption('target', '', InputOption::VALUE_REQUIRED, 'The name of a target within the specified database connection.', 'default'); + $command->addOption('show-passwords', '', InputOption::VALUE_NONE, 'Show password on the CLI. Useful for debugging.'); + } + } + + +} diff --git a/src/Listeners/OptionsetTableSelectionListener.php b/src/Listeners/OptionsetTableSelectionListener.php new file mode 100644 index 0000000000..6e6f54ee60 --- /dev/null +++ b/src/Listeners/OptionsetTableSelectionListener.php @@ -0,0 +1,37 @@ +getApplication()->all() as $id => $command) { + $reflection = new \ReflectionObject($command); + $attributes = $reflection->getAttributes(OptionsetTableSelection::class); + if (empty($attributes)) { + continue; + } + $command->addOption('skip-tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)'); + $command->addOption('structure-tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)'); + $command->addOption('tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $tables array.'); + $command->addOption('skip-tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to exclude completely.'); + $command->addOption('structure-tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to include for structure, but not data.'); + $command->addOption('tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to transfer.', []); + } + } + + +} diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 2a2c148d52..5e75ef45c0 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -36,7 +36,7 @@ public function testImage() $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. - $this->drush(ImageFlushCommand::FLUSH, [$style_name], ['all' => null]); + $this->drush(ImageFlushCommand::NAME, [$style_name], ['all' => null]); $this->assertFileDoesNotExist($thumbnail); // Check that "drush image-flush --all" deletes all image styles by creating two different ones and testing its @@ -45,7 +45,7 @@ public function testImage() $this->assertFileExists($thumbnail); $this->drush(ImageCommands::DERIVE, ['medium', $logo]); $this->assertFileExists($medium); - $this->drush(ImageFlushCommand::FLUSH, [], ['all' => null]); + $this->drush(ImageFlushCommand::NAME, [], ['all' => null]); $this->assertFileDoesNotExist($thumbnail); $this->assertFileDoesNotExist($medium); } From ac5ad80d1b4da6e56a2b2d404dc34e8ddf7ad735 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 15 Oct 2024 18:27:48 -0400 Subject: [PATCH 19/48] PHPCS --- src/Listeners/OptionsetSqlListener.php | 3 --- src/Listeners/OptionsetTableSelectionListener.php | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/Listeners/OptionsetSqlListener.php b/src/Listeners/OptionsetSqlListener.php index a9641f4bc3..d6182f70d7 100644 --- a/src/Listeners/OptionsetSqlListener.php +++ b/src/Listeners/OptionsetSqlListener.php @@ -14,7 +14,6 @@ #[CLI\Bootstrap(level: DrupalBootLevels::NONE)] class OptionsetSqlListener { - public function __invoke(ConsoleDefinitionsEvent $event): void { foreach ($event->getApplication()->all() as $id => $command) { @@ -29,6 +28,4 @@ public function __invoke(ConsoleDefinitionsEvent $event): void $command->addOption('show-passwords', '', InputOption::VALUE_NONE, 'Show password on the CLI. Useful for debugging.'); } } - - } diff --git a/src/Listeners/OptionsetTableSelectionListener.php b/src/Listeners/OptionsetTableSelectionListener.php index 6e6f54ee60..836395b308 100644 --- a/src/Listeners/OptionsetTableSelectionListener.php +++ b/src/Listeners/OptionsetTableSelectionListener.php @@ -15,7 +15,6 @@ #[CLI\Bootstrap(level: DrupalBootLevels::NONE)] class OptionsetTableSelectionListener { - public function __invoke(ConsoleDefinitionsEvent $event): void { foreach ($event->getApplication()->all() as $id => $command) { @@ -32,6 +31,4 @@ public function __invoke(ConsoleDefinitionsEvent $event): void $command->addOption('tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to transfer.', []); } } - - } From 0239f0f35a39448a7a87f91a97c23b8d89ec755c Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 16 Oct 2024 06:54:40 -0400 Subject: [PATCH 20/48] Experiment with static method to add OptionSets --- src/Commands/OptionSets.php | 17 ++++++++++++++ src/Commands/core/ImageFlushCommand.php | 2 +- src/Commands/sql/SqlDumpCommand.php | 3 ++- src/Formatters/FormatterTrait.php | 3 +-- src/Listeners/OptionsetSqlListener.php | 31 ------------------------- 5 files changed, 21 insertions(+), 35 deletions(-) create mode 100644 src/Commands/OptionSets.php delete mode 100644 src/Listeners/OptionsetSqlListener.php diff --git a/src/Commands/OptionSets.php b/src/Commands/OptionSets.php new file mode 100644 index 0000000000..9ff331d657 --- /dev/null +++ b/src/Commands/OptionSets.php @@ -0,0 +1,17 @@ +addOption('database', '', InputOption::VALUE_REQUIRED, 'The DB connection key if using multiple connections in settings.php.', 'default'); + $command->addOption('db-url', '', InputOption::VALUE_REQUIRED, 'A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbname'); + $command->addOption('target', '', InputOption::VALUE_REQUIRED, 'The name of a target within the specified database connection.', 'default'); + $command->addOption('show-passwords', '', InputOption::VALUE_NONE, 'Show password on the CLI. Useful for debugging.'); + } +} diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index b7200e446d..18851695c0 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -73,6 +73,6 @@ public function execute(InputInterface $input, OutputInterface $output): int $style->flush(); $this->io->success("Image style $style_name flushed"); } - return Command::SUCCESS; + return static::SUCCESS; } } diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 91e35489e5..aa2e9f5f8d 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -9,6 +9,7 @@ use Drush\Attributes as CLI; use Drush\Boot\DrupalBootLevels; use Drush\Commands\AutowireTrait; +use Drush\Commands\OptionSets; use Drush\Formatters\FormatterTrait; use Drush\Sql\SqlBase; use Symfony\Component\Console\Attribute\AsCommand; @@ -24,7 +25,6 @@ aliases: ['sql-dump'] )] #[CLI\Bootstrap(level: DrupalBootLevels::MAX, max_level: DrupalBootLevels::CONFIGURATION)] -#[CLI\OptionsetSql] #[CLI\OptionsetTableSelection] #[CLI\FieldLabels(labels: ['path' => 'Path'])] final class SqlDumpCommand extends Command @@ -56,6 +56,7 @@ protected function configure() ->addUsage('sql:dump --extra-dump=--no-data') ->setHelp('--create-db is used by sql:sync, since including the DROP TABLE statements interferes with the import when the database is created.'); $this->addFormatterOptions(); + OptionSets::sql($this); } protected function doExecute(InputInterface $input, OutputInterface $output): PropertyList diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index b9334adeda..f33f9e6ae1 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -6,7 +6,6 @@ use Consolidation\Filter\LogicalOpFactory; use Consolidation\OutputFormatters\Options\FormatterOptions; use Drush\Attributes\FilterDefaultField; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -52,7 +51,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $data = $this->alterResult($data, $input); } $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); - return Command::SUCCESS; + return static::SUCCESS; } protected function alterResult($result, InputInterface $input): mixed diff --git a/src/Listeners/OptionsetSqlListener.php b/src/Listeners/OptionsetSqlListener.php deleted file mode 100644 index d6182f70d7..0000000000 --- a/src/Listeners/OptionsetSqlListener.php +++ /dev/null @@ -1,31 +0,0 @@ -getApplication()->all() as $id => $command) { - $reflection = new \ReflectionObject($command); - $attributes = $reflection->getAttributes(CLI\OptionsetSql::class); - if (empty($attributes)) { - continue; - } - $command->addOption('database', '', InputOption::VALUE_REQUIRED, 'The DB connection key if using multiple connections in settings.php.', 'default'); - $command->addOption('db-url', '', InputOption::VALUE_REQUIRED, 'A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbname'); - $command->addOption('target', '', InputOption::VALUE_REQUIRED, 'The name of a target within the specified database connection.', 'default'); - $command->addOption('show-passwords', '', InputOption::VALUE_NONE, 'Show password on the CLI. Useful for debugging.'); - } - } -} From 8b16ff4975dac97e0912c10a069c540f898b4485 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 16 Oct 2024 07:45:33 -0400 Subject: [PATCH 21/48] Demonstrate a static method approach for Validators --- src/Commands/Validators.php | 16 ++++++++++++++++ src/Commands/core/ImageFlushCommand.php | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/Commands/Validators.php diff --git a/src/Commands/Validators.php b/src/Commands/Validators.php new file mode 100644 index 0000000000..8eb175951f --- /dev/null +++ b/src/Commands/Validators.php @@ -0,0 +1,16 @@ +getStorage($entityType)->loadMultiple($ids); + if ($missing = array_diff($ids, array_keys($loaded))) { + $msg = dt('Unable to load the !type: !str', ['!type' => $entityType, '!str' => implode(', ', $missing)]); + throw new \Exception($msg); + } + } +} diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 18851695c0..3dd59b6358 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -7,6 +7,7 @@ use Drupal\image\Entity\ImageStyle; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; +use Drush\Commands\Validators; use Drush\Utils\StringUtils; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -21,7 +22,6 @@ description: 'Flush all derived images for a given style.', aliases: ['if', 'image-flush'] )] -#[CLI\ValidateEntityLoad(entityType: 'image_style', argumentName: 'style_names')] #[CLI\ValidateModulesEnabled(modules: ['image'])] final class ImageFlushCommand extends Command { @@ -64,6 +64,10 @@ protected function interact(InputInterface $input, OutputInterface $output): voi public function execute(InputInterface $input, OutputInterface $output): int { + if ($names = $input->getArgument('style_names')) { + Validators::entityLoad(StringUtils::csvToArray($names), 'image_style'); + } + // Needed for non-interactive requests. if ($input->getOption('all')) { $input->setArgument('style_names', implode(',', array_keys(ImageStyle::loadMultiple()))); From 7d6e8ea3161380129c1931ac10fab0944b40963d Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 16 Oct 2024 11:54:11 -0400 Subject: [PATCH 22/48] Experiment with FormatterOptions via code not Attributes --- src/Commands/core/TwigUnusedCommand.php | 8 ++++ src/Commands/sql/SqlDumpCommand.php | 8 +++- src/Formatters/FormatterTrait.php | 11 +++-- src/Listeners/ValidateEntityLoadListener.php | 47 -------------------- 4 files changed, 22 insertions(+), 52 deletions(-) delete mode 100644 src/Listeners/ValidateEntityLoadListener.php diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index d919b40593..5fa3461c22 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -5,6 +5,7 @@ namespace Drush\Commands\core; use Consolidation\OutputFormatters\FormatterManager; +use Consolidation\OutputFormatters\Options\FormatterOptions; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\Template\TwigEnvironment; @@ -90,4 +91,11 @@ public function doExecute(InputInterface $input, OutputInterface $output): RowsO $this->logger->notice('Found {count} unused', ['count' => count($unused)]); return new RowsOfFields($unused); } + + + protected function getFormatterOptions(): FormatterOptions + { + // @todo WIP + return new FormatterOptions($this->getConfigurationData()); + } } diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index aa2e9f5f8d..4efc6faa2d 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -5,6 +5,7 @@ namespace Drush\Commands\sql; use Consolidation\OutputFormatters\FormatterManager; +use Consolidation\OutputFormatters\Options\FormatterOptions; use Consolidation\OutputFormatters\StructuredData\PropertyList; use Drush\Attributes as CLI; use Drush\Boot\DrupalBootLevels; @@ -26,7 +27,6 @@ )] #[CLI\Bootstrap(level: DrupalBootLevels::MAX, max_level: DrupalBootLevels::CONFIGURATION)] #[CLI\OptionsetTableSelection] -#[CLI\FieldLabels(labels: ['path' => 'Path'])] final class SqlDumpCommand extends Command { use AutowireTrait; @@ -73,4 +73,10 @@ protected function doExecute(InputInterface $input, OutputInterface $output): Pr } return new PropertyList(['path' => $return]); } + + protected function getFormatterOptions(): FormatterOptions + { + return (new FormatterOptions()) + ->setFieldLabels(['path' => 'Path']); + } } diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index f33f9e6ae1..3accc3f03d 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -14,7 +14,6 @@ trait FormatterTrait { public function addFormatterOptions() { - $formatterOptions = new FormatterOptions($this->getConfigurationData(), []); $reflection = new \ReflectionMethod($this, 'doExecute'); $returnType = $reflection->getReturnType(); if ($returnType instanceof \ReflectionNamedType) { @@ -22,7 +21,7 @@ public function addFormatterOptions() } else { throw new \Exception($reflection->getDeclaringClass() . '::doExecute method must specify a return type.'); } - $inputOptions = $this->formatterManager->automaticOptions($formatterOptions, $dataType); + $inputOptions = $this->formatterManager->automaticOptions($this->getFormatterOptions(), $dataType); foreach ($inputOptions as $inputOption) { $mode = $this->getPrivatePropValue($inputOption, 'mode'); $suggestedValues = $this->getPrivatePropValue($inputOption, 'suggestedValues'); @@ -43,8 +42,7 @@ public function addFormatterOptions() */ public function execute(InputInterface $input, OutputInterface $output): int { - $configurationData = $this->getConfigurationData(); - $formatterOptions = new FormatterOptions($configurationData, $input->getOptions()); + $formatterOptions = $this->getFormatterOptions(); $formatterOptions->setInput($input); $data = $this->doExecute($input, $output); if ($input->hasOption('filter')) { @@ -109,4 +107,9 @@ protected function getConfigurationData(): array * to help the formatter know what to expect. */ abstract protected function doExecute(InputInterface $input, OutputInterface $output); + + /** + * Override this method with the commands's formatter config. + */ + abstract protected function getFormatterOptions(): FormatterOptions; } diff --git a/src/Listeners/ValidateEntityLoadListener.php b/src/Listeners/ValidateEntityLoadListener.php deleted file mode 100644 index 2d26262c00..0000000000 --- a/src/Listeners/ValidateEntityLoadListener.php +++ /dev/null @@ -1,47 +0,0 @@ -getCommand(); - $reflection = new \ReflectionObject($command); - $attributes = $reflection->getAttributes(ValidateEntityLoad::class); - if (empty($attributes)) { - return; - } - $instance = $attributes[0]->newInstance(); - $names = StringUtils::csvToArray($event->getInput()->getArgument($instance->argumentName)); - $loaded = $this->entityTypeManager->getStorage($instance->entityType)->loadMultiple($names); - if ($missing = array_diff($names, array_keys($loaded))) { - $msg = dt('Unable to load the !type: !str', ['!type' => $instance->entityType, '!str' => implode(', ', $missing)]); - $this->logger->error($msg); - $event->disableCommand(); - } - } -} From 9847b616ff210a975a7079839f0e688758b21a96 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 16 Oct 2024 14:55:06 -0400 Subject: [PATCH 23/48] Build io when needed - don't get it from the container --- src/Commands/core/ImageFlushCommand.php | 14 ++++++++------ src/Commands/sql/SqlDumpCommand.php | 8 ++++---- src/Runtime/DependencyInjection.php | 7 ------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 3dd59b6358..d4829e749e 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -4,10 +4,12 @@ namespace Drush\Commands\core; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\image\Entity\ImageStyle; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; use Drush\Commands\Validators; +use Drush\Style\DrushStyle; use Drush\Utils\StringUtils; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -15,7 +17,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: self::NAME, @@ -30,11 +31,10 @@ final class ImageFlushCommand extends Command public const NAME = 'image:flush'; public function __construct( - private readonly SymfonyStyle $io + private readonly EntityTypeManagerInterface $entityTypeManager ) { parent::__construct(); } - protected function configure(): void { $this @@ -47,14 +47,15 @@ protected function configure(): void protected function interact(InputInterface $input, OutputInterface $output): void { - $styles = array_keys(ImageStyle::loadMultiple()); + $io = new DrushStyle($input, $output); + $styles = array_keys($this->entityTypeManager->getStorage('image_style')->loadMultiple()); $style_names = $input->getArgument('style_names'); if (empty($style_names) && !$input->getOption('all')) { $styles_all = $styles; array_unshift($styles_all, 'all'); $choices = array_combine($styles_all, $styles_all); - $style_names = $this->io->choice(dt("Choose a style to flush"), $choices, 'all'); + $style_names = $io->choice(dt("Choose a style to flush"), $choices, 'all'); if ($style_names == 'all') { $style_names = implode(',', $styles); } @@ -75,7 +76,8 @@ public function execute(InputInterface $input, OutputInterface $output): int foreach (ImageStyle::loadMultiple(StringUtils::csvToArray($input->getArgument('style_names'))) as $style_name => $style) { $style->flush(); - $this->io->success("Image style $style_name flushed"); + $io = new DrushStyle($input, $output); + $io->success("Image style $style_name flushed"); } return static::SUCCESS; } diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 4efc6faa2d..effc14be30 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -13,12 +13,12 @@ use Drush\Commands\OptionSets; use Drush\Formatters\FormatterTrait; use Drush\Sql\SqlBase; +use Drush\Style\DrushStyle; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: self::NAME, @@ -35,8 +35,7 @@ final class SqlDumpCommand extends Command public const NAME = 'sql:dump'; public function __construct( - protected readonly FormatterManager $formatterManager, - private readonly SymfonyStyle $io + protected readonly FormatterManager $formatterManager ) { parent::__construct(); } @@ -69,7 +68,8 @@ protected function doExecute(InputInterface $input, OutputInterface $output): Pr // SqlBase::dump() returns null if 'result-file' option is empty. if ($return) { - $this->io->success(dt('Database dump saved to !path', ['!path' => $return])); + $io = new DrushStyle($input, $output); + $io->success(dt('Database dump saved to !path', ['!path' => $return])); } return new PropertyList(['path' => $return]); } diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 36e5b1b47a..ba9e6f22d4 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -24,7 +24,6 @@ use Drush\Formatters\EntityToArraySimplifier; use Drush\Log\Logger; use Drush\SiteAlias\ProcessManager; -use Drush\Style\DrushStyle; use Drush\Symfony\DrushStyleInjector; use League\Container\Container; use League\Container\ContainerInterface; @@ -32,7 +31,6 @@ use Robo\Robo; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; /** * Prepare our Dependency Injection Container @@ -168,11 +166,6 @@ protected function alterServicesForDrush($container, Application $application, I $paramInjection = $container->get('parameterInjection'); $paramInjection->register('Symfony\Component\Console\Style\SymfonyStyle', new DrushStyleInjector()); - Robo::addShared($container, 'io', DrushStyle::class) - ->addArguments([$input, $output]); - // @todo Does this alias interfere with the paramInjector above? Could we drop that feature? - Robo::addShared($container, SymfonyStyle::class, 'io'); // For autowiring - // Add our own callback to the hook manager $hookManager = $container->get('hookManager'); $hookManager->addCommandEvent(new GlobalOptionsEventListener()); From b9e11f3d85addbdbd8f535978ca33514b76cbf23 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 16 Oct 2024 15:11:31 -0400 Subject: [PATCH 24/48] Use DI --- src/Commands/core/ImageFlushCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index d4829e749e..e3556b879c 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -74,7 +74,8 @@ public function execute(InputInterface $input, OutputInterface $output): int $input->setArgument('style_names', implode(',', array_keys(ImageStyle::loadMultiple()))); } - foreach (ImageStyle::loadMultiple(StringUtils::csvToArray($input->getArgument('style_names'))) as $style_name => $style) { + $ids = StringUtils::csvToArray($input->getArgument('style_names')); + foreach ($this->entityTypeManager->getStorage('image_style')->loadMultiple($ids) as $style_name => $style) { $style->flush(); $io = new DrushStyle($input, $output); $io->success("Image style $style_name flushed"); From 6216ad68e4f0a41013e879ecb206877abaf57be5 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 18 Oct 2024 08:23:30 -0400 Subject: [PATCH 25/48] Less magic - commands that format have own execute() with a bit of boilerplate --- src/Commands/core/TwigUnusedCommand.php | 12 ++++++- src/Commands/sql/SqlDumpCommand.php | 11 +++++- src/Formatters/FormatterTrait.php | 46 ++++++------------------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 5fa3461c22..51eb828d48 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -54,7 +54,17 @@ protected function configure(): void // Usages can't have a description with plain Console :(. Use setHelp() if desired as per https://github.com/symfony/symfony/issues/45050 ->addUsage('twig:unused /var/www/mass.local/docroot/modules/custom,/var/www/mass.local/docroot/themes/custom') ->setHelp('Immediately before running this command, web crawl your entire web site. Or use your Production PHPStorage dir for comparison.'); - $this->addFormatterOptions(); + $this->addFormatterOptions(RowsOfFields::class); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $formatterOptions = $this->getFormatterOptions(); + $formatterOptions->setInput($input); + $data = $this->doExecute($input, $output); + $data = $this->alterResult($data, $input); + $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); + return self::SUCCESS; } public function doExecute(InputInterface $input, OutputInterface $output): RowsOfFields diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index effc14be30..334a5fd551 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -54,10 +54,19 @@ protected function configure() ->addUsage('sql:dump --skip-tables-key=common') ->addUsage('sql:dump --extra-dump=--no-data') ->setHelp('--create-db is used by sql:sync, since including the DROP TABLE statements interferes with the import when the database is created.'); - $this->addFormatterOptions(); + $this->addFormatterOptions(PropertyList::class); OptionSets::sql($this); } + public function execute(InputInterface $input, OutputInterface $output): int + { + $formatterOptions = $this->getFormatterOptions(); + $formatterOptions->setInput($input); + $data = $this->doExecute($input, $output); + $data = $this->alterResult($data, $input); + $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); + return static::SUCCESS; + } protected function doExecute(InputInterface $input, OutputInterface $output): PropertyList { $sql = SqlBase::create($input->getOptions()); diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index 3accc3f03d..17cb7c0d7c 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -4,23 +4,20 @@ use Consolidation\Filter\FilterOutputData; use Consolidation\Filter\LogicalOpFactory; -use Consolidation\OutputFormatters\Options\FormatterOptions; use Drush\Attributes\FilterDefaultField; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; trait FormatterTrait { - public function addFormatterOptions() + /** + * Adds options to the command definition based on data type. The --format description is dynamic. + * + * @param string $dataType + * Usually the same as the return type of a doExecute() method. + */ + public function addFormatterOptions(string $dataType): void { - $reflection = new \ReflectionMethod($this, 'doExecute'); - $returnType = $reflection->getReturnType(); - if ($returnType instanceof \ReflectionNamedType) { - $dataType = $returnType->getName(); - } else { - throw new \Exception($reflection->getDeclaringClass() . '::doExecute method must specify a return type.'); - } $inputOptions = $this->formatterManager->automaticOptions($this->getFormatterOptions(), $dataType); foreach ($inputOptions as $inputOption) { $mode = $this->getPrivatePropValue($inputOption, 'mode'); @@ -37,23 +34,11 @@ public function addFormatterOptions() } } - /** - * Format the structured data as per user input and the command definition. - */ - public function execute(InputInterface $input, OutputInterface $output): int - { - $formatterOptions = $this->getFormatterOptions(); - $formatterOptions->setInput($input); - $data = $this->doExecute($input, $output); - if ($input->hasOption('filter')) { - $data = $this->alterResult($data, $input); - } - $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); - return static::SUCCESS; - } - protected function alterResult($result, InputInterface $input): mixed { + if (!$input->hasOption('filter') || !$input->getOption('filter')) { + return $result; + } $expression = $input->getOption('filter'); $reflection = new \ReflectionObject($this); $attributes = $reflection->getAttributes(FilterDefaultField::class); @@ -101,15 +86,4 @@ protected function getConfigurationData(): array } return $configurationData; } - - /** - * Override this method with the actual command logic. Type hint the return value - * to help the formatter know what to expect. - */ - abstract protected function doExecute(InputInterface $input, OutputInterface $output); - - /** - * Override this method with the commands's formatter config. - */ - abstract protected function getFormatterOptions(): FormatterOptions; } From 8d7b2e47c54ef1e83b9e8021599c50db47eeb540 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 18 Oct 2024 09:54:17 -0400 Subject: [PATCH 26/48] FormatterOptions now has methods we need to deprecate formatting Attributes --- composer.json | 2 +- src/Attributes/DefaultFields.php | 9 +-------- src/Attributes/DefaultTableFields.php | 9 +-------- src/Attributes/FieldLabels.php | 9 +-------- src/Attributes/FilterDefaultField.php | 9 +-------- src/Attributes/Format.php | 12 +----------- src/Commands/core/TwigUnusedCommand.php | 7 +++---- ...matterConfigurationItemProviderInterface.php | 10 ---------- src/Formatters/FormatterTrait.php | 17 ----------------- 9 files changed, 9 insertions(+), 75 deletions(-) delete mode 100644 src/Formatters/FormatterConfigurationItemProviderInterface.php diff --git a/composer.json b/composer.json index a040b807c4..6114ae8c0f 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "consolidation/annotated-command": "^4.9.2", "consolidation/config": "^2.1.2 || ^3", "consolidation/filter-via-dot-access-data": "^2.0.2", - "consolidation/output-formatters": "^4.3.2", + "consolidation/output-formatters": "dev-use-command-drush as 4.x-dev", "consolidation/robo": "^4.0.6 || ^5", "consolidation/site-alias": "^4", "consolidation/site-process": "^5.2.0", diff --git a/src/Attributes/DefaultFields.php b/src/Attributes/DefaultFields.php index 62680db5b7..21290d491f 100644 --- a/src/Attributes/DefaultFields.php +++ b/src/Attributes/DefaultFields.php @@ -6,10 +6,9 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; -use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -class DefaultFields implements FormatterConfigurationItemProviderInterface +class DefaultFields { const KEY = 'default-fields'; @@ -21,12 +20,6 @@ public function __construct(public array $fields) { } - public function getConfigurationItem(\ReflectionAttribute $attribute): array - { - $args = $attribute->getArguments(); - return [self::KEY => $args['fields']]; - } - public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) { $args = $attribute->getArguments(); diff --git a/src/Attributes/DefaultTableFields.php b/src/Attributes/DefaultTableFields.php index 1a31ba2ab9..f966a52029 100644 --- a/src/Attributes/DefaultTableFields.php +++ b/src/Attributes/DefaultTableFields.php @@ -7,10 +7,9 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Consolidation\OutputFormatters\Options\FormatterOptions; -use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -class DefaultTableFields implements FormatterConfigurationItemProviderInterface +class DefaultTableFields { const KEY = FormatterOptions::DEFAULT_TABLE_FIELDS; @@ -22,12 +21,6 @@ public function __construct(public array $fields) { } - public function getConfigurationItem(\ReflectionAttribute $attribute): array - { - $args = $attribute->getArguments(); - return [self::KEY => $args['fields']]; - } - public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) { $args = $attribute->getArguments(); diff --git a/src/Attributes/FieldLabels.php b/src/Attributes/FieldLabels.php index dfa0c607b2..444cfa2dde 100644 --- a/src/Attributes/FieldLabels.php +++ b/src/Attributes/FieldLabels.php @@ -5,10 +5,9 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Consolidation\OutputFormatters\Options\FormatterOptions; -use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] -class FieldLabels implements FormatterConfigurationItemProviderInterface +class FieldLabels { const KEY = FormatterOptions::FIELD_LABELS; @@ -21,12 +20,6 @@ public function __construct( ) { } - public function getConfigurationItem(\ReflectionAttribute $attribute): array - { - $args = $attribute->getArguments(); - return [self::KEY => $args['labels']]; - } - public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) { $args = $attribute->getArguments(); diff --git a/src/Attributes/FilterDefaultField.php b/src/Attributes/FilterDefaultField.php index adb0eabaa9..994086fd5e 100644 --- a/src/Attributes/FilterDefaultField.php +++ b/src/Attributes/FilterDefaultField.php @@ -4,10 +4,9 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; -use Drush\Formatters\FormatterConfigurationItemProviderInterface; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] -class FilterDefaultField implements FormatterConfigurationItemProviderInterface +class FilterDefaultField { const KEY = 'filter-default-field'; @@ -20,12 +19,6 @@ public function __construct( ) { } - public function getConfigurationItem(\ReflectionAttribute $attribute): array - { - $args = $attribute->getArguments(); - return [self::KEY => $args['field']]; - } - public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) { $args = $attribute->getArguments(); diff --git a/src/Attributes/Format.php b/src/Attributes/Format.php index 236ef88de6..b2f16bc01e 100644 --- a/src/Attributes/Format.php +++ b/src/Attributes/Format.php @@ -7,11 +7,10 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Consolidation\OutputFormatters\Options\FormatterOptions; -use Drush\Formatters\FormatterConfigurationItemProviderInterface; use JetBrains\PhpStorm\ExpectedValues; #[Attribute(Attribute::TARGET_METHOD)] -class Format implements FormatterConfigurationItemProviderInterface +class Format { /** * @param ?string $listDelimiter @@ -32,13 +31,4 @@ public static function handle(\ReflectionAttribute $attribute, CommandInfo $comm $commandInfo->addAnnotation(FormatterOptions::LIST_DELIMITER, $instance->listDelimiter); $commandInfo->addAnnotation(FormatterOptions::TABLE_STYLE, $instance->tableStyle); } - - public function getConfigurationItem(\ReflectionAttribute $attribute): array - { - $instance = $attribute->newInstance(); - return [ - FormatterOptions::LIST_DELIMITER => $instance->listDelimiter, - FormatterOptions::TABLE_STYLE => $instance->tableStyle - ]; - } } diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 51eb828d48..5837678581 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -28,8 +28,6 @@ description: 'Find potentially unused Twig templates.', aliases: ['twu'] )] -#[CLI\FieldLabels(labels: ['template' => 'Template', 'compiled' => 'Compiled'])] -#[CLI\DefaultTableFields(fields: ['template', 'compiled'])] #[CLI\FilterDefaultField(field: 'template')] final class TwigUnusedCommand extends Command { @@ -105,7 +103,8 @@ public function doExecute(InputInterface $input, OutputInterface $output): RowsO protected function getFormatterOptions(): FormatterOptions { - // @todo WIP - return new FormatterOptions($this->getConfigurationData()); + return (new FormatterOptions()) + ->setFieldLabels(['template' => 'Template', 'compiled' => 'Compiled']) + ->setTableDefaultFields(['template', 'compiled']); } } diff --git a/src/Formatters/FormatterConfigurationItemProviderInterface.php b/src/Formatters/FormatterConfigurationItemProviderInterface.php deleted file mode 100644 index 78314cad85..0000000000 --- a/src/Formatters/FormatterConfigurationItemProviderInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -getProperty($name); return $prop->getValue($object); } - - /** - * Build the formatter configuration from the command's attributes - */ - protected function getConfigurationData(): array - { - $configurationData = []; - $reflection = new \ReflectionObject($this); - $attributes = $reflection->getAttributes(); - foreach ($attributes as $attribute) { - $instance = $attribute->newInstance(); - if ($instance instanceof FormatterConfigurationItemProviderInterface) { - $configurationData = array_merge($configurationData, $instance->getConfigurationItem($attribute)); - } - } - return $configurationData; - } } From 1a795af1a51e92e55cda40c523dda40fc28210b0 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 18 Oct 2024 15:49:58 -0400 Subject: [PATCH 27/48] Revert Formatting Attributes and use new fluid syntax for setInput --- composer.json | 5 +++-- src/Attributes/DefaultFields.php | 20 ++------------------ src/Attributes/DefaultTableFields.php | 21 ++------------------- src/Attributes/FieldLabels.php | 24 ++++-------------------- src/Attributes/FilterDefaultField.php | 23 ++++------------------- src/Commands/core/TwigUnusedCommand.php | 3 +-- src/Commands/sql/SqlDumpCommand.php | 3 +-- 7 files changed, 17 insertions(+), 82 deletions(-) diff --git a/composer.json b/composer.json index 6114ae8c0f..4e3992b626 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "consolidation/annotated-command": "^4.9.2", "consolidation/config": "^2.1.2 || ^3", "consolidation/filter-via-dot-access-data": "^2.0.2", - "consolidation/output-formatters": "dev-use-command-drush as 4.x-dev", + "consolidation/output-formatters": "dev-use-command-directly-of as 4.x-dev", "consolidation/robo": "^4.0.6 || ^5", "consolidation/site-alias": "^4", "consolidation/site-process": "^5.2.0", @@ -88,7 +88,8 @@ "config": { "allow-plugins": { "composer/installers": true, - "cweagans/composer-patches": true + "cweagans/composer-patches": true, + "symfony/runtime": true }, "optimize-autoloader": true, "preferred-install": "dist", diff --git a/src/Attributes/DefaultFields.php b/src/Attributes/DefaultFields.php index 21290d491f..58b0e02a9f 100644 --- a/src/Attributes/DefaultFields.php +++ b/src/Attributes/DefaultFields.php @@ -5,24 +5,8 @@ namespace Drush\Attributes; use Attribute; -use Consolidation\AnnotatedCommand\Parser\CommandInfo; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -class DefaultFields +#[Attribute(Attribute::TARGET_METHOD)] +class DefaultFields extends \Consolidation\AnnotatedCommand\Attributes\DefaultFields { - const KEY = 'default-fields'; - - /** - * @param $fields - * An array of field names to show by default. - */ - public function __construct(public array $fields) - { - } - - public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) - { - $args = $attribute->getArguments(); - $commandInfo->addAnnotation('default-fields', $args['fields']); - } } diff --git a/src/Attributes/DefaultTableFields.php b/src/Attributes/DefaultTableFields.php index f966a52029..7e340474bf 100644 --- a/src/Attributes/DefaultTableFields.php +++ b/src/Attributes/DefaultTableFields.php @@ -5,25 +5,8 @@ namespace Drush\Attributes; use Attribute; -use Consolidation\AnnotatedCommand\Parser\CommandInfo; -use Consolidation\OutputFormatters\Options\FormatterOptions; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -class DefaultTableFields +#[Attribute(Attribute::TARGET_METHOD)] +class DefaultTableFields extends \Consolidation\AnnotatedCommand\Attributes\DefaultTableFields { - const KEY = FormatterOptions::DEFAULT_TABLE_FIELDS; - - /** - * @param $fields - * An array of field names to show by default when using table formatter. - */ - public function __construct(public array $fields) - { - } - - public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) - { - $args = $attribute->getArguments(); - $commandInfo->addAnnotation('default-table-fields', $args['fields']); - } } diff --git a/src/Attributes/FieldLabels.php b/src/Attributes/FieldLabels.php index 444cfa2dde..6e7a26a1b2 100644 --- a/src/Attributes/FieldLabels.php +++ b/src/Attributes/FieldLabels.php @@ -1,28 +1,12 @@ getArguments(); - $commandInfo->addAnnotation('field-labels', $args['labels']); - } } diff --git a/src/Attributes/FilterDefaultField.php b/src/Attributes/FilterDefaultField.php index 994086fd5e..70d2749cc0 100644 --- a/src/Attributes/FilterDefaultField.php +++ b/src/Attributes/FilterDefaultField.php @@ -1,27 +1,12 @@ getArguments(); - $commandInfo->addAnnotation('filter-default-field', $args['field']); - } } diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 5837678581..8fc93c39d4 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -57,8 +57,7 @@ protected function configure(): void public function execute(InputInterface $input, OutputInterface $output): int { - $formatterOptions = $this->getFormatterOptions(); - $formatterOptions->setInput($input); + $formatterOptions = $this->getFormatterOptions()->setInput($input); $data = $this->doExecute($input, $output); $data = $this->alterResult($data, $input); $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 334a5fd551..431aae2d28 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -60,8 +60,7 @@ protected function configure() public function execute(InputInterface $input, OutputInterface $output): int { - $formatterOptions = $this->getFormatterOptions(); - $formatterOptions->setInput($input); + $formatterOptions = $this->getFormatterOptions()->setInput($input); $data = $this->doExecute($input, $output); $data = $this->alterResult($data, $input); $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); From 680da18768528fa51cae56fe733f53201db9b7e9 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 18 Oct 2024 17:30:18 -0400 Subject: [PATCH 28/48] Start restore of automatic mention of the formatting help topic Use a web URL instead of the actual topic text. --- src/Application.php | 8 ++++++++ src/Attributes/FilterDefaultField.php | 2 +- src/Commands/core/TwigUnusedCommand.php | 2 +- src/Commands/sql/SqlDumpCommand.php | 6 +++--- src/Formatters/FormatterTrait.php | 14 +++++++++++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Application.php b/src/Application.php index 1a543fadeb..9f2e295a8a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -366,4 +366,12 @@ public function remove(string $id): void unset($commands[$id]); $rf->setValue($this, $commands); } + + /** + * A base URL for help. + */ + public function getDocsBaseUrl(): string + { + return 'https://www.drush.org/latest'; + } } diff --git a/src/Attributes/FilterDefaultField.php b/src/Attributes/FilterDefaultField.php index 70d2749cc0..52096a21ff 100644 --- a/src/Attributes/FilterDefaultField.php +++ b/src/Attributes/FilterDefaultField.php @@ -6,7 +6,7 @@ use Attribute; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class FilterDefaultField extends \Consolidation\AnnotatedCommand\Attributes\FilterDefaultField { } diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 8fc93c39d4..a82c45709b 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -52,7 +52,7 @@ protected function configure(): void // Usages can't have a description with plain Console :(. Use setHelp() if desired as per https://github.com/symfony/symfony/issues/45050 ->addUsage('twig:unused /var/www/mass.local/docroot/modules/custom,/var/www/mass.local/docroot/themes/custom') ->setHelp('Immediately before running this command, web crawl your entire web site. Or use your Production PHPStorage dir for comparison.'); - $this->addFormatterOptions(RowsOfFields::class); + $this->configureFormatter(RowsOfFields::class); } public function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 431aae2d28..985d6c472d 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -44,6 +44,7 @@ protected function configure() { $this ->addOption('result-file', null, InputOption::VALUE_REQUIRED, 'Save to a file. The file should be relative to Drupal root. If --result-file is provided with the value \'auto\', a date-based filename will be created under ~/drush-backups directory.') + // create-db is used by sql:sync, since including the DROP TABLE statements interferes with the import when the database is created. ->addOption('create-db', null, InputOption::VALUE_NONE, 'Omit DROP TABLE statements. Used by Postgres and Oracle only.') ->addOption('data-only', null, InputOption::VALUE_NONE, 'Dump data without statements to create any of the schema.') ->addOption('ordered-dump', null, InputOption::VALUE_NONE, 'Order by primary key and add line breaks for efficient diffs. Slows down the dump. Mysql only.') @@ -52,9 +53,8 @@ protected function configure() ->addOption('extra-dump', null, InputOption::VALUE_REQUIRED, 'Add custom arguments/options to the dumping of the database (e.g. mysqldump command).') ->addUsage('sql:dump --result-file=../18.sql') ->addUsage('sql:dump --skip-tables-key=common') - ->addUsage('sql:dump --extra-dump=--no-data') - ->setHelp('--create-db is used by sql:sync, since including the DROP TABLE statements interferes with the import when the database is created.'); - $this->addFormatterOptions(PropertyList::class); + ->addUsage('sql:dump --extra-dump=--no-data'); + $this->configureFormatter(PropertyList::class); OptionSets::sql($this); } diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index 288eb5083d..cc76621b29 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -5,6 +5,7 @@ use Consolidation\Filter\FilterOutputData; use Consolidation\Filter\LogicalOpFactory; use Drush\Attributes\FilterDefaultField; +use Drush\Drush; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -16,7 +17,7 @@ trait FormatterTrait * @param string $dataType * Usually the same as the return type of a doExecute() method. */ - public function addFormatterOptions(string $dataType): void + public function configureFormatter(string $dataType): void { $inputOptions = $this->formatterManager->automaticOptions($this->getFormatterOptions(), $dataType); foreach ($inputOptions as $inputOption) { @@ -25,6 +26,17 @@ public function addFormatterOptions(string $dataType): void $this->addOption($inputOption->getName(), $inputOption->getShortcut(), $mode, $inputOption->getDescription(), $inputOption->getDefault(), $suggestedValues); } + // Append a web link to the command's help. + // @todo $this->getApplication() throws an Exception - we are called during __construct(). Get base URL from the Container? + $application = Drush::getApplication(); + if (method_exists($application, 'getDocsBaseUrl')) { + $url = sprintf('%s/output-formats-filters', $application->getDocsBaseUrl()); + $section = sprintf('Learn more about about output formatting and filtering at %s', $url); + $help = $this->getHelp(); + $help .= "\n\n" . $section; + $this->setHelp($help); + } + // Add the --filter option if the command has a FilterDefaultField attribute. $reflection = new \ReflectionObject($this); $attributes = $reflection->getAttributes(FilterDefaultField::class); From 12e74f8d41a53aa36ddfb3c172ead992e4fe4473 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 19 Oct 2024 22:29:25 -0400 Subject: [PATCH 29/48] Pass formattterOptions during configure() --- src/Commands/core/TwigUnusedCommand.php | 2 +- src/Commands/sql/SqlDumpCommand.php | 2 +- src/Formatters/FormatterTrait.php | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index a82c45709b..2faa271272 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -52,7 +52,7 @@ protected function configure(): void // Usages can't have a description with plain Console :(. Use setHelp() if desired as per https://github.com/symfony/symfony/issues/45050 ->addUsage('twig:unused /var/www/mass.local/docroot/modules/custom,/var/www/mass.local/docroot/themes/custom') ->setHelp('Immediately before running this command, web crawl your entire web site. Or use your Production PHPStorage dir for comparison.'); - $this->configureFormatter(RowsOfFields::class); + $this->configureFormatter(RowsOfFields::class, $this->getFormatterOptions()); } public function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 985d6c472d..4db4a8d0e6 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -54,7 +54,7 @@ protected function configure() ->addUsage('sql:dump --result-file=../18.sql') ->addUsage('sql:dump --skip-tables-key=common') ->addUsage('sql:dump --extra-dump=--no-data'); - $this->configureFormatter(PropertyList::class); + $this->configureFormatter(PropertyList::class, $this->getFormatterOptions()); OptionSets::sql($this); } diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index cc76621b29..efaf6dc749 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -4,6 +4,7 @@ use Consolidation\Filter\FilterOutputData; use Consolidation\Filter\LogicalOpFactory; +use Consolidation\OutputFormatters\Options\FormatterOptions; use Drush\Attributes\FilterDefaultField; use Drush\Drush; use Symfony\Component\Console\Input\InputInterface; @@ -12,14 +13,18 @@ trait FormatterTrait { /** - * Adds options to the command definition based on data type. The --format description is dynamic. + * Add to the command definition based on data type. + * - The --format description is dynamic. + * - Add a link in help() * * @param string $dataType * Usually the same as the return type of a doExecute() method. + * @param FormatterOptions $formatterOptions + * The formatter options for this command. */ - public function configureFormatter(string $dataType): void + public function configureFormatter(string $dataType, FormatterOptions $formatterOptions): void { - $inputOptions = $this->formatterManager->automaticOptions($this->getFormatterOptions(), $dataType); + $inputOptions = $this->formatterManager->automaticOptions($formatterOptions, $dataType); foreach ($inputOptions as $inputOption) { $mode = $this->getPrivatePropValue($inputOption, 'mode'); $suggestedValues = $this->getPrivatePropValue($inputOption, 'suggestedValues'); From 74becdc94f98d3a5b6d620ff92b69da622daffed Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 19 Oct 2024 22:58:05 -0400 Subject: [PATCH 30/48] Simplify ImageFlushCommands a bit --- src/Commands/core/ImageFlushCommand.php | 9 ++------- src/Formatters/FormatterTrait.php | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index e3556b879c..4f5ea18e5e 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -5,7 +5,6 @@ namespace Drush\Commands\core; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\image\Entity\ImageStyle; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; use Drush\Commands\Validators; @@ -65,14 +64,10 @@ protected function interact(InputInterface $input, OutputInterface $output): voi public function execute(InputInterface $input, OutputInterface $output): int { - if ($names = $input->getArgument('style_names')) { - Validators::entityLoad(StringUtils::csvToArray($names), 'image_style'); - } - - // Needed for non-interactive requests. if ($input->getOption('all')) { - $input->setArgument('style_names', implode(',', array_keys(ImageStyle::loadMultiple()))); + $input->setArgument('style_names', array_keys($this->entityTypeManager->getStorage('image_style')->loadMultiple())); } + Validators::entityLoad(StringUtils::csvToArray($input->getArgument('style_names')), 'image_style'); $ids = StringUtils::csvToArray($input->getArgument('style_names')); foreach ($this->entityTypeManager->getStorage('image_style')->loadMultiple($ids) as $style_name => $style) { diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index efaf6dc749..3c5576bdad 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -43,6 +43,7 @@ public function configureFormatter(string $dataType, FormatterOptions $formatter } // Add the --filter option if the command has a FilterDefaultField attribute. + // @todo Determine --filter via $formatterOptions instead of FilterDefaultField attribute. $reflection = new \ReflectionObject($this); $attributes = $reflection->getAttributes(FilterDefaultField::class); if (!empty($attributes)) { From 6c899f44f61932398d6cc904b502c6469eef467a Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sun, 20 Oct 2024 21:53:14 -0400 Subject: [PATCH 31/48] Start deprecating a few Attribute classes --- src/Attributes/DefaultFields.php | 2 ++ src/Attributes/DefaultTableFields.php | 2 ++ src/Attributes/FieldLabels.php | 2 ++ src/Attributes/OptionsetSql.php | 2 ++ src/Attributes/ValidateEntityLoad.php | 2 ++ src/Commands/OptionSets.php | 2 +- 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Attributes/DefaultFields.php b/src/Attributes/DefaultFields.php index 58b0e02a9f..91da4866d7 100644 --- a/src/Attributes/DefaultFields.php +++ b/src/Attributes/DefaultFields.php @@ -5,7 +5,9 @@ namespace Drush\Attributes; use Attribute; +use JetBrains\PhpStorm\Deprecated; +#[Deprecated(replacement: 'Call \Drush\Formatters\FormatterTrait::configureFormatter during configure()')] #[Attribute(Attribute::TARGET_METHOD)] class DefaultFields extends \Consolidation\AnnotatedCommand\Attributes\DefaultFields { diff --git a/src/Attributes/DefaultTableFields.php b/src/Attributes/DefaultTableFields.php index 7e340474bf..ae32282dfa 100644 --- a/src/Attributes/DefaultTableFields.php +++ b/src/Attributes/DefaultTableFields.php @@ -5,7 +5,9 @@ namespace Drush\Attributes; use Attribute; +use JetBrains\PhpStorm\Deprecated; +#[Deprecated(replacement: 'Call \Drush\Formatters\FormatterTrait::configureFormatter during configure()')] #[Attribute(Attribute::TARGET_METHOD)] class DefaultTableFields extends \Consolidation\AnnotatedCommand\Attributes\DefaultTableFields { diff --git a/src/Attributes/FieldLabels.php b/src/Attributes/FieldLabels.php index 6e7a26a1b2..4ecc39d7dc 100644 --- a/src/Attributes/FieldLabels.php +++ b/src/Attributes/FieldLabels.php @@ -5,7 +5,9 @@ namespace Drush\Attributes; use Attribute; +use JetBrains\PhpStorm\Deprecated; +#[Deprecated(replacement: 'Call \Drush\Formatters\FormatterTrait::configureFormatter during configure()')] #[Attribute(Attribute::TARGET_METHOD)] class FieldLabels extends \Consolidation\AnnotatedCommand\Attributes\FieldLabels { diff --git a/src/Attributes/OptionsetSql.php b/src/Attributes/OptionsetSql.php index 02e19631f7..2f26a0755f 100644 --- a/src/Attributes/OptionsetSql.php +++ b/src/Attributes/OptionsetSql.php @@ -7,7 +7,9 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drush\Commands\DrushCommands; +use JetBrains\PhpStorm\Deprecated; +#[Deprecated(replacement: 'Directly add options by calling \Drush\Commands\OptionSets::sql during configure()')] #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class OptionsetSql { diff --git a/src/Attributes/ValidateEntityLoad.php b/src/Attributes/ValidateEntityLoad.php index a210b35518..92c5a9ea0b 100644 --- a/src/Attributes/ValidateEntityLoad.php +++ b/src/Attributes/ValidateEntityLoad.php @@ -8,7 +8,9 @@ use Consolidation\AnnotatedCommand\CommandData; use Consolidation\AnnotatedCommand\CommandError; use Drush\Utils\StringUtils; +use JetBrains\PhpStorm\Deprecated; +#[Deprecated(replacement: 'Directly call \Drush\Commands\Validators::entityLoad during execute()')] #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class ValidateEntityLoad extends ValidatorBase implements ValidatorInterface { diff --git a/src/Commands/OptionSets.php b/src/Commands/OptionSets.php index 9ff331d657..94cf8168b2 100644 --- a/src/Commands/OptionSets.php +++ b/src/Commands/OptionSets.php @@ -7,7 +7,7 @@ class OptionSets { - public static function sql(Command $command) + public static function sql(Command $command): void { $command->addOption('database', '', InputOption::VALUE_REQUIRED, 'The DB connection key if using multiple connections in settings.php.', 'default'); $command->addOption('db-url', '', InputOption::VALUE_REQUIRED, 'A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbname'); From 74b697cfea12c00581bfc0731fff5450ca4461b2 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 23 Oct 2024 21:52:46 -0400 Subject: [PATCH 32/48] Minor improvements --- src/Attributes/Topics.php | 2 +- src/Commands/core/ImageFlushCommand.php | 3 ++- src/Runtime/DependencyInjection.php | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Attributes/Topics.php b/src/Attributes/Topics.php index 9f974ba950..1aac0a0f69 100644 --- a/src/Attributes/Topics.php +++ b/src/Attributes/Topics.php @@ -6,7 +6,7 @@ use Attribute; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class Topics extends \Consolidation\AnnotatedCommand\Attributes\Topics { } diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 4f5ea18e5e..0115ff46b4 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -64,6 +64,8 @@ protected function interact(InputInterface $input, OutputInterface $output): voi public function execute(InputInterface $input, OutputInterface $output): int { + $io = new DrushStyle($input, $output); + if ($input->getOption('all')) { $input->setArgument('style_names', array_keys($this->entityTypeManager->getStorage('image_style')->loadMultiple())); } @@ -72,7 +74,6 @@ public function execute(InputInterface $input, OutputInterface $output): int $ids = StringUtils::csvToArray($input->getArgument('style_names')); foreach ($this->entityTypeManager->getStorage('image_style')->loadMultiple($ids) as $style_name => $style) { $style->flush(); - $io = new DrushStyle($input, $output); $io->success("Image style $style_name flushed"); } return static::SUCCESS; diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index ba9e6f22d4..9c902bc0b4 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -27,6 +27,7 @@ use Drush\Symfony\DrushStyleInjector; use League\Container\Container; use League\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Robo\Robo; use Symfony\Component\Console\Input\InputInterface; @@ -166,6 +167,10 @@ protected function alterServicesForDrush($container, Application $application, I $paramInjection = $container->get('parameterInjection'); $paramInjection->register('Symfony\Component\Console\Style\SymfonyStyle', new DrushStyleInjector()); + // Alias the dispatcher service that is defined in \Robo\Robo::configureContainer. + Robo::addShared($container, EventDispatcherInterface::class, 'eventDispatcher'); // For autowiring + + // Add our own callback to the hook manager $hookManager = $container->get('hookManager'); $hookManager->addCommandEvent(new GlobalOptionsEventListener()); From 5b55f98950a299a74b0632ebc7deaa881797e8ca Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 23 Oct 2024 21:55:01 -0400 Subject: [PATCH 33/48] Convert sql:sanitize command. Convert the UserTable sanitizer to a Listener --- src/Commands/sql/sanitize/SanitizeCommand.php | 88 ++++++++++++++++++ .../sql/sanitize/SanitizeCommands.php | 54 +---------- .../sql/sanitize/SanitizePluginInterface.php | 2 + src/Event/SanitizeConfirmsEvent.php | 48 ++++++++++ .../sanitize/SanitizeUserTableListener.php} | 93 ++++++++++--------- src/Runtime/ServiceManager.php | 9 +- 6 files changed, 198 insertions(+), 96 deletions(-) create mode 100644 src/Commands/sql/sanitize/SanitizeCommand.php create mode 100644 src/Event/SanitizeConfirmsEvent.php rename src/{Commands/sql/sanitize/SanitizeUserTableCommands.php => Listeners/sanitize/SanitizeUserTableListener.php} (61%) diff --git a/src/Commands/sql/sanitize/SanitizeCommand.php b/src/Commands/sql/sanitize/SanitizeCommand.php new file mode 100644 index 0000000000..7c3c661298 --- /dev/null +++ b/src/Commands/sql/sanitize/SanitizeCommand.php @@ -0,0 +1,88 @@ +setDescription('Sanitize the database by removing or obfuscating user data.') + ->addUsage('drush sql:sanitize --sanitize-password=no') + ->addUsage('drush sql:sanitize --allowlist-fields=field_biography,field_phone_number'); + } + + /** + * Commandfiles may add custom operations by implementing a Listener that subscribes to two events: + * + * - `\Drush\Events\SanitizeConfirmsEvent`. Display summary to user before confirmation. + * - `\Symfony\Component\Console\Event\ConsoleTerminateEvent`. Run queries or call APIs to perform sanitizing + * + * Several working Listeners may be found at https://github.com/drush-ops/drush/tree/13.x/src/Drush/Listeners/sanitize + */ + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new DrushStyle($input, $output); + + /** + * In order to present only one prompt, collect all confirmations up front. + */ + $event = new SanitizeConfirmsEvent($input); + $this->eventDispatcher->dispatch($event, SanitizeConfirmsEvent::class); + $messages = $event->getMessages(); + + // Also collect from legacy commandfiles. + $handlers = $this->getCustomEventHandlers(SanitizeCommands::CONFIRMS); + foreach ($handlers as $handler) { + $handler($messages, $input); + } + // @phpstan-ignore if.alwaysFalse + if ($messages) { + $output->writeln(dt('The following operations will be performed:')); + $io->listing($messages); + } + if (!$io->confirm(dt('Do you want to sanitize the current database?'))) { + throw new UserAbortException(); + } + // All sanitize operations happen during the built-in console.terminate event. + + return self::SUCCESS; + } +} diff --git a/src/Commands/sql/sanitize/SanitizeCommands.php b/src/Commands/sql/sanitize/SanitizeCommands.php index 79633a40e3..18120f1ee7 100644 --- a/src/Commands/sql/sanitize/SanitizeCommands.php +++ b/src/Commands/sql/sanitize/SanitizeCommands.php @@ -4,59 +4,11 @@ namespace Drush\Commands\sql\sanitize; -use Consolidation\AnnotatedCommand\Events\CustomEventAwareInterface; -use Consolidation\AnnotatedCommand\Events\CustomEventAwareTrait; -use Drush\Attributes as CLI; -use Drush\Boot\DrupalBootLevels; -use Drush\Commands\core\DocsCommands; -use Drush\Commands\DrushCommands; -use Drush\Exceptions\UserAbortException; +use JetBrains\PhpStorm\Deprecated; -#[CLI\Bootstrap(level: DrupalBootLevels::FULL)] -final class SanitizeCommands extends DrushCommands implements CustomEventAwareInterface +#[Deprecated('Moved to Drush\Commands\sql\sanitize\SanitizeCommand.')] +final class SanitizeCommands { - use CustomEventAwareTrait; - const SANITIZE = 'sql:sanitize'; const CONFIRMS = 'sql-sanitize-confirms'; - - /** - * Sanitize the database by removing or obfuscating user data. - * - * Commandfiles may add custom operations by implementing: - * - * - `#[CLI\Hook(type: HookManager::ON_EVENT, target: SanitizeCommands::CONFIRMS)]`. Display summary to user before confirmation. - * - `#[CLI\Hook(type: HookManager::POST_COMMAND_HOOK, target: SanitizeCommands::SANITIZE)]`. Run queries or call APIs to perform sanitizing - * - * Several working commandfiles may be found at https://github.com/drush-ops/drush/tree/13.x/src/Commands/sql/sanitize - */ - #[CLI\Command(name: self::SANITIZE, aliases: ['sqlsan','sql-sanitize'])] - #[CLI\Usage(name: 'drush sql:sanitize --sanitize-password=no', description: 'Sanitize database without modifying any passwords.')] - #[CLI\Usage(name: 'drush sql:sanitize --allowlist-fields=field_biography,field_phone_number', description: 'Sanitizes database but exempts two user fields from modification.')] - #[CLI\Topics(topics: [DocsCommands::HOOKS])] - public function sanitize(): void - { - /** - * In order to present only one prompt, collect all confirmations from - * commandfiles up front. sql:sanitize plugins are commandfiles that implement - * \Drush\Commands\sql\SanitizePluginInterface - */ - $messages = []; - $input = $this->input(); - $handlers = $this->getCustomEventHandlers(self::CONFIRMS); - foreach ($handlers as $handler) { - $handler($messages, $input); - } - // @phpstan-ignore if.alwaysFalse - if ($messages) { - $this->output()->writeln(dt('The following operations will be performed:')); - $this->io()->listing($messages); - } - if (!$this->io()->confirm(dt('Do you want to sanitize the current database?'))) { - throw new UserAbortException(); - } - - // All sanitize operations defined in post-command hooks, including Drush - // core sanitize routines. See \Drush\Commands\sql\sanitize\SanitizePluginInterface. - } } diff --git a/src/Commands/sql/sanitize/SanitizePluginInterface.php b/src/Commands/sql/sanitize/SanitizePluginInterface.php index a125f65a71..53bf8e4ee8 100644 --- a/src/Commands/sql/sanitize/SanitizePluginInterface.php +++ b/src/Commands/sql/sanitize/SanitizePluginInterface.php @@ -5,11 +5,13 @@ namespace Drush\Commands\sql\sanitize; use Consolidation\AnnotatedCommand\CommandData; +use JetBrains\PhpStorm\Deprecated; use Symfony\Component\Console\Input\InputInterface; /** * Implement this interface when building a Drush sql-sanitize plugin. */ +#[Deprecated(reason: 'Implement an event listener instead.')] interface SanitizePluginInterface { /** diff --git a/src/Event/SanitizeConfirmsEvent.php b/src/Event/SanitizeConfirmsEvent.php new file mode 100644 index 0000000000..3c34542062 --- /dev/null +++ b/src/Event/SanitizeConfirmsEvent.php @@ -0,0 +1,48 @@ +input = $input; + } + + public function getInput(): InputInterface + { + return $this->input; + } + + public function addMessage(string $message): self + { + $this->messages[] = $message; + return $this; + } + + public function getMessages(): array + { + return $this->messages; + } + + public function setMessages(array $messages): self + { + $this->messages = $messages; + return $this; + } +} diff --git a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php b/src/Listeners/sanitize/SanitizeUserTableListener.php similarity index 61% rename from src/Commands/sql/sanitize/SanitizeUserTableCommands.php rename to src/Listeners/sanitize/SanitizeUserTableListener.php index bd2a18fef4..e1203143a6 100644 --- a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php +++ b/src/Listeners/sanitize/SanitizeUserTableListener.php @@ -2,44 +2,76 @@ declare(strict_types=1); -namespace Drush\Commands\sql\sanitize; +namespace Drush\Listeners\sanitize; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Query\SelectInterface; -use Consolidation\AnnotatedCommand\CommandData; -use Consolidation\AnnotatedCommand\Hooks\HookManager; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Password\PasswordInterface; -use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; -use Drush\Commands\DrushCommands; +use Drush\Commands\sql\sanitize\SanitizeCommand; +use Drush\Event\ConsoleDefinitionsEvent; +use Drush\Event\SanitizeConfirmsEvent; use Drush\Sql\SqlBase; use Drush\Utils\StringUtils; -use Symfony\Component\Console\Input\InputInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** - * A sql:sanitize plugin. + * Sanitize emails and passwords. This also an example of how to write a + * database sanitizer for sql:sync. */ -final class SanitizeUserTableCommands extends DrushCommands implements SanitizePluginInterface +#[AsEventListener(method: 'onDefinition')] +#[AsEventListener(method: 'onSanitizeConfirm')] +#[AsEventListener(method: 'onConsoleTerminate')] +final class SanitizeUserTableListener { use AutowireTrait; public function __construct( protected Connection $database, protected PasswordInterface $passwordHasher, - protected EntityTypeManagerInterface $entityTypeManager + protected EntityTypeManagerInterface $entityTypeManager, + protected LoggerInterface $logger, ) { - parent::__construct(); } - /** - * Sanitize emails and passwords. This also an example of how to write a - * database sanitizer for sql:sync. - */ - #[CLI\Hook(type: HookManager::POST_COMMAND_HOOK, target: SanitizeCommands::SANITIZE)] - public function sanitize($result, CommandData $commandData): void + public function onDefinition(ConsoleDefinitionsEvent $event): void + { + foreach ($event->getApplication()->all() as $id => $command) { + if ($command->getName() === SanitizeCommand::NAME) { + $command->addOption( + 'sanitize-email', + null, + InputOption::VALUE_REQUIRED, + 'The pattern for test email addresses in the sanitization operation, or no to keep email addresses unchanged. May contain replacement patterns %uid, %mail or %name.', + 'user+%uid@localhost.localdomain' + ) + ->addOption('sanitize-password', null, InputOption::VALUE_REQUIRED, 'By default, passwords are randomized. Specify no to disable that. Specify any other value to set all passwords to that value.') + ->addOption('ignored-roles', null, InputOption::VALUE_REQUIRED, 'A comma delimited list of roles. Users with at least one of the roles will be exempt from sanitization.'); + } + } + } + + public function onSanitizeConfirm(SanitizeConfirmsEvent $event): void + { + $options = $event->getInput()->getOptions(); + if ($this->isEnabled($options['sanitize-password'])) { + $event->addMessage(dt('Sanitize user passwords.')); + } + if ($this->isEnabled($options['sanitize-email'])) { + $event->addMessage(dt('Sanitize user emails.')); + } + if (in_array('ignored-roles', $options)) { + $event->addMessage(dt('Preserve user emails and passwords for the specified roles.')); + } + } + + public function onConsoleTerminate(ConsoleTerminateEvent $event): void { - $options = $commandData->options(); + $options = $event->getInput()->getOptions(); $query = $this->database->update('users_field_data')->condition('uid', 0, '>'); $messages = []; @@ -60,7 +92,7 @@ public function sanitize($result, CommandData $commandData): void if ($this->isEnabled($options['sanitize-email'])) { if (str_contains($options['sanitize-email'], '%')) { // We need a different sanitization query for MSSQL, Postgres and Mysql. - $sql = SqlBase::create($commandData->input()->getOptions()); + $sql = SqlBase::create($event->getInput()->getOptions()); $db_driver = $sql->scheme(); if ($db_driver === 'pgsql') { $email_map = ['%uid' => "' || uid || '", '%mail' => "' || replace(mail, '@', '_') || '", '%name' => "' || replace(name, ' ', '_') || '"]; @@ -100,34 +132,11 @@ public function sanitize($result, CommandData $commandData): void $query->execute(); $this->entityTypeManager->getStorage('user')->resetCache(); foreach ($messages as $message) { - $this->logger()->success($message); + $this->logger->success($message); } } } - #[CLI\Hook(type: HookManager::OPTION_HOOK, target: SanitizeCommands::SANITIZE)] - #[CLI\Option(name: 'sanitize-email', description: 'The pattern for test email addresses in the sanitization operation, or no to keep email addresses unchanged. May contain replacement patterns %uid, %mail or %name.')] - #[CLI\Option(name: 'sanitize-password', description: 'By default, passwords are randomized. Specify no to disable that. Specify any other value to set all passwords to that value.')] - #[CLI\Option(name: 'ignored-roles', description: 'A comma delimited list of roles. Users with at least one of the roles will be exempt from sanitization.')] - public function options($options = ['sanitize-email' => 'user+%uid@localhost.localdomain', 'sanitize-password' => null, 'ignored-roles' => null]): void - { - } - - #[CLI\Hook(type: HookManager::ON_EVENT, target: SanitizeCommands::CONFIRMS)] - public function messages(&$messages, InputInterface $input): void - { - $options = $input->getOptions(); - if ($this->isEnabled($options['sanitize-password'])) { - $messages[] = dt('Sanitize user passwords.'); - } - if ($this->isEnabled($options['sanitize-email'])) { - $messages[] = dt('Sanitize user emails.'); - } - if (in_array('ignored-roles', $options)) { - $messages[] = dt('Preserve user emails and passwords for the specified roles.'); - } - } - /** * Test an option value to see if it is disabled. */ diff --git a/src/Runtime/ServiceManager.php b/src/Runtime/ServiceManager.php index 772790422e..80e5c0b81b 100644 --- a/src/Runtime/ServiceManager.php +++ b/src/Runtime/ServiceManager.php @@ -34,6 +34,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Input\InputAwareInterface; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; @@ -438,9 +439,11 @@ public function addListeners(iterable $classes, ContainerInterface $drushContain } else { throw new \Exception('Event listener method must have a single parameter with a type hint.'); } - if ($eventName == ConsoleCommandEvent::class) { - $eventName = ConsoleEvents::COMMAND; - } + $eventName = match ($eventName) { + ConsoleCommandEvent::class => ConsoleEvents::COMMAND, + ConsoleTerminateEvent::class => ConsoleEvents::TERMINATE, + default => $eventName, + }; Drush::getContainer()->get('eventDispatcher')->addListener($eventName, $instance->$method(...), $priority); } } From ebd367a69bebb9bd96dea0d9fc112c5e98d1eeb8 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 23 Oct 2024 22:52:20 -0400 Subject: [PATCH 34/48] PHPStan fixes --- src/Commands/sql/sanitize/SanitizeCommand.php | 12 ++++++------ src/Listeners/sanitize/SanitizeUserTableListener.php | 11 ++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Commands/sql/sanitize/SanitizeCommand.php b/src/Commands/sql/sanitize/SanitizeCommand.php index 7c3c661298..98043080db 100644 --- a/src/Commands/sql/sanitize/SanitizeCommand.php +++ b/src/Commands/sql/sanitize/SanitizeCommand.php @@ -65,15 +65,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int * In order to present only one prompt, collect all confirmations up front. */ $event = new SanitizeConfirmsEvent($input); - $this->eventDispatcher->dispatch($event, SanitizeConfirmsEvent::class); + $this->eventDispatcher->dispatch($event); $messages = $event->getMessages(); // Also collect from legacy commandfiles. - $handlers = $this->getCustomEventHandlers(SanitizeCommands::CONFIRMS); - foreach ($handlers as $handler) { - $handler($messages, $input); - } - // @phpstan-ignore if.alwaysFalse + // This works but we would need backwars compat forv POST_COMMAND AC hook as well. +// $handlers = $this->getCustomEventHandlers(SanitizeCommands::CONFIRMS); +// foreach ($handlers as $handler) { +// $handler($messages, $input); +// } if ($messages) { $output->writeln(dt('The following operations will be performed:')); $io->listing($messages); diff --git a/src/Listeners/sanitize/SanitizeUserTableListener.php b/src/Listeners/sanitize/SanitizeUserTableListener.php index e1203143a6..c4b8a9cd38 100644 --- a/src/Listeners/sanitize/SanitizeUserTableListener.php +++ b/src/Listeners/sanitize/SanitizeUserTableListener.php @@ -13,8 +13,8 @@ use Drush\Event\ConsoleDefinitionsEvent; use Drush\Event\SanitizeConfirmsEvent; use Drush\Sql\SqlBase; +use Drush\Style\DrushStyle; use Drush\Utils\StringUtils; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; @@ -34,7 +34,6 @@ public function __construct( protected Connection $database, protected PasswordInterface $passwordHasher, protected EntityTypeManagerInterface $entityTypeManager, - protected LoggerInterface $logger, ) { } @@ -71,6 +70,12 @@ public function onSanitizeConfirm(SanitizeConfirmsEvent $event): void public function onConsoleTerminate(ConsoleTerminateEvent $event): void { + if ($event->getCommand()->getName() !== SanitizeCommand::NAME) { + return; + } + + $io = new DrushStyle($event->getInput(), $event->getOutput()); + $options = $event->getInput()->getOptions(); $query = $this->database->update('users_field_data')->condition('uid', 0, '>'); $messages = []; @@ -132,7 +137,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event): void $query->execute(); $this->entityTypeManager->getStorage('user')->resetCache(); foreach ($messages as $message) { - $this->logger->success($message); + $io->success($message); } } } From 34afdc75562bff5e3730853ffb778cf28d40c9fa Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 24 Oct 2024 08:01:44 -0400 Subject: [PATCH 35/48] Convert to UserTableFields listener. Needed for green --- .../sanitize/SanitizeUserFieldsCommands.php | 123 ------------------ .../sanitize/SanitizeUserFieldsListener.php | 118 +++++++++++++++++ .../sanitize/SanitizeUserTableListener.php | 2 +- tests/functional/SqlSyncTest.php | 3 +- 4 files changed, 121 insertions(+), 125 deletions(-) delete mode 100644 src/Commands/sql/sanitize/SanitizeUserFieldsCommands.php create mode 100644 src/Listeners/sanitize/SanitizeUserFieldsListener.php diff --git a/src/Commands/sql/sanitize/SanitizeUserFieldsCommands.php b/src/Commands/sql/sanitize/SanitizeUserFieldsCommands.php deleted file mode 100644 index b858e62154..0000000000 --- a/src/Commands/sql/sanitize/SanitizeUserFieldsCommands.php +++ /dev/null @@ -1,123 +0,0 @@ -database; - } - - /** - * @return mixed - */ - public function getEntityFieldManager() - { - return $this->entityFieldManager; - } - - /** - * Sanitize string fields associated with the user. - */ - #[CLI\Hook(type: HookManager::POST_COMMAND_HOOK, target: SanitizeCommands::SANITIZE)] - public function sanitize($result, CommandData $commandData): void - { - $options = $commandData->options(); - $conn = $this->getDatabase(); - $field_definitions = $this->getEntityFieldManager()->getFieldDefinitions('user', 'user'); - $field_storage = $this->getEntityFieldManager()->getFieldStorageDefinitions('user'); - foreach (explode(',', $options['allowlist-fields']) as $key) { - unset($field_definitions[$key], $field_storage[$key]); - } - - foreach ($field_definitions as $key => $def) { - $execute = false; - if (!isset($field_storage[$key]) || $field_storage[$key]->isBaseField()) { - continue; - } - - $table = 'user__' . $key; - $query = $conn->update($table); - $name = $def->getName(); - $field_type_class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($def->getType()); - $supported_field_types = ['email', 'string', 'string_long', 'telephone', 'text', 'text_long', 'text_with_summary']; - if (in_array($def->getType(), $supported_field_types)) { - $value_array = $field_type_class::generateSampleValue($def); - $value = $value_array['value']; - } else { - continue; - } - switch ($def->getType()) { - case 'string': - case 'string_long': - case 'text': - case 'text_long': - case 'email': - $query->fields([$name . '_value' => $value]); - $execute = true; - break; - - case 'telephone': - $query->fields([$name . '_value' => '15555555555']); - $execute = true; - break; - - case 'text_with_summary': - $query->fields([ - $name . '_value' => $value, - $name . '_summary' => $value_array['summary'], - ]); - $execute = true; - break; - } - if ($execute) { - $query->execute(); - $this->entityTypeManager->getStorage('user')->resetCache(); - $this->logger()->success(dt('!table table sanitized.', ['!table' => $table])); - } else { - $this->logger()->success(dt('No text fields for users need sanitizing.', ['!table' => $table])); - } - } - } - - #[CLI\Hook(type: HookManager::ON_EVENT, target: SanitizeCommands::CONFIRMS)] - public function messages(&$messages, InputInterface $input): void - { - $messages[] = dt('Sanitize text fields associated with users.'); - } - - #[CLI\Hook(type: HookManager::OPTION_HOOK, target: SanitizeCommands::SANITIZE)] - #[CLI\Option(name: 'allowlist-fields', description: 'A comma delimited list of fields exempt from sanitization.')] - public function options($options = ['allowlist-fields' => '']): void - { - } -} diff --git a/src/Listeners/sanitize/SanitizeUserFieldsListener.php b/src/Listeners/sanitize/SanitizeUserFieldsListener.php new file mode 100644 index 0000000000..fb54ea79e7 --- /dev/null +++ b/src/Listeners/sanitize/SanitizeUserFieldsListener.php @@ -0,0 +1,118 @@ +getApplication()->all() as $id => $command) { + if ($command->getName() === SanitizeCommand::NAME) { + $command->addOption('allowlist-fields', null, InputOption::VALUE_REQUIRED, 'A comma delimited list of fields exempt from sanitization.'); + } + } + } + + public function onSanitizeConfirm(SanitizeConfirmsEvent $event): void + { + $event->addMessage(dt('Sanitize text fields associated with users.')); + } + + public function onConsoleTerminate(ConsoleTerminateEvent $event): void + { + if ($event->getCommand()->getName() !== SanitizeCommand::NAME) { + return; + } + + $io = new DrushStyle($event->getInput(), $event->getOutput()); + + $options = $event->getInput()->getOptions(); + $field_definitions = $this->entityFieldManager->getFieldDefinitions('user', 'user'); + $field_storage = $this->entityFieldManager->getFieldStorageDefinitions('user'); + foreach (StringUtils::csvToArray($options['allowlist-fields']) as $key) { + unset($field_definitions[$key], $field_storage[$key]); + } + + foreach ($field_definitions as $key => $def) { + $execute = false; + if (!isset($field_storage[$key]) || $field_storage[$key]->isBaseField()) { + continue; + } + + $table = 'user__' . $key; + $query = $this->database->update($table); + $name = $def->getName(); + $field_type_class = $this->fieldTypePluginManager->getPluginClass($def->getType()); + $supported_field_types = ['email', 'string', 'string_long', 'telephone', 'text', 'text_long', 'text_with_summary']; + if (in_array($def->getType(), $supported_field_types)) { + $value_array = $field_type_class::generateSampleValue($def); + $value = $value_array['value']; + } else { + continue; + } + switch ($def->getType()) { + case 'string': + case 'string_long': + case 'text': + case 'text_long': + case 'email': + $query->fields([$name . '_value' => $value]); + $execute = true; + break; + + case 'telephone': + $query->fields([$name . '_value' => '15555555555']); + $execute = true; + break; + + case 'text_with_summary': + $query->fields([ + $name . '_value' => $value, + $name . '_summary' => $value_array['summary'], + ]); + $execute = true; + break; + } + if ($execute) { + $query->execute(); + $this->entityTypeManager->getStorage('user')->resetCache(); + $io->success(dt('!table table sanitized.', ['!table' => $table])); + } else { + $io->success(dt('No text fields for users need sanitizing.', ['!table' => $table])); + } + } + } +} diff --git a/src/Listeners/sanitize/SanitizeUserTableListener.php b/src/Listeners/sanitize/SanitizeUserTableListener.php index c4b8a9cd38..0e7fc3d626 100644 --- a/src/Listeners/sanitize/SanitizeUserTableListener.php +++ b/src/Listeners/sanitize/SanitizeUserTableListener.php @@ -73,7 +73,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event): void if ($event->getCommand()->getName() !== SanitizeCommand::NAME) { return; } - + $io = new DrushStyle($event->getInput(), $event->getOutput()); $options = $event->getInput()->getOptions(); diff --git a/tests/functional/SqlSyncTest.php b/tests/functional/SqlSyncTest.php index 8bb1377e3f..9e2c5d964f 100644 --- a/tests/functional/SqlSyncTest.php +++ b/tests/functional/SqlSyncTest.php @@ -7,6 +7,7 @@ use Drush\Commands\core\PhpCommands; use Drush\Commands\core\UserCommands; use Drush\Commands\pm\PmCommands; +use Drush\Commands\sql\sanitize\SanitizeCommand; use Drush\Commands\sql\sanitize\SanitizeCommands; use Drush\Commands\sql\SqlCommands; use Drush\Commands\sql\SqlSyncCommands; @@ -115,7 +116,7 @@ public function localSqlSync() 'structure-tables-list' => 'cache,cache*', ]; $this->drush(SqlSyncCommands::SYNC, ['@sut.stage', '@sut.dev'], $sync_options); - $this->drush(SanitizeCommands::SANITIZE, [], ['yes' => null, 'uri' => 'dev',], '@sut.dev'); + $this->drush(SanitizeCommand::NAME, [], ['yes' => null, 'uri' => 'dev',], '@sut.dev'); // Confirm that the sample user is unchanged on the staging site $this->drush(UserCommands::INFORMATION, [$name], $options + ['format' => 'json'], '@sut.stage'); From f923a801dd50d4aa17fd2390790336bc962785a3 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 24 Oct 2024 10:13:34 -0400 Subject: [PATCH 36/48] Introduce writeFormattedOutput() - use in execute() --- src/Commands/core/TwigUnusedCommand.php | 17 +++++------------ src/Commands/sql/SqlDumpCommand.php | 14 ++++---------- src/Formatters/FormatterTrait.php | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Commands/core/TwigUnusedCommand.php b/src/Commands/core/TwigUnusedCommand.php index 2faa271272..ad7981a787 100644 --- a/src/Commands/core/TwigUnusedCommand.php +++ b/src/Commands/core/TwigUnusedCommand.php @@ -52,15 +52,16 @@ protected function configure(): void // Usages can't have a description with plain Console :(. Use setHelp() if desired as per https://github.com/symfony/symfony/issues/45050 ->addUsage('twig:unused /var/www/mass.local/docroot/modules/custom,/var/www/mass.local/docroot/themes/custom') ->setHelp('Immediately before running this command, web crawl your entire web site. Or use your Production PHPStorage dir for comparison.'); - $this->configureFormatter(RowsOfFields::class, $this->getFormatterOptions()); + $formatterOptions = (new FormatterOptions()) + ->setFieldLabels(['template' => 'Template', 'compiled' => 'Compiled']) + ->setTableDefaultFields(['template', 'compiled']); + $this->configureFormatter(RowsOfFields::class, $formatterOptions); } public function execute(InputInterface $input, OutputInterface $output): int { - $formatterOptions = $this->getFormatterOptions()->setInput($input); $data = $this->doExecute($input, $output); - $data = $this->alterResult($data, $input); - $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); + $this->writeFormattedOutput($input, $output, $data); return self::SUCCESS; } @@ -98,12 +99,4 @@ public function doExecute(InputInterface $input, OutputInterface $output): RowsO $this->logger->notice('Found {count} unused', ['count' => count($unused)]); return new RowsOfFields($unused); } - - - protected function getFormatterOptions(): FormatterOptions - { - return (new FormatterOptions()) - ->setFieldLabels(['template' => 'Template', 'compiled' => 'Compiled']) - ->setTableDefaultFields(['template', 'compiled']); - } } diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 4db4a8d0e6..fe02593c0f 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -54,16 +54,16 @@ protected function configure() ->addUsage('sql:dump --result-file=../18.sql') ->addUsage('sql:dump --skip-tables-key=common') ->addUsage('sql:dump --extra-dump=--no-data'); - $this->configureFormatter(PropertyList::class, $this->getFormatterOptions()); + $formatterOptions = (new FormatterOptions()) + ->setFieldLabels(['path' => 'Path']); + $this->configureFormatter(PropertyList::class, $formatterOptions); OptionSets::sql($this); } public function execute(InputInterface $input, OutputInterface $output): int { - $formatterOptions = $this->getFormatterOptions()->setInput($input); $data = $this->doExecute($input, $output); - $data = $this->alterResult($data, $input); - $this->formatterManager->write($output, $input->getOption('format'), $data, $formatterOptions); + $this->writeFormattedOutput($input, $output, $data); return static::SUCCESS; } protected function doExecute(InputInterface $input, OutputInterface $output): PropertyList @@ -81,10 +81,4 @@ protected function doExecute(InputInterface $input, OutputInterface $output): Pr } return new PropertyList(['path' => $return]); } - - protected function getFormatterOptions(): FormatterOptions - { - return (new FormatterOptions()) - ->setFieldLabels(['path' => 'Path']); - } } diff --git a/src/Formatters/FormatterTrait.php b/src/Formatters/FormatterTrait.php index 3c5576bdad..3d0a5a0783 100644 --- a/src/Formatters/FormatterTrait.php +++ b/src/Formatters/FormatterTrait.php @@ -9,9 +9,12 @@ use Drush\Drush; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; trait FormatterTrait { + protected FormatterOptions $formatterOptions; + /** * Add to the command definition based on data type. * - The --format description is dynamic. @@ -24,6 +27,7 @@ trait FormatterTrait */ public function configureFormatter(string $dataType, FormatterOptions $formatterOptions): void { + $this->setFormatterOptions($formatterOptions); $inputOptions = $this->formatterManager->automaticOptions($formatterOptions, $dataType); foreach ($inputOptions as $inputOption) { $mode = $this->getPrivatePropValue($inputOption, 'mode'); @@ -52,6 +56,15 @@ public function configureFormatter(string $dataType, FormatterOptions $formatter } } + /** + * Filter, format, and write to the output + */ + protected function writeFormattedOutput(InputInterface $input, OutputInterface $output, $data): void + { + $data = $this->alterResult($data, $input); + $this->formatterManager->write($output, $input->getOption('format'), $data, $this->getFormatterOptions()->setInput($input)); + } + protected function alterResult($result, InputInterface $input): mixed { if (!$input->hasOption('filter') || !$input->getOption('filter')) { @@ -81,6 +94,16 @@ protected function wrapFilteredResult($data, $source) return new $sourceClass($data); } + public function getFormatterOptions(): FormatterOptions + { + return $this->formatterOptions; + } + + public function setFormatterOptions(FormatterOptions $formatterOptions): void + { + $this->formatterOptions = $formatterOptions; + } + protected function getPrivatePropValue(mixed $object, $name): mixed { $rc = new \ReflectionClass($object); From 2e759121b54acc2b1303df8e0c30ec76e37a4821 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 24 Oct 2024 23:06:17 -0400 Subject: [PATCH 37/48] Just put validators right into the Command Its OK to duplicate a few lines of code --- src/Attributes/ValidateEntityLoad.php | 4 +- src/Attributes/ValidateModulesEnabled.php | 4 +- src/Commands/Validators.php | 16 ------- src/Commands/core/ImageFlushCommand.php | 32 ++++++++++--- .../ValidateModulesEnabledListener.php | 45 ------------------- 5 files changed, 31 insertions(+), 70 deletions(-) delete mode 100644 src/Commands/Validators.php delete mode 100644 src/Listeners/ValidateModulesEnabledListener.php diff --git a/src/Attributes/ValidateEntityLoad.php b/src/Attributes/ValidateEntityLoad.php index 92c5a9ea0b..0e3346f0a4 100644 --- a/src/Attributes/ValidateEntityLoad.php +++ b/src/Attributes/ValidateEntityLoad.php @@ -10,8 +10,8 @@ use Drush\Utils\StringUtils; use JetBrains\PhpStorm\Deprecated; -#[Deprecated(replacement: 'Directly call \Drush\Commands\Validators::entityLoad during execute()')] -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +#[Deprecated(replacement: 'Copy \Drush\Commands\core\ImageFlushCommand::validateEntityLoad into command and call during execute()')] +#[Attribute(Attribute::TARGET_METHOD)] class ValidateEntityLoad extends ValidatorBase implements ValidatorInterface { /** diff --git a/src/Attributes/ValidateModulesEnabled.php b/src/Attributes/ValidateModulesEnabled.php index a9d5751e49..aaf44ff887 100644 --- a/src/Attributes/ValidateModulesEnabled.php +++ b/src/Attributes/ValidateModulesEnabled.php @@ -7,8 +7,10 @@ use Attribute; use Consolidation\AnnotatedCommand\CommandData; use Consolidation\AnnotatedCommand\CommandError; +use JetBrains\PhpStorm\Deprecated; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +#[Deprecated(replacement: 'Copy \Drush\Commands\core\ImageFlushCommand::validateModulesEnabled into command and call during execute()')] +#[Attribute(Attribute::TARGET_METHOD)] class ValidateModulesEnabled extends ValidatorBase implements ValidatorInterface { /** diff --git a/src/Commands/Validators.php b/src/Commands/Validators.php deleted file mode 100644 index 8eb175951f..0000000000 --- a/src/Commands/Validators.php +++ /dev/null @@ -1,16 +0,0 @@ -getStorage($entityType)->loadMultiple($ids); - if ($missing = array_diff($ids, array_keys($loaded))) { - $msg = dt('Unable to load the !type: !str', ['!type' => $entityType, '!str' => implode(', ', $missing)]); - throw new \Exception($msg); - } - } -} diff --git a/src/Commands/core/ImageFlushCommand.php b/src/Commands/core/ImageFlushCommand.php index 0115ff46b4..a05c7bd28d 100644 --- a/src/Commands/core/ImageFlushCommand.php +++ b/src/Commands/core/ImageFlushCommand.php @@ -5,11 +5,11 @@ namespace Drush\Commands\core; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drush\Attributes as CLI; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drush\Commands\AutowireTrait; -use Drush\Commands\Validators; use Drush\Style\DrushStyle; use Drush\Utils\StringUtils; +use InvalidArgumentException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -22,7 +22,6 @@ description: 'Flush all derived images for a given style.', aliases: ['if', 'image-flush'] )] -#[CLI\ValidateModulesEnabled(modules: ['image'])] final class ImageFlushCommand extends Command { use AutowireTrait; @@ -30,7 +29,8 @@ final class ImageFlushCommand extends Command public const NAME = 'image:flush'; public function __construct( - private readonly EntityTypeManagerInterface $entityTypeManager + private readonly EntityTypeManagerInterface $entityTypeManager, + private readonly ModuleHandlerInterface $moduleHandler, ) { parent::__construct(); } @@ -69,13 +69,33 @@ public function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('all')) { $input->setArgument('style_names', array_keys($this->entityTypeManager->getStorage('image_style')->loadMultiple())); } - Validators::entityLoad(StringUtils::csvToArray($input->getArgument('style_names')), 'image_style'); + + $this->validateModulesEnabled(['image']); + $this->validateEntityLoad(StringUtils::csvToArray($input->getArgument('style_names')), 'image_style'); $ids = StringUtils::csvToArray($input->getArgument('style_names')); foreach ($this->entityTypeManager->getStorage('image_style')->loadMultiple($ids) as $style_name => $style) { $style->flush(); $io->success("Image style $style_name flushed"); } - return static::SUCCESS; + return self::SUCCESS; + } + + protected function validateEntityLoad(array $ids, string $entity_type_id): void + { + $loaded = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple($ids); + if ($missing = array_diff($ids, array_keys($loaded))) { + $msg = dt('Unable to load the !type: !str', ['!type' => $entity_type_id, '!str' => implode(', ', $missing)]); + throw new \InvalidArgumentException($msg); + } + } + + protected function validateModulesEnabled(array $modules): void + { + $missing = array_filter($modules, fn($module) => !$this->moduleHandler->moduleExists($module)); + if ($missing) { + $message = dt('The following modules are required: !modules', ['!modules' => implode(', ', $missing)]); + throw new InvalidArgumentException($message); + } } } diff --git a/src/Listeners/ValidateModulesEnabledListener.php b/src/Listeners/ValidateModulesEnabledListener.php deleted file mode 100644 index 50a0f8ea69..0000000000 --- a/src/Listeners/ValidateModulesEnabledListener.php +++ /dev/null @@ -1,45 +0,0 @@ -getCommand(); - $reflection = new \ReflectionObject($command); - $attributes = $reflection->getAttributes(ValidateModulesEnabled::class); - if (empty($attributes)) { - return; - } - $instance = $attributes[0]->newInstance(); - $missing = array_filter($instance->modules, fn($module) => !$this->moduleHandler->moduleExists($module)); - if ($missing) { - $message = dt('The following modules are required: !modules', ['!modules' => implode(', ', $missing)]); - $this->logger->error($message); - $event->disableCommand(); - } - } -} From a2b44fdee72abbff6fb5b5d4bf5bfba3b3ebcc9a Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 25 Oct 2024 07:07:18 -0400 Subject: [PATCH 38/48] Lets go with calling Optionset methods statically from configure() --- src/Attributes/OptionsetTableSelection.php | 4 ++- src/Commands/OptionSets.php | 10 ++++++ src/Commands/sql/SqlDumpCommand.php | 2 +- .../OptionsetTableSelectionListener.php | 34 ------------------- 4 files changed, 14 insertions(+), 36 deletions(-) delete mode 100644 src/Listeners/OptionsetTableSelectionListener.php diff --git a/src/Attributes/OptionsetTableSelection.php b/src/Attributes/OptionsetTableSelection.php index 7d84ce06ca..7af27d9192 100644 --- a/src/Attributes/OptionsetTableSelection.php +++ b/src/Attributes/OptionsetTableSelection.php @@ -7,8 +7,10 @@ use Attribute; use Consolidation\AnnotatedCommand\Parser\CommandInfo; use Drush\Commands\DrushCommands; +use JetBrains\PhpStorm\Deprecated; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +#[Deprecated(replacement: 'Call \Drush\Commands\OptionSets::tableSelection during configure()')] +#[Attribute(Attribute::TARGET_METHOD)] class OptionsetTableSelection { public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo) diff --git a/src/Commands/OptionSets.php b/src/Commands/OptionSets.php index 94cf8168b2..e6e5b808f6 100644 --- a/src/Commands/OptionSets.php +++ b/src/Commands/OptionSets.php @@ -14,4 +14,14 @@ public static function sql(Command $command): void $command->addOption('target', '', InputOption::VALUE_REQUIRED, 'The name of a target within the specified database connection.', 'default'); $command->addOption('show-passwords', '', InputOption::VALUE_NONE, 'Show password on the CLI. Useful for debugging.'); } + + public static function tableSelection(Command $command): void + { + $command->addOption('skip-tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)'); + $command->addOption('structure-tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)'); + $command->addOption('tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $tables array.'); + $command->addOption('skip-tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to exclude completely.'); + $command->addOption('structure-tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to include for structure, but not data.'); + $command->addOption('tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to transfer.', []); + } } diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index fe02593c0f..bf92d63cc3 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -26,7 +26,6 @@ aliases: ['sql-dump'] )] #[CLI\Bootstrap(level: DrupalBootLevels::MAX, max_level: DrupalBootLevels::CONFIGURATION)] -#[CLI\OptionsetTableSelection] final class SqlDumpCommand extends Command { use AutowireTrait; @@ -58,6 +57,7 @@ protected function configure() ->setFieldLabels(['path' => 'Path']); $this->configureFormatter(PropertyList::class, $formatterOptions); OptionSets::sql($this); + OptionSets::tableSelection($this); } public function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Listeners/OptionsetTableSelectionListener.php b/src/Listeners/OptionsetTableSelectionListener.php deleted file mode 100644 index 836395b308..0000000000 --- a/src/Listeners/OptionsetTableSelectionListener.php +++ /dev/null @@ -1,34 +0,0 @@ -getApplication()->all() as $id => $command) { - $reflection = new \ReflectionObject($command); - $attributes = $reflection->getAttributes(OptionsetTableSelection::class); - if (empty($attributes)) { - continue; - } - $command->addOption('skip-tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)'); - $command->addOption('structure-tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)'); - $command->addOption('tables-key', '', InputOption::VALUE_REQUIRED, 'A key in the $tables array.'); - $command->addOption('skip-tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to exclude completely.'); - $command->addOption('structure-tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to include for structure, but not data.'); - $command->addOption('tables-list', '', InputOption::VALUE_REQUIRED, 'A comma-separated list of tables to transfer.', []); - } - } -} From b4ed0bf6e1978abbfbefaddcac2477aaea0d49c7 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 25 Oct 2024 08:16:37 -0400 Subject: [PATCH 39/48] Experiment with CommandTester - it works --- tests/integration/ImageTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 5e75ef45c0..809769d9a6 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -7,6 +7,8 @@ use Drush\Commands\core\ImageCommands; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; +use Drush\Drush; +use Symfony\Component\Console\Tester\CommandTester; /** * Tests image-flush command @@ -36,8 +38,12 @@ public function testImage() $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. - $this->drush(ImageFlushCommand::NAME, [$style_name], ['all' => null]); + // @todo We probably need a new base class for CommandTester tests. How should it be structured? + $command = Drush::getApplication()->find(ImageFlushCommand::NAME); + $commandTester = new CommandTester($command); + $commandTester->execute(['style_names' => $style_name]); $this->assertFileDoesNotExist($thumbnail); + // @todo note stdin testing documented at https://github.com/symfony/symfony/issues/37835 // Check that "drush image-flush --all" deletes all image styles by creating two different ones and testing its // existence afterwards. From 58c2ddd842515d12d870749d3849fb9ad628f4d5 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 26 Oct 2024 16:30:03 -0400 Subject: [PATCH 40/48] Move the install check to setUp() --- tests/integration/ImageTest.php | 13 +++++++------ tests/unish/UnishIntegrationTestCase.php | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 809769d9a6..f4cc2ee5d8 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -7,8 +7,8 @@ use Drush\Commands\core\ImageCommands; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; -use Drush\Drush; -use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Tester\ApplicationTester; +use Unish\Controllers\RuntimeController; /** * Tests image-flush command @@ -38,10 +38,11 @@ public function testImage() $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. - // @todo We probably need a new base class for CommandTester tests. How should it be structured? - $command = Drush::getApplication()->find(ImageFlushCommand::NAME); - $commandTester = new CommandTester($command); - $commandTester->execute(['style_names' => $style_name]); + // @todo Perhaps create a $this->getApplication() method with the line below. + // @todo Simplify RuntimeController once all commands are using a Tester? Its singleton is still useful. + $application = RuntimeController::instance()->application($this->webroot(), [$this->getDrush()]); + $applicationTester = new ApplicationTester($application); + $applicationTester->run([ImageFlushCommand::NAME, 'style_names' => $style_name]); $this->assertFileDoesNotExist($thumbnail); // @todo note stdin testing documented at https://github.com/symfony/symfony/issues/37835 diff --git a/tests/unish/UnishIntegrationTestCase.php b/tests/unish/UnishIntegrationTestCase.php index 3dbfc4f040..cb4504d45d 100644 --- a/tests/unish/UnishIntegrationTestCase.php +++ b/tests/unish/UnishIntegrationTestCase.php @@ -27,6 +27,18 @@ abstract class UnishIntegrationTestCase extends UnishTestCase protected string $stdout = ''; protected string $stderr = ''; + /** + * This method is called before each test. + */ + protected function setUp(): void + { + // Install the SUT if necessary + if (!RuntimeController::instance()->initialized()) { + $this->checkInstallSut(); + } + parent::setUp(); + } + /** * @inheritdoc */ @@ -44,7 +56,8 @@ public function getErrorOutputRaw(): string } /** - * Invoke drush via a direct method call to Application::run(). + * Invoke drush via a direct method call to Application::run(). When + * testing command output, use CommandTester or ApplicationTester instead. * * @param $command * A defined drush command such as 'cron', 'status' and so on @@ -60,11 +73,6 @@ public function getErrorOutputRaw(): string */ public function drush(string $command, array $args = [], array $options = [], ?int $expected_return = self::EXIT_SUCCESS, string|bool $stdin = false): ?int { - // Install the SUT if necessary - if (!RuntimeController::instance()->initialized()) { - $this->checkInstallSut(); - } - $cmd = $this->buildCommandLine($command, $args, $options); // Get the application instance from the runtime controller. From c1056edefde23fc4139247651245eeaf07fc4649 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 29 Oct 2024 22:15:00 -0400 Subject: [PATCH 41/48] A bit more experimentation with Applicationtester --- tests/integration/ImageTest.php | 19 +++++++++++++------ .../unish/UnishApplicationTesterTestCase.php | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 tests/unish/UnishApplicationTesterTestCase.php diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index f4cc2ee5d8..343558914a 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -7,19 +7,29 @@ use Drush\Commands\core\ImageCommands; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Tester\ApplicationTester; -use Unish\Controllers\RuntimeController; /** * Tests image-flush command * * @group commands */ -class ImageTest extends UnishIntegrationTestCase +class ImageTest extends UnishApplicationTesterTestCase { public function testImage() { - $this->drush(PmCommands::INSTALL, ['image']); + // We aren't testing pm:install so don't use ApplicationTester yet. + // This is the recommended approach from https://symfony.com/doc/current/console/calling_commands.html + $input = new ArrayInput([ + 'command' => PmCommands::INSTALL, + 'modules' => ['image'], + ]); + $output = new NullOutput(); + $application = $this->getApplication(); + $returnCode = $application->doRun($input, $output); + $logo = 'core/misc/menu-expanded.png'; $styles_dir = $this->webroot() . '/sites/default/files/styles/'; $thumbnail = $styles_dir . 'thumbnail/public/' . $logo; @@ -38,9 +48,6 @@ public function testImage() $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. - // @todo Perhaps create a $this->getApplication() method with the line below. - // @todo Simplify RuntimeController once all commands are using a Tester? Its singleton is still useful. - $application = RuntimeController::instance()->application($this->webroot(), [$this->getDrush()]); $applicationTester = new ApplicationTester($application); $applicationTester->run([ImageFlushCommand::NAME, 'style_names' => $style_name]); $this->assertFileDoesNotExist($thumbnail); diff --git a/tests/unish/UnishApplicationTesterTestCase.php b/tests/unish/UnishApplicationTesterTestCase.php new file mode 100644 index 0000000000..80b8428150 --- /dev/null +++ b/tests/unish/UnishApplicationTesterTestCase.php @@ -0,0 +1,16 @@ +application($this->webroot(), [$this->getDrush()]); + } +} From 8f4f57315e2a93d86da38fe5b757227351798ee4 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 30 Oct 2024 23:46:37 -0400 Subject: [PATCH 42/48] Delay getting $application because that can make entitytypeManager stale --- tests/integration/ImageTest.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 343558914a..b7654b9416 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -7,8 +7,6 @@ use Drush\Commands\core\ImageCommands; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Tester\ApplicationTester; /** @@ -20,15 +18,17 @@ class ImageTest extends UnishApplicationTesterTestCase { public function testImage() { - // We aren't testing pm:install so don't use ApplicationTester yet. - // This is the recommended approach from https://symfony.com/doc/current/console/calling_commands.html - $input = new ArrayInput([ - 'command' => PmCommands::INSTALL, - 'modules' => ['image'], - ]); - $output = new NullOutput(); + $this->drush(PmCommands::INSTALL, ['image']); $application = $this->getApplication(); - $returnCode = $application->doRun($input, $output); + // We aren't testing pm:install so don't use ApplicationTester. + // This is the recommended approach from https://symfony.com/doc/current/console/calling_commands.html + // We can't actually do this until pm:install moves from an AnnotatedCommand. +// $input = new ArrayInput([ +// 'command' => PmCommands::INSTALL, +// 'modules' => ['image'], +// ]); +// $output = new NullOutput(); +// $returnCode = $application->doRun($input, $output); $logo = 'core/misc/menu-expanded.png'; $styles_dir = $this->webroot() . '/sites/default/files/styles/'; @@ -41,6 +41,7 @@ public function testImage() // Remove stray files left over from previous runs @unlink($thumbnail); + $this->assertFileDoesNotExist($thumbnail); // Test that "drush image-derive" works. $style_name = 'thumbnail'; @@ -49,7 +50,8 @@ public function testImage() // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. $applicationTester = new ApplicationTester($application); - $applicationTester->run([ImageFlushCommand::NAME, 'style_names' => $style_name]); + $applicationTester->run([ImageFlushCommand::NAME, 'style_names' => $style_name, '--no-interaction' => true]); + $output = $applicationTester->getDisplay(); $this->assertFileDoesNotExist($thumbnail); // @todo note stdin testing documented at https://github.com/symfony/symfony/issues/37835 From fdfaf0f05a2a17ecaa0aab8c94fb44d4ad70864d Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 31 Oct 2024 08:01:12 -0400 Subject: [PATCH 43/48] Dont store $application in local variable. it gets stale. --- tests/integration/ImageTest.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index b7654b9416..98df2d6e08 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -7,6 +7,8 @@ use Drush\Commands\core\ImageCommands; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Tester\ApplicationTester; /** @@ -18,17 +20,15 @@ class ImageTest extends UnishApplicationTesterTestCase { public function testImage() { - $this->drush(PmCommands::INSTALL, ['image']); - $application = $this->getApplication(); // We aren't testing pm:install so don't use ApplicationTester. // This is the recommended approach from https://symfony.com/doc/current/console/calling_commands.html - // We can't actually do this until pm:install moves from an AnnotatedCommand. -// $input = new ArrayInput([ -// 'command' => PmCommands::INSTALL, -// 'modules' => ['image'], -// ]); -// $output = new NullOutput(); -// $returnCode = $application->doRun($input, $output); + $input = new ArrayInput([ + 'command' => PmCommands::INSTALL, + 'modules' => ['image'], + ]); + $output = new NullOutput(); + $returnCode = $this->getApplication()->doRun($input, $output); + // $this->drush(PmCommands::INSTALL, ['image']); $logo = 'core/misc/menu-expanded.png'; $styles_dir = $this->webroot() . '/sites/default/files/styles/'; @@ -49,7 +49,7 @@ public function testImage() $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. - $applicationTester = new ApplicationTester($application); + $applicationTester = new ApplicationTester($this->getApplication()); $applicationTester->run([ImageFlushCommand::NAME, 'style_names' => $style_name, '--no-interaction' => true]); $output = $applicationTester->getDisplay(); $this->assertFileDoesNotExist($thumbnail); From 3712e5dcef6298a4d9e1290dc6601602b6f916e0 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 31 Oct 2024 09:30:43 -0400 Subject: [PATCH 44/48] drush() method is good enough for now --- tests/integration/ImageTest.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 98df2d6e08..68550a272f 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -7,8 +7,6 @@ use Drush\Commands\core\ImageCommands; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Tester\ApplicationTester; /** @@ -20,15 +18,7 @@ class ImageTest extends UnishApplicationTesterTestCase { public function testImage() { - // We aren't testing pm:install so don't use ApplicationTester. - // This is the recommended approach from https://symfony.com/doc/current/console/calling_commands.html - $input = new ArrayInput([ - 'command' => PmCommands::INSTALL, - 'modules' => ['image'], - ]); - $output = new NullOutput(); - $returnCode = $this->getApplication()->doRun($input, $output); - // $this->drush(PmCommands::INSTALL, ['image']); + $this->drush(PmCommands::INSTALL, ['image']); $logo = 'core/misc/menu-expanded.png'; $styles_dir = $this->webroot() . '/sites/default/files/styles/'; From f0ffbd94c9089abfa325e8445dcd0e28af0cbbb8 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 1 Nov 2024 19:48:39 -0400 Subject: [PATCH 45/48] Docs for Console commands --- docs/commands.md | 83 ++++++++++------------------- src/Commands/sql/SqlDumpCommand.php | 3 +- 2 files changed, 29 insertions(+), 57 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 095cfb88ca..cf619918f2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2,8 +2,8 @@ !!! tip - 1. Drush 13 expects commandfiles to use the [AutowireTrait](https://github.com/drush-ops/drush/blob/13.x/src/Commands/AutowireTrait.php) 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 14. - 1. Drush 12 expects all commandfiles in the `/src/Drush/` directory. The `Drush` subdirectory is a new requirement. + 1. Drush 13+ expects commandfiles to use the [AutowireTrait](https://github.com/drush-ops/drush/blob/13.x/src/Commands/AutowireTrait.php) 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 14. + 1. Drush 12+ expects all commandfiles in the `/src/Drush/` directory. The `Drush` subdirectory is a new requirement. Creating a new Drush command is easy. Follow the steps below. @@ -14,67 +14,38 @@ Creating a new Drush command is easy. Follow the steps below. 5. You may [inject dependencies](dependency-injection.md) into a command instance. 6. Write PHPUnit tests based on [Drush Test Traits](https://github.com/drush-ops/drush/blob/13.x/docs/contribute/unish.md#drush-test-traits). -## Attributes or Annotations -The following are both valid ways to declare a command: - -=== "PHP8 Attributes" - - ```php - use Drush\Attributes as CLI; - - /** - * Retrieve and display xkcd cartoons (attribute variant). - */ - #[CLI\Command(name: 'xkcd:fetch-attributes', aliases: ['xkcd-attributes'])] - #[CLI\Argument(name: 'search', description: 'Optional argument to retrieve the cartoons matching an index, keyword, or "random".')] - #[CLI\Option(name: 'image-viewer', description: 'Command to use to view images (e.g. xv, firefox).', suggestedValues: ['open', 'xv', 'firefox'])] - #[CLI\Option(name: 'google-custom-search-api-key', description: 'Google Custom Search API Key')] - #[CLI\Usage(name: 'drush xkcd', description: 'Retrieve and display the latest cartoon')] - #[CLI\Usage(name: 'drush xkcd sandwich', description: 'Retrieve and display cartoons about sandwiches.')] - public function fetch($search = null, $options = ['image-viewer' => 'open', 'google-custom-search-api-key' => 'AIza']) { - $this->doFetch($search, $options); - } - ``` - -=== "Annotations" - - ```php - /** - * @command xkcd:fetch - * @param $search Optional argument to retrieve the cartoons matching an index number, keyword, or "random". - * @option image-viewer Command to use to view images (e.g. xv, firefox). - * @option google-custom-search-api-key Google Custom Search API Key. - * @usage drush xkcd - * Retrieve and display the latest cartoon. - * @usage drush xkcd sandwich - * Retrieve and display cartoons about sandwiches. - * @aliases xkcd - */ - public function fetch($search = null, $options = ['image-viewer' => 'open', 'google-custom-search-api-key' => 'AIza']) { - $this->doFetch($search, $options); - } - ``` +## Symfony Console Commands -- A commandfile that will only be used on PHP8+ should [use PHP Attributes](https://github.com/drush-ops/drush/pull/4821) instead of Annotations. -- [See Attributes provided by Drush core](https://www.drush.org/api/Drush/Attributes.html). Custom code can supply additional Attribute classes, which may then be added to any command. For example see [InteractConfigName](https://github.com/drush-ops/drush/blob/13.x/src/Attributes/InteractConfigName.php) which is used by [ConfigCommands](https://github.com/drush-ops/drush/blob/8b77c9abe6639de42a198c7e69565f09dcf5f22d/src/Commands/config/ConfigCommands.php#L98). +Drush 14+ deprecates old-style Annotated Commands in favor of pure [Symfony Console commands](https://symfony.com/doc/current/console.html). This implies: + +- Each command lives in its own class file +- The command class extends `Symfony\Component\Console\Command\Command` directly. The base class `DrushCommands` is deprecated. +- The command class should use Console's #[AsCommand] Attribute to declare its name, aliases, and hidden status. The old #[Command] Attribute is deprecated. +- Options and Arguments moved from Attributes to a configure() method on the command class +- The main logic of the command moved to an execute() method on the command class. +- User interaction now happens in an interact() method on the command class. +- Drush and Drupal services may still be autowired. This is how you access the logger. Build own $io as needed. +- Commands that wish to offer multiple _output formats_ (yes please!) should (Example: _TwigUnusedCommand_, + _SqlDumpCommand_): + - inject `FormatterManager` in __construct() + - `use FormatterTrait` + - call `$this->configureFormatter()` in `configure()` in order to automatically add the needed options. + - `execute()` is boilerplate. By convention, do your work in a `doExecute()` method instead. +- [See Optionsets provided by Drush core](https://github.com/drush-ops/drush/blob/13.x/src/Commands/OptionsSets.php). Custom code can supply additional Optionset methods, which any command may choose to use. ## 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 classes that are able to intercept and manipulate an existing command annotation. -In the module that wants to alter a command info, add a class that: +Drush command info can be altered from other modules. This is done by creating and registering a command definition listener. Listeners are dispatched once after non-bootstrap commands are instantiated and once again after bootstrap commands are instantiated. -1. The 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/13.x/sut/modules/unish/woot/src/Drush/CommandInfoAlterers/WootCommandInfoAlterer.php) provided by the testing 'woot' module. +In the module that wants to alter a command info, add a class that: -## Symfony Console Commands +1. The class namespace, relative to base namespace, should be `Drupal\\Drush\Listeners` and the class file should be located under the `src/Drush/Listeners` directory. +1. The filename must have a name like FooListener.php. The prefix `Foo` can be whatever string you want. The file must end in `Listener.php`. +1. The class should implement the `#[AsListener]` PHP Attribute. +1. Implement the alteration logic via a `__invoke(ConsoleDefinitionsEvent $event)` 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 logger in the class constructor. -Drush lists and runs Symfony Console commands, in addition to more typical annotated commands. -See [GreetCommands](https://github.com/drush-ops/drush/blob/13.x/sut/modules/unish/woot/src/Drush/Commands/GreetCommands.php) as an example. Note that these commands must conform to the usual class name and class namespace requirements. You might need to extend the Console class if you can't rename and move it. +For an example, see [WootDefinitionListener](https://github.com/drush-ops/drush/blob/13.x/sut/modules/unish/woot/src/Drush/Liseners/WootDefinitionListener.php) provided by the testing 'woot' module. ## Auto-discovered commands (PSR4) diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index bf92d63cc3..4f5214d6a7 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -64,8 +64,9 @@ public function execute(InputInterface $input, OutputInterface $output): int { $data = $this->doExecute($input, $output); $this->writeFormattedOutput($input, $output, $data); - return static::SUCCESS; + return self::SUCCESS; } + protected function doExecute(InputInterface $input, OutputInterface $output): PropertyList { $sql = SqlBase::create($input->getOptions()); From aa21ad937cd43052b8e431e8fc042fb235f98f82 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 2 Nov 2024 06:31:33 -0400 Subject: [PATCH 46/48] Convert image:derive --- src/Attributes/ValidateFileExists.php | 2 + src/Commands/core/ImageCommands.php | 24 +------ src/Commands/core/ImageDeriveCommand.php | 88 ++++++++++++++++++++++++ src/Commands/sql/SqlDumpCommand.php | 2 +- tests/integration/ImageTest.php | 10 +-- 5 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 src/Commands/core/ImageDeriveCommand.php diff --git a/src/Attributes/ValidateFileExists.php b/src/Attributes/ValidateFileExists.php index 2e2cc613c2..d05debfe6d 100644 --- a/src/Attributes/ValidateFileExists.php +++ b/src/Attributes/ValidateFileExists.php @@ -7,7 +7,9 @@ use Attribute; use Consolidation\AnnotatedCommand\CommandData; use Consolidation\AnnotatedCommand\CommandError; +use JetBrains\PhpStorm\Deprecated; +#[Deprecated(replacement: 'Copy \Drush\Commands\core\ImageDeriveCommand::validateFileExists into command and call during execute()')] #[Attribute(Attribute::TARGET_METHOD)] class ValidateFileExists extends ValidatorBase implements ValidatorInterface { diff --git a/src/Commands/core/ImageCommands.php b/src/Commands/core/ImageCommands.php index e544c6210e..cc446e3d70 100644 --- a/src/Commands/core/ImageCommands.php +++ b/src/Commands/core/ImageCommands.php @@ -4,35 +4,13 @@ namespace Drush\Commands\core; -use Drupal\image\Entity\ImageStyle; -use Drush\Attributes as CLI; -use Drush\Boot\DrupalBootLevels; use Drush\Commands\DrushCommands; use JetBrains\PhpStorm\Deprecated; final class ImageCommands extends DrushCommands { + #[Deprecated(replacement: 'ImageDeriveCommand::NAME')] const DERIVE = 'image:derive'; #[Deprecated(replacement: 'ImageFlushCommand::NAME')] const FLUSH = ImageFlushCommand::NAME; - - /** - * Create an image derivative. - */ - #[CLI\Command(name: self::DERIVE, aliases: ['id', 'image-derive'])] - #[CLI\Argument(name: 'style_name', description: 'An image style machine name.')] - #[CLI\Argument(name: 'source', description: 'Path to a source image. Optionally prepend stream wrapper scheme. Relative paths calculated from Drupal root.')] - #[CLI\Usage(name: 'drush image:derive thumbnail core/themes/bartik/screenshot.png', description: 'Save thumbnail sized derivative of logo image.')] - #[CLI\ValidateFileExists(argName: 'source')] - #[CLI\ValidateEntityLoad(entityType: 'image_style', argumentName: 'style_name')] - #[CLI\ValidateModulesEnabled(modules: ['image'])] - #[CLI\Bootstrap(level: DrupalBootLevels::FULL)] - public function derive($style_name, $source) - { - $image_style = ImageStyle::load($style_name); - $derivative_uri = $image_style->buildUri($source); - if ($image_style->createDerivative($source, $derivative_uri)) { - return $derivative_uri; - } - } } diff --git a/src/Commands/core/ImageDeriveCommand.php b/src/Commands/core/ImageDeriveCommand.php new file mode 100644 index 0000000000..fd7bfbc499 --- /dev/null +++ b/src/Commands/core/ImageDeriveCommand.php @@ -0,0 +1,88 @@ +addArgument('style_name', InputArgument::REQUIRED, 'An image style machine name.') + ->addArgument('source', InputArgument::REQUIRED, 'Path to a source image. Optionally prepend stream wrapper scheme. Relative paths calculated from Drupal root.') + ->addUsage('image:derive thumbnail core/themes/bartik/screenshot.png'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $io = new DrushStyle($input, $output); + + $this->validateModulesEnabled(['image']); + $this->validateEntityLoad([$input->getArgument('style_name')], 'image_style'); + $this->validateFileExists($input->getArgument('source')); + + $image_style = $this->entityTypeManager->getStorage('image_style')->load($input->getArgument('style_name')); + $derivative_uri = $image_style->buildUri($input->getArgument('source')); + if ($image_style->createDerivative($input->getArgument('source'), $derivative_uri)) { + $io->success(dt('Derivative image created: !uri', ['!uri' => $derivative_uri])); + return self::SUCCESS; + } + return self::FAILURE; + } + + protected function validateFileExists(string $path): void + { + if (!empty($path) && !file_exists($path)) { + $msg = dt('File not found: !path', ['!path' => $path]); + throw new InvalidArgumentException($msg); + } + } + + protected function validateEntityLoad(array $ids, string $entity_type_id): void + { + $loaded = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple($ids); + if ($missing = array_diff($ids, array_keys($loaded))) { + $msg = dt('Unable to load the !type: !str', ['!type' => $entity_type_id, '!str' => implode(', ', $missing)]); + throw new \InvalidArgumentException($msg); + } + } + + protected function validateModulesEnabled(array $modules): void + { + $missing = array_filter($modules, fn($module) => !$this->moduleHandler->moduleExists($module)); + if ($missing) { + $message = dt('The following modules are required: !modules', ['!modules' => implode(', ', $missing)]); + throw new InvalidArgumentException($message); + } + } +} diff --git a/src/Commands/sql/SqlDumpCommand.php b/src/Commands/sql/SqlDumpCommand.php index 4f5214d6a7..8cd257ecd8 100644 --- a/src/Commands/sql/SqlDumpCommand.php +++ b/src/Commands/sql/SqlDumpCommand.php @@ -66,7 +66,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $this->writeFormattedOutput($input, $output, $data); return self::SUCCESS; } - + protected function doExecute(InputInterface $input, OutputInterface $output): PropertyList { $sql = SqlBase::create($input->getOptions()); diff --git a/tests/integration/ImageTest.php b/tests/integration/ImageTest.php index 68550a272f..8a0299be5c 100644 --- a/tests/integration/ImageTest.php +++ b/tests/integration/ImageTest.php @@ -4,13 +4,13 @@ namespace Unish; -use Drush\Commands\core\ImageCommands; +use Drush\Commands\core\ImageDeriveCommand; use Drush\Commands\core\ImageFlushCommand; use Drush\Commands\pm\PmCommands; use Symfony\Component\Console\Tester\ApplicationTester; /** - * Tests image-flush command + * Tests image:flush and image:derive commands. * * @group commands */ @@ -35,7 +35,7 @@ public function testImage() // Test that "drush image-derive" works. $style_name = 'thumbnail'; - $this->drush(ImageCommands::DERIVE, [$style_name, $logo]); + $this->drush(ImageDeriveCommand::NAME, [$style_name, $logo]); $this->assertFileExists($thumbnail); // Test that "drush image-flush thumbnail" deletes derivatives created by the thumbnail image style. @@ -47,9 +47,9 @@ public function testImage() // Check that "drush image-flush --all" deletes all image styles by creating two different ones and testing its // existence afterwards. - $this->drush(ImageCommands::DERIVE, ['thumbnail', $logo]); + $this->drush(ImageDeriveCommand::NAME, ['thumbnail', $logo]); $this->assertFileExists($thumbnail); - $this->drush(ImageCommands::DERIVE, ['medium', $logo]); + $this->drush(ImageDeriveCommand::NAME, ['medium', $logo]); $this->assertFileExists($medium); $this->drush(ImageFlushCommand::NAME, [], ['all' => null]); $this->assertFileDoesNotExist($thumbnail); From 8ef8994f04e9095fa524f743ef811920aa743f4a Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 2 Nov 2024 13:36:54 -0400 Subject: [PATCH 47/48] Migrate config:set into a Console command --- src/Commands/config/ConfigCommands.php | 71 -------------- src/Commands/config/ConfigSetCommand.php | 117 +++++++++++++++++++++++ src/Commands/core/ImageDeriveCommand.php | 3 - src/Runtime/DependencyInjection.php | 2 + 4 files changed, 119 insertions(+), 74 deletions(-) create mode 100644 src/Commands/config/ConfigSetCommand.php diff --git a/src/Commands/config/ConfigCommands.php b/src/Commands/config/ConfigCommands.php index a2830f7cf6..aab2336cd0 100644 --- a/src/Commands/config/ConfigCommands.php +++ b/src/Commands/config/ConfigCommands.php @@ -34,7 +34,6 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Path; -use Symfony\Component\Yaml\Parser; final class ConfigCommands extends DrushCommands implements StdinAwareInterface { @@ -106,76 +105,6 @@ public function get($config_name, $key = '', $options = ['format' => 'yaml', 'so return $key ? ["$config_name:$key" => $value] : $value; } - /** - * Save a config value directly. Does not perform a config import. - */ - #[CLI\Command(name: self::SET, aliases: ['cset', 'config-set'])] - #[CLI\Argument(name: 'config_name', description: 'The config object name, for example system.site.')] - #[CLI\Argument(name: 'key', description: 'The config key, for example page.front. Use ? if you are updating multiple top-level keys.')] - #[CLI\Argument(name: 'value', description: 'The value to assign to the config key. Use - to read from Stdin.')] - #[CLI\Option(name: 'input-format', description: 'Format to parse the object. Recognized values: string, yaml. Since JSON is a subset of YAML, $value may be in JSON format.', suggestedValues: ['string', 'json'])] - #[CLI\Usage(name: 'drush config:set system.site name MySite', description: 'Sets a value for the key name of system.site config object.')] - #[CLI\Usage(name: 'drush config:set system.site page.front /path/to/page', description: 'Sets the given URL path as value for the config item with key page.front of system.site config object.')] - #[CLI\Usage(name: 'drush config:set system.site \'[]\'', description: 'Sets the given key to an empty array.')] - #[CLI\Usage(name: 'drush config:set system.site \'NULL\'', description: 'Sets the given key to NULL.')] - #[CLI\Usage(name: 'drush config:set --input-format=yaml user.role.authenticated permissions [foo,bar]', description: 'Use a sequence as value for the key permissions of user.role.authenticated config object.')] - #[CLI\Usage(name: "drush config:set --input-format=yaml system.site page {403: '403', front: home}", description: 'Use a mapping as value for the key page of system.site config object.')] - #[CLI\Usage(name: 'drush config:set --input-format=yaml user.role.authenticated ? "{label: \'Auth user\', weight: 5}"', description: 'Update two top level keys (label, weight) in the system.site config object.')] - #[CLI\Usage(name: 'cat tmp.yml | drush config:set --input-format=yaml user.mail ? -', description: 'Update the user.mail config object in its entirety.')] - #[CLI\Complete(method_name_or_callable: 'configComplete')] - public function set($config_name, $key, $value, $options = ['input-format' => 'string']) - { - $data = $value; - - if (!isset($data)) { - throw new \Exception(dt('No config value specified.')); - } - - // Special flag indicating that the value has been passed via STDIN. - if ($data === '-') { - $data = $this->stdin()->contents(); - } - - // 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); - } - - $config = $this->getConfigFactory()->getEditable($config_name); - // Check to see if config key already exists. - $new_key = $config->get($key) === null; - $simulate = $this->getConfig()->simulate(); - - if ($key == '?' && !empty($data) && $this->io()->confirm(dt('Do you want to update or set multiple keys on !name config.', ['!name' => $config_name]))) { - foreach ($data as $data_key => $val) { - $config->set($data_key, $val); - } - return $simulate ? self::EXIT_SUCCESS : $config->save(); - } else { - $confirmed = false; - if ($config->isNew() && $this->io()->confirm(dt('!name config does not exist. Do you want to create a new config object?', ['!name' => $config_name]))) { - $confirmed = true; - } elseif ($new_key && $this->io()->confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', ['!key' => $key, '!name' => $config_name]))) { - $confirmed = true; - } elseif ($this->io()->confirm(dt('Do you want to update !key key in !name config?', ['!key' => $key, '!name' => $config_name]))) { - $confirmed = true; - } - if ($confirmed && !$simulate) { - return $config->set($key, $data)->save(); - } - } - } - /** * Open a config file in a text editor. Edits are imported after closing editor. */ diff --git a/src/Commands/config/ConfigSetCommand.php b/src/Commands/config/ConfigSetCommand.php new file mode 100644 index 0000000000..7a5173972f --- /dev/null +++ b/src/Commands/config/ConfigSetCommand.php @@ -0,0 +1,117 @@ +addArgument('config_name', InputArgument::REQUIRED, 'The config object name, for example system.site.') + ->addArgument('key', InputArgument::REQUIRED, 'The config key, for example page.front. Use ? if you are updating multiple top-level keys.') + ->addArgument('value', InputArgument::REQUIRED, 'The value to assign to the config key. Use - to read from Stdin.') + ->addOption('input-format', null, InputOption::VALUE_REQUIRED, 'Format to parse the object. Recognized values: string, yaml. Since JSON is a subset of YAML, $value may be in JSON format.', 'string') + // @todo Move the old descriptions of these Usages into setHelp(). + ->addUsage('config:set system.site name MySite') + ->addUsage('config:set user.role.anonymous permissions \'[]\'') + ->addUsage('config:set system.site name \'NULL\'') + ->addUsage("config:set --input-format=yaml system.site page {403: '403', front: home}") + ->addUsage('config:set --input-format=yaml user.role.authenticated permissions [foo,bar]') + ->addUsage('config:set --input-format=yaml user.role.authenticated ? "{label: \'Auth user\', weight: 5}') + ->addUsage('cat tmp.yml | drush config:set --input-format=yaml user.mail ? -'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $io = new DrushStyle($input, $output); + + $data = $input->getArgument('value'); + + // Special flag indicating that the value has been passed via STDIN. + if ($data === '-') { + // See https://github.com/symfony/symfony/issues/37835#issuecomment-674386588. + // If testing this will get input added by `CommandTester::setInputs` method. + $inputStream = ($input instanceof StreamableInputInterface) ? $input->getStream() : null; + // If nothing from input stream use STDIN instead. + $inputStream = $inputStream ?? STDIN; + $data = stream_get_contents($inputStream); + } + + // Special handling for null. + if (strtolower($data) === 'null') { + $data = null; + } + + // Special handling for empty array. + if ($data == '[]') { + $data = []; + } + + if ($input->getOption('input-format') === 'yaml') { + $parser = new Parser(); + $data = $parser->parse($data); + } + + $config_name = $input->getArgument('config_name'); + $config = $this->configFactory->getEditable($config_name); + // Check to see if config key already exists. + $key = $input->getArgument('key'); + $new_key = $config->get($key) === null; + $simulate = $this->drushConfig->simulate(); + + if ($key == '?' && !empty($data) && $io->confirm(dt('Do you want to update or set multiple keys on !name config.', ['!name' => $config_name]))) { + foreach ($data as $data_key => $val) { + $config->set($data_key, $val); + } + if (!$simulate) { + $config->save(); + } + return self::SUCCESS; + } else { + $confirmed = false; + if ($config->isNew() && $io->confirm(dt('!name config does not exist. Do you want to create a new config object?', ['!name' => $config_name]))) { + $confirmed = true; + } elseif ($new_key && $io->confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', ['!key' => $key, '!name' => $config_name]))) { + $confirmed = true; + } elseif ($io->confirm(dt('Do you want to update !key key in !name config?', ['!key' => $key, '!name' => $config_name]))) { + $confirmed = true; + } + if ($confirmed && !$simulate) { + $config->set($key, $data)->save(); + return self::SUCCESS; + } + } + } +} diff --git a/src/Commands/core/ImageDeriveCommand.php b/src/Commands/core/ImageDeriveCommand.php index fd7bfbc499..2dc6b36684 100644 --- a/src/Commands/core/ImageDeriveCommand.php +++ b/src/Commands/core/ImageDeriveCommand.php @@ -6,8 +6,6 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; -use Drush\Attributes as CLI; -use Drush\Boot\DrupalBootLevels; use Drush\Commands\AutowireTrait; use Drush\Style\DrushStyle; use InvalidArgumentException; @@ -22,7 +20,6 @@ description: 'Create an image derivative', aliases: ['id', 'image-derive'] )] -#[CLI\Bootstrap(level: DrupalBootLevels::FULL)] final class ImageDeriveCommand extends Command { use AutowireTrait; diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 9c902bc0b4..bd0afff68b 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -170,6 +170,8 @@ protected function alterServicesForDrush($container, Application $application, I // Alias the dispatcher service that is defined in \Robo\Robo::configureContainer. Robo::addShared($container, EventDispatcherInterface::class, 'eventDispatcher'); // For autowiring + // Alias the config service that is defined in \Robo\Robo::configureContainer. + Robo::addShared($container, DrushConfig::class, 'config'); // For autowiring // Add our own callback to the hook manager $hookManager = $container->get('hookManager'); From b9817b96f11bcf03fe838786cb7f9407a2773c31 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sun, 3 Nov 2024 23:30:34 -0500 Subject: [PATCH 48/48] Config:get is now a Console command --- src/Commands/config/ConfigCommands.php | 24 +------- src/Commands/config/ConfigGetCommand.php | 70 ++++++++++++++++++++++++ src/Commands/config/ConfigNameTrait.php | 45 +++++++++++++++ src/Commands/config/ConfigSetCommand.php | 3 +- tests/functional/ConfigPullTest.php | 7 ++- tests/functional/ConfigTest.php | 32 ++++++----- tests/functional/FieldTest.php | 4 +- 7 files changed, 140 insertions(+), 45 deletions(-) create mode 100644 src/Commands/config/ConfigGetCommand.php create mode 100644 src/Commands/config/ConfigNameTrait.php diff --git a/src/Commands/config/ConfigCommands.php b/src/Commands/config/ConfigCommands.php index aab2336cd0..c07f052d24 100644 --- a/src/Commands/config/ConfigCommands.php +++ b/src/Commands/config/ConfigCommands.php @@ -43,6 +43,7 @@ final class ConfigCommands extends DrushCommands implements StdinAwareInterface const INTERACT_CONFIG_NAME = 'interact-config-name'; const VALIDATE_CONFIG_NAME = 'validate-config-name'; + #[Deprecated(reason: 'Use ConfigGetCommand::NAME')] const GET = 'config:get'; const SET = 'config:set'; const EDIT = 'config:edit'; @@ -82,29 +83,6 @@ public function getImportTransformer(): ImportStorageTransformer return $this->importStorageTransformer; } - /** - * Display a config value, or a whole configuration object. - */ - #[CLI\Command(name: self::GET, aliases: ['cget','config-get'])] - #[CLI\Argument(name: 'config_name', description: 'The config object name, for example system.site.')] - #[CLI\Argument(name: 'key', description: 'The config key, for example page.front. Optional.')] - #[CLI\Option(name: 'source', description: 'The config storage source to read.')] - #[CLI\Option(name: 'include-overridden', description: 'Apply module and settings.php overrides to values.')] - #[CLI\Usage(name: 'drush config:get system.site', description: 'Displays the system.site config.')] - #[CLI\Usage(name: 'drush config:get system.site page.front', description: 'Gets system.site:page.front value.')] - #[CLI\Complete(method_name_or_callable: 'configComplete')] - #[CLI\ValidateConfigName()] - #[CLI\InteractConfigName()] - public function get($config_name, $key = '', $options = ['format' => 'yaml', 'source' => 'active', 'include-overridden' => false]) - { - // Displaying overrides only applies to active storage. - $factory = $this->getConfigFactory(); - $config = $options['include-overridden'] ? $factory->get($config_name) : $factory->getEditable($config_name); - $value = $config->get($key); - // @todo If the value is TRUE (for example), nothing gets printed. Is this yaml formatter's fault? - return $key ? ["$config_name:$key" => $value] : $value; - } - /** * Open a config file in a text editor. Edits are imported after closing editor. */ diff --git a/src/Commands/config/ConfigGetCommand.php b/src/Commands/config/ConfigGetCommand.php new file mode 100644 index 0000000000..18551dbbe5 --- /dev/null +++ b/src/Commands/config/ConfigGetCommand.php @@ -0,0 +1,70 @@ +addArgument('config_name', InputArgument::REQUIRED, 'The config object name, for example system.site.') + ->addArgument('key', InputArgument::OPTIONAL, 'The config key, for example page.front. Optional') + ->addOption('source', null, InputOption::VALUE_REQUIRED, 'The config storage source to read.', 'active') + ->addOption('include-overridden', null, InputOption::VALUE_NEGATABLE, 'Apply module and settings.php overrides to values') + ->addUsage('config:get system.site page.front') + ->addUsage('config:get system.site'); + $formatterOptions = (new FormatterOptions()); + $this->configureFormatter(UnstructuredListData::class, $formatterOptions); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $data = $this->doExecute($input); + $this->writeFormattedOutput($input, $output, $data); + return self::SUCCESS; + } + + protected function doExecute($input): string|array + { + $config_name = $input->getArgument('config_name'); + $this->validateConfigName($config_name); + $key = $input->getArgument('key'); + + // Displaying overrides only applies to active storage. + $config = $input->getOption('include-overridden') ? $this->configFactory->get($config_name) : $this->configFactory->getEditable($config_name); + $value = $config->get($key); + return $key ? ["$config_name:$key" => $value] : $value; + } +} diff --git a/src/Commands/config/ConfigNameTrait.php b/src/Commands/config/ConfigNameTrait.php new file mode 100644 index 0000000000..9e7882e034 --- /dev/null +++ b/src/Commands/config/ConfigNameTrait.php @@ -0,0 +1,45 @@ +hasArgument('config_name') && empty($input->getArgument('config_name'))) { + $io = new DrushStyle($input, $output); + // Classes using this trait must have a $configFactory property. + $config_names = $this->configFactory->listAll(); + $choice = $io->suggest('Choose a configuration', array_combine($config_names, $config_names), scroll: 200, required: true); + $input->setArgument('config_name', $choice); + } + } + + // Call this from the execute method of the command that uses this trait. + protected function validateConfigName(string|array $config_name): void + { + $names = StringUtils::csvToArray($config_name); + foreach ($names as $name) { + $config = $this->configFactory->get($name); + if ($config->isNew()) { + $msg = dt('Config !name does not exist', ['!name' => $name]); + throw new InvalidArgumentException($msg); + } + } + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('config_name')) { + $suggestions->suggestValues($this->configFactory->listAll()); + } + } +} diff --git a/src/Commands/config/ConfigSetCommand.php b/src/Commands/config/ConfigSetCommand.php index 7a5173972f..86a6fefe66 100644 --- a/src/Commands/config/ConfigSetCommand.php +++ b/src/Commands/config/ConfigSetCommand.php @@ -98,7 +98,6 @@ public function execute(InputInterface $input, OutputInterface $output): int if (!$simulate) { $config->save(); } - return self::SUCCESS; } else { $confirmed = false; if ($config->isNew() && $io->confirm(dt('!name config does not exist. Do you want to create a new config object?', ['!name' => $config_name]))) { @@ -110,8 +109,8 @@ public function execute(InputInterface $input, OutputInterface $output): int } if ($confirmed && !$simulate) { $config->set($key, $data)->save(); - return self::SUCCESS; } } + return self::SUCCESS; } } diff --git a/tests/functional/ConfigPullTest.php b/tests/functional/ConfigPullTest.php index bc614218a1..6981a9eec0 100644 --- a/tests/functional/ConfigPullTest.php +++ b/tests/functional/ConfigPullTest.php @@ -4,9 +4,10 @@ namespace Unish; -use Drush\Commands\config\ConfigPullCommands; use Drush\Commands\config\ConfigCommands; +use Drush\Commands\config\ConfigGetCommand; use Drush\Commands\config\ConfigImportCommands; +use Drush\Commands\config\ConfigPullCommands; use Symfony\Component\Filesystem\Path; /** @@ -37,14 +38,14 @@ public function testConfigPull() $source = $aliases['stage']; $destination = $aliases['dev']; // Make UUID match. - $this->drush(ConfigCommands::GET, ['system.site', 'uuid'], $options, $source); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'uuid'], $options, $source); list($name, $uuid) = explode(' ', $this->getOutput()); $this->drush(ConfigCommands::SET, ['system.site', 'uuid', $uuid], $options, $destination); $this->drush(ConfigCommands::SET, ['system.site', 'name', 'testConfigPull'], $options, $source); $this->drush(ConfigPullCommands::PULL, [$source, $destination], $options); $this->drush(ConfigImportCommands::IMPORT, [], $options, $destination); - $this->drush(ConfigCommands::GET, ['system.site', 'name'], $options, $source); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'name'], $options, $source); $this->assertEquals("'system.site:name': testConfigPull", $this->getOutput(), 'Config was successfully pulled.'); // Test that custom target dir works diff --git a/tests/functional/ConfigTest.php b/tests/functional/ConfigTest.php index 3616e5c059..694e11becc 100644 --- a/tests/functional/ConfigTest.php +++ b/tests/functional/ConfigTest.php @@ -5,12 +5,14 @@ namespace Unish; use Drupal\Core\Serialization\Yaml; -use Drush\Commands\core\PhpCommands; -use Drush\Commands\core\StatusCommands; use Drush\Commands\config\ConfigCommands; use Drush\Commands\config\ConfigExportCommands; +use Drush\Commands\config\ConfigGetCommand; use Drush\Commands\config\ConfigImportCommands; +use Drush\Commands\config\ConfigSetCommand; +use Drush\Commands\core\PhpCommands; use Drush\Commands\core\StateCommands; +use Drush\Commands\core\StatusCommands; use Drush\Commands\pm\PmCommands; use Symfony\Component\Filesystem\Path; @@ -40,29 +42,29 @@ public function setup(): void public function testConfigGetSet() { // Simple value - $this->drush(ConfigCommands::SET, ['system.site', 'name', 'config_test']); - $this->drush(ConfigCommands::GET, ['system.site', 'name']); + $this->drush(ConfigSetCommand::NAME, ['system.site', 'name', 'config_test']); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'name']); $this->assertEquals("'system.site:name': config_test", $this->getOutput()); // Nested value - $this->drush(ConfigCommands::SET, ['system.site', 'page.front', 'llama']); - $this->drush(ConfigCommands::GET, ['system.site', 'page.front']); + $this->drush(ConfigSetCommand::NAME, ['system.site', 'page.front', 'llama']); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'page.front']); $this->assertEquals("'system.site:page.front': llama", $this->getOutput()); // Simple sequence value - $this->drush(ConfigCommands::SET, ['user.role.authenticated', 'permissions', '[foo,bar]'], ['input-format' => 'yaml']); - $this->drush(ConfigCommands::GET, ['user.role.authenticated', 'permissions'], ['format' => 'json']); + $this->drush(ConfigSetCommand::NAME, ['user.role.authenticated', 'permissions', '[foo,bar]'], ['input-format' => 'yaml']); + $this->drush(ConfigGetCommand::NAME, ['user.role.authenticated', 'permissions'], ['format' => 'json']); $output = $this->getOutputFromJSON('user.role.authenticated:permissions'); // Mapping value - $this->drush(ConfigCommands::SET, ['system.site', 'page', "{403: '403', front: home}"], ['input-format' => 'yaml']); - $this->drush(ConfigCommands::GET, ['system.site', 'page'], ['format' => 'json']); + $this->drush(ConfigSetCommand::NAME, ['system.site', 'page', "{403: '403', front: home}"], ['input-format' => 'yaml']); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'page'], ['format' => 'json']); $output = $this->getOutputFromJSON('system.site:page'); $this->assertSame(['403' => '403', 'front' => 'home'], $output); // Multiple top-level keys - $this->drush(ConfigCommands::SET, ['user.role.authenticated', '?', "{label: 'Auth user', weight: 5}"], ['input-format' => 'yaml']); - $this->drush(ConfigCommands::GET, ['user.role.authenticated'], ['format' => 'json']); + $this->drush(ConfigSetCommand::NAME, ['user.role.authenticated', '?', "{label: 'Auth user', weight: 5}"], ['input-format' => 'yaml']); + $this->drush(ConfigGetCommand::NAME, ['user.role.authenticated'], ['format' => 'json']); $output = $this->getOutputFromJSON(); $this->assertSame('Auth user', $output['label']); $this->assertSame(5, $output['weight']); @@ -87,7 +89,7 @@ public function testConfigExportImportStatusExistingConfig() // Test import. $this->drush(ConfigImportCommands::IMPORT); - $this->drush(ConfigCommands::GET, ['system.site', 'page'], ['format' => 'json']); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'page'], ['format' => 'json']); $page = $this->getOutputFromJSON('system.site:page'); $this->assertStringContainsString('unish', $page['front'], 'Config was successfully imported.'); @@ -113,7 +115,7 @@ public function testConfigExportImportStatusExistingConfig() $contents = preg_replace('/front: .*/', 'front: unish existing', $contents); file_put_contents($system_site_yml, $contents); $this->installDrupal('dev', true, ['existing-config' => true], false); - $this->drush(ConfigCommands::GET, ['system.site', 'page'], ['format' => 'json']); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'page'], ['format' => 'json']); $page = $this->getOutputFromJSON('system.site:page'); $this->assertStringContainsString('unish existing', $page['front'], 'Existing config was successfully imported during site:install.'); @@ -124,7 +126,7 @@ public function testConfigExportImportStatusExistingConfig() $this->mkdir($partial_path); $contents = file_put_contents($partial_path . '/system.site.yml', $contents); $this->drush(ConfigImportCommands::IMPORT, [], ['partial' => null, 'source' => $partial_path]); - $this->drush(ConfigCommands::GET, ['system.site', 'page'], ['format' => 'json']); + $this->drush(ConfigGetCommand::NAME, ['system.site', 'page'], ['format' => 'json']); $page = $this->getOutputFromJSON('system.site:page'); $this->assertStringContainsString('unish partial', $page['front'], '--partial was successfully imported.'); } diff --git a/tests/functional/FieldTest.php b/tests/functional/FieldTest.php index 751e97956b..d2c9ec347c 100644 --- a/tests/functional/FieldTest.php +++ b/tests/functional/FieldTest.php @@ -5,7 +5,7 @@ namespace Unish; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drush\Commands\config\ConfigCommands; +use Drush\Commands\config\ConfigGetCommand; use Drush\Commands\core\PhpCommands; use Drush\Commands\field\FieldBaseInfoCommands; use Drush\Commands\field\FieldBaseOverrideCreateCommands; @@ -133,7 +133,7 @@ public function testFieldBaseCreateOverride() 'is-required' => true, ]; $this->drush(FieldBaseOverrideCreateCommands::BASE_OVERRIDE_CREATE, ['user', 'user'], $options); - $this->drush(ConfigCommands::GET, ['core.base_field_override.user.user.name'], ['format' => 'json']); + $this->drush(ConfigGetCommand::NAME, ['core.base_field_override.user.user.name'], ['format' => 'json']); $json = $this->getOutputFromJSON(); $this->assertSame('Handle', $json['label']); $this->assertSame(true, $json['required']);