From 5f1c8e6def859a01ac9cbd8b00b7bbde5fd06629 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 11 Oct 2017 17:22:10 +0200 Subject: [PATCH 01/12] WIP Drush 9: commands are discovered correctly. Converted fs command. --- composer.json | 13 +- drush.services.yml | 8 + src/Commands/FeaturesCommands.php | 237 ++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 drush.services.yml create mode 100644 src/Commands/FeaturesCommands.php diff --git a/composer.json b/composer.json index 9de2a5c..0d5bf6d 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,13 @@ { - "name": "drupal/features", - "description": "Enables administrators to package configuration into modules.", - "type": "drupal-module", + "autoload": { + "psr-4": { + "Drush\\": "src" + } + }, + "description": "Provides drush commands for Features", "license": "GPL-2.0+", "minimum-stability": "dev", - "require": { } + "name": "drupal/features", + "require": { }, + "type": "drupal-module" } diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 0000000..5caae8e --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,8 @@ +services: + features.commands: + class: \Drush\Commands\FeaturesCommands + arguments: + - '@features_assigner' + - '@features.manager' + tags: + - { name: drush.command } diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php new file mode 100644 index 0000000..2473573 --- /dev/null +++ b/src/Commands/FeaturesCommands.php @@ -0,0 +1,237 @@ +assigner = $assigner; + $this->manager = $manager; + } + + /** + * Applies global options for Features drush commands. + * + * The option --name="bundle_name" sets the bundle namespace. + * + * @return \Drupal\features\FeaturesAssignerInterface + */ + protected function featuresOptions(array $options) { + $bundleName = $this->getOption($options, 'bundle'); + if (!empty($bundleName)) { + $bundle = $this->assigner->applyBundle($bundleName); + if ($bundle->getMachineName() !== $bundleName) { + $this->logger()->warning('Bundle {name} not found. Using default.', [ + 'name' => $bundleName, + ]); + } + } + else { + $this->assigner->assignConfigPackages(); + } + return $this->assigner; + } + + protected function getOption(array $options, $name, $default = NULL) { + return isset($options[$name]) + ? $options[$name] + : $default; + } + + /** + * Display current Features settings. + * + * @param string $keys + * A possibly empty, comma-separated, list of config information to display. + * + * @command features:status + * + * @option bundle Use a specific bundle namespace. + * + * @aliases fs,features-status + */ + public function status($keys = NULL, array $options = ['bundle' => null]) { + $this->featuresOptions($options); + + $currentBundle = $this->assigner->getBundle(); + $export_settings = $this->manager->getExportSettings(); + $methods = $this->assigner->getEnabledAssigners(); + if ($currentBundle->isDefault()) { + $this->output()->writeln(dt('Current bundle: none')); + } + else { + $this->output()->writeln(dt('Current bundle: @name (@machine_name)', [ + '@name' => $currentBundle->getName(), + '@machine_name' => $currentBundle->getMachineName(), + ])); + } + $this->output()->writeln(dt('Export folder: @folder', [ + '@folder' => $export_settings['folder'], + ])); + $this->output()->writeln(dt('The following assignment methods are enabled:')); + $this->output()->writeln(dt(' @methods', [ + '@methods' => implode(', ', array_keys($methods)), + ])); + + if (!empty($keys)) { + $config = $this->manager->getConfigCollection(); + $keys = StringUtils::csvToArray($keys); + if (count($keys) > 1) { + $this->output()->writeln(print_r(array_keys($config), TRUE)); + } + else { + $this->output()->writeln($config[$keys[0]], TRUE); + } + } + } + + /** + * Display a list of all existing features and packages available to be generated. If a package name is provided as an argument, then all of the configuration objects assigned to that package will be listed. + * + * @command features:list:packages + * @param $Package The package to list. Optional; if specified, lists all configuration objects assigned to that package. If no package is specified, lists all of the features. + * @option bundle Use a specific bundle namespace. + * @usage drush features-list-packages + * Display a list of all existing featurea and packages available to be generated. + * @usage drush features-list-packages 'example_article' + * Display a list of all configuration objects assigned to the 'example_article' package. + * @aliases fl,features-list-packages + */ + public function listPackages($Package, $options = ['bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + /** + * Import module config from all installed features. + * + * @command features:import:all + * @option bundle Use a specific bundle namespace. + * @usage drush features-import-all + * Import module config from all installed features. + * @aliases fra,fia,fim-all,features-import-all + */ + public function importAll($options = ['bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + /** + * Export the configuration on your site into a custom module. + * + * @command features:export + * @param $Package A space delimited list of features to export. + * @option add-profile Package features into an install profile. + * @option bundle Use a specific bundle namespace. + * @usage drush features-export + * Export all available packages. + * @usage drush features-export example_article example_page + * Export the example_article and example_page packages. + * @usage drush features-export --add-profile + * Export all available packages and add them to an install profile. + * @aliases fex,fu,fua,fu-all,features-export + */ + public function export($Package, $options = ['add-profile' => null, 'bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + /** + * Add a config item to a feature package. + * + * @command features:add + * @param $Feature Feature package to export and add config to. + * @param $Components Patterns of config to add, see features-components for the format of patterns. + * @option bundle Use a specific bundle namespace. + * @aliases fa,fe,features-add + */ + public function add($Feature, $Components, $options = ['bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + /** + * List features components. + * + * @command features:components + * @param $Patterns The features components type to list. Omit this argument to list all components. + * @option exported Show only components that have been exported. + * @option not-exported Show only components that have not been exported. + * @option bundle Use a specific bundle namespace. + * @aliases fc,features-components + */ + public function components($Patterns, $options = ['exported' => null, 'not-exported' => null, 'bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + /** + * Show the difference between the active config and the default config stored in a feature package. + * + * @command features:diff + * @param $Feature The feature in question. + * @option ctypes Comma separated list of component types to limit the output to. Defaults to all types. + * @option lines Generate diffs with lines of context instead of the usual two. + * @option bundle Use a specific bundle namespace. + * @aliases fd,features-diff + */ + public function diff($Feature, $options = ['ctypes' => null, 'lines' => null, 'bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + /** + * Import a module config into your site. + * + * @command features:import + * @param $Feature A space delimited list of features or feature:component pairs to import. + * @option force Force import even if config is not overridden. + * @option bundle Use a specific bundle namespace. + * @usage drush features-import foo:node.type.page foo:taxonomy.vocabulary.tags bar + * Import node and taxonomy config of feature "foo". Import all config of feature "bar". + * @aliases fim,fr,features-import + */ + public function import($Feature, $options = ['force' => null, 'bundle' => null]) + { + // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a + // legacy command. + } + + +} From 754f3555093e8fd247884afc76a40721761316e0 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Tue, 17 Oct 2017 11:35:57 +0200 Subject: [PATCH 02/12] Moshe https://gist.github.com/weitzman/663485af507433e2331d9f2bcb519e42 --- composer.json | 5 - drush.services.yml | 5 +- ...features.drush.inc => features.drush8.inc} | 0 src/Commands/FeaturesCommands.php | 713 ++++++++++++++++-- 4 files changed, 674 insertions(+), 49 deletions(-) rename drush/{features.drush.inc => features.drush8.inc} (100%) diff --git a/composer.json b/composer.json index 0d5bf6d..ba55895 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,4 @@ { - "autoload": { - "psr-4": { - "Drush\\": "src" - } - }, "description": "Provides drush commands for Features", "license": "GPL-2.0+", "minimum-stability": "dev", diff --git a/drush.services.yml b/drush.services.yml index 5caae8e..16b0f29 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -1,8 +1,11 @@ services: features.commands: - class: \Drush\Commands\FeaturesCommands + class: \Drupal\features\Commands\FeaturesCommands arguments: - '@features_assigner' - '@features.manager' + - '@features_generator' + - '@config_update.config_diff' + - '@config.storage' tags: - { name: drush.command } diff --git a/drush/features.drush.inc b/drush/features.drush8.inc similarity index 100% rename from drush/features.drush.inc rename to drush/features.drush8.inc diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 2473573..dec8f1f 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -1,9 +1,17 @@ assigner = $assigner; - $this->manager = $manager; + parent::__construct(); + $this->featuresAssigner = $featuresAssigner; + $this->featuresManager = $featuresManager; + $this->featuresGenerator = $featuresGenerator; + $this->configDiff = $configDiff; + $this->configStorage = $configStorage; } /** @@ -71,6 +91,7 @@ protected function getOption(array $options, $name, $default = NULL) { /** * Display current Features settings. * +<<<<<<< HEAD * @param string $keys * A possibly empty, comma-separated, list of config information to display. * @@ -119,18 +140,60 @@ public function status($keys = NULL, array $options = ['bundle' => null]) { * Display a list of all existing features and packages available to be generated. If a package name is provided as an argument, then all of the configuration objects assigned to that package will be listed. * * @command features:list:packages - * @param $Package The package to list. Optional; if specified, lists all configuration objects assigned to that package. If no package is specified, lists all of the features. + * @param $package_name The package to list. Optional; if specified, lists all configuration objects assigned to that package. If no package is specified, lists all of the features. * @option bundle Use a specific bundle namespace. * @usage drush features-list-packages - * Display a list of all existing featurea and packages available to be generated. + * Display a list of all existing features and packages available to be generated. * @usage drush features-list-packages 'example_article' * Display a list of all configuration objects assigned to the 'example_article' package. + * @field-labels + * name: Name + * machine_name: Machine name + * status: Status + * version: Version + * state: State * @aliases fl,features-list-packages */ - public function listPackages($Package, $options = ['bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function listPackages($package_name = NULL, $options = ['format' => 'table', 'bundle' => null]) { + $assigner = $this->getAssigner($options); + $current_bundle = $assigner->getBundle(); + $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); + + $manager = $this->featuresManager; + $packages = $manager->getPackages(); + + $packages = $manager->filterPackages($packages, $namespace); + $result = array(); + + // If no package was specified, list all packages. + if (empty($package_name)) { + foreach ($packages as $package) { + $overrides = $manager->detectOverrides($package); + $state = $package->getState(); + if (!empty($overrides) && ($package->getStatus() != FeaturesManagerInterface::STATUS_NO_EXPORT)) { + $state = FeaturesManagerInterface::STATE_OVERRIDDEN; + } + + $result[$package->getMachineName()] = array( + 'name' => $package->getName(), + 'machine_name' => $package->getMachineName(), + 'status' => $manager->statusLabel($package->getStatus()), + 'version' => $package->getVersion(), + 'state' => ($state != FeaturesManagerInterface::STATE_DEFAULT) + ? $manager->stateLabel($state) + : '', + ); + } + return new RowsOfFields($result); + } + // If a valid package was listed, list its configuration. + else { + // @todo. I suggest changing this command to return YAML with config as nested values. + } + + // If no matching package found, return an error. + $this->logger()->warning(dt('Package "@package" not found.', array('@package' => $package_name))); + return FALSE; } /** @@ -142,17 +205,37 @@ public function listPackages($Package, $options = ['bundle' => null]) * Import module config from all installed features. * @aliases fra,fia,fim-all,features-import-all */ - public function importAll($options = ['bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function importAll($options = ['bundle' => null]) { + $assigner = $this->getAssigner($options); + $current_bundle = $assigner->getBundle(); + $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); + + $manager = $this->featuresManager; + $packages = $manager->getPackages(); + $packages = $manager->filterPackages($packages, $namespace); + $overridden = array(); + + foreach ($packages as $package) { + $overrides = $manager->detectOverrides($package); + $missing = $manager->detectMissing($package); + if ((!empty($missing) || !empty($overrides)) && ($package->getStatus() == FeaturesManagerInterface::STATUS_INSTALLED)) { + $overridden[] = $package->getMachineName(); + } + } + + if (!empty($overridden)) { + $this->import($overridden); + } + else { + $this->logger->info(dt('Current state already matches active config, aborting.')); + } } /** * Export the configuration on your site into a custom module. * * @command features:export - * @param $Package A space delimited list of features to export. + * @param $packages A space delimited list of features to export. * @option add-profile Package features into an install profile. * @option bundle Use a specific bundle namespace. * @usage drush features-export @@ -163,75 +246,619 @@ public function importAll($options = ['bundle' => null]) * Export all available packages and add them to an install profile. * @aliases fex,fu,fua,fu-all,features-export */ - public function export($Package, $options = ['add-profile' => null, 'bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function export(array $packages, $options = ['add-profile' => null, 'bundle' => null]) { + $assigner = $this->getAssigner($options); + $manager = $this->featuresManager; + $generator = $this->featuresGenerator; + + $current_bundle = $assigner->getBundle(); + + if ($options['add-profile']) { + if ($current_bundle->isDefault) { + throw new \Exception((dt("Must specify a profile name with --name"))); + } + $current_bundle->setIsProfile(TRUE); + } + + $all_packages = $manager->getPackages(); + foreach ($packages as $name) { + if (!isset($all_packages[$name])) { + throw new \Exception(dt("The package @name does not exist.", array('@name' => $name))); + } + } + + if (empty($packages)) { + $packages = $all_packages; + $dt_args = array('@modules' => implode(', ', array_keys($packages))); + drush_print(dt('The following extensions will be exported: @modules', $dt_args)); + if (!$this->io()->confirm('Do you really want to continue?')) { + throw new UserAbortException(); + } + } + + // If any packages exist, confirm before overwriting. + if ($existing_packages = $manager->listPackageDirectories($packages, $current_bundle)) { + foreach ($existing_packages as $name => $directory) { + drush_print(dt("The extension @name already exists at @directory.", array('@name' => $name, '@directory' => $directory))); + } + // Apparently, format_plural is not always available. + if (count($existing_packages) == 1) { + $message = dt('Would you like to overwrite it?'); + } + else { + $message = dt('Would you like to overwrite them?'); + } + if (!$this->io()->confirm($message)) { + throw new UserAbortException(); + } + } + + // Use the write generation method. + $method_id = FeaturesGenerationWrite::METHOD_ID; + $result = $generator->generatePackages($method_id, $current_bundle, array_keys($packages)); + + foreach ($result as $message) { + $method = $message['success'] ? 'success' : 'error'; + $this->logger()->$method(dt($message['message'], $message['variables'])); + } } /** * Add a config item to a feature package. * * @command features:add - * @param $Feature Feature package to export and add config to. - * @param $Components Patterns of config to add, see features-components for the format of patterns. + * @todo @param $feature Feature package to export and add config to. + * @param $components Patterns of config to add, see features-components for the format of patterns. * @option bundle Use a specific bundle namespace. * @aliases fa,fe,features-add */ - public function add($Feature, $Components, $options = ['bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function add($components = null, $options = ['bundle' => null]) { + if ($components) { + $assigner = $this->getAssigner($options); + $manager = $this->featuresManager; + $generator = $this->featuresGenerator; + + $current_bundle = $assigner->getBundle(); + + $module = array_shift($args); + if (empty($args)) { + throw new \Exception('No components supplied.'); + } + $components = $this->componentList(); + $options = array( + 'exported' => FALSE, + ); + + $filtered_components = $this->componentFilter($components, $args, $options); + $items = $filtered_components['components']; + + if (empty($items)) { + throw new \Exception('No components to add.'); + } + + $packages = array($module); + // If any packages exist, confirm before overwriting. + if ($existing_packages = $manager->listPackageDirectories($packages)) { + foreach ($existing_packages as $name => $directory) { + drush_print(dt("The extension @name already exists at @directory.", array('@name' => $name, '@directory' => $directory))); + } + // Apparently, format_plural is not always available. + if (count($existing_packages) == 1) { + $message = dt('Would you like to overwrite it?'); + } + else { + $message = dt('Would you like to overwrite them?'); + } + if (!$this->io()->confirm($message)) { + throw new UserAbortException(); + } + } + else { + $package = $manager->initPackage($module, NULL, '', 'module', $current_bundle); + list($full_name, $path) = $manager->getExportInfo($package, $current_bundle); + drush_print(dt('Will create a new extension @name in @directory', array('@name' => $full_name, '@directory' => $path))); + if (!$this->io()->confirm(dt('Do you really want to continue?'))) { + throw new UserAbortException(); + } + } + + $config = $this->buildConfig($items); + + $manager->assignConfigPackage($module, $config); + + // Use the write generation method. + $method_id = FeaturesGenerationWrite::METHOD_ID; + $result = $generator->generatePackages($method_id, $current_bundle, $packages); + + foreach ($result as $message) { + $method = $message['success'] ? 'success' : 'error'; + $this->logger()->$method(dt($message['message'], $message['variables'])); + } + } + else { + throw new \Exception('No feature name given.'); + } } /** * List features components. * * @command features:components - * @param $Patterns The features components type to list. Omit this argument to list all components. + * @param $patterns The features components type to list. Omit this argument to list all components. * @option exported Show only components that have been exported. * @option not-exported Show only components that have not been exported. * @option bundle Use a specific bundle namespace. * @aliases fc,features-components + * @field-labels + * source: Available sources + * + * @return RowsOfFields */ - public function components($Patterns, $options = ['exported' => null, 'not-exported' => null, 'bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function components(array $patterns, $options = ['format' => 'table', 'exported' => null, 'not-exported' => null, 'bundle' => null]) { + $args = $patterns; + $assigner = $this->getAssigner($options); + + $components = $this->componentList(); + ksort($components); + // If no args supplied, prompt with a list. + if (empty($args)) { + $types = array_keys($components); + array_unshift($types, 'all'); + $choice = $this->io()->choice('Enter a number to choose which component type to list.', $types); + if ($choice === FALSE) { + return; + } + + $args = ($choice == 0) ? array('*') : array($types[$choice]); + } + $options = array( + 'provided by' => TRUE, + ); + if ($options['exported']) { + $options['not exported'] = FALSE; + } + elseif ($options['not-exported']) { + $options['exported'] = FALSE; + } + + $filtered_components = $this->componentFilter($components, $args, $options); + if ($filtered_components) { + return $this->componentPrint($filtered_components); + } } /** * Show the difference between the active config and the default config stored in a feature package. * * @command features:diff - * @param $Feature The feature in question. + * @param $feature The feature in question. * @option ctypes Comma separated list of component types to limit the output to. Defaults to all types. * @option lines Generate diffs with lines of context instead of the usual two. * @option bundle Use a specific bundle namespace. * @aliases fd,features-diff */ - public function diff($Feature, $options = ['ctypes' => null, 'lines' => null, 'bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'bundle' => null]) { + $manager = $this->featuresManager; + $assigner = $this->getAssigner($options); + $assigner->assignConfigPackages(); + + $module = $feature; + $filter_ctypes = $options["ctypes"]; + if ($filter_ctypes) { + $filter_ctypes = explode(',', $filter_ctypes); + } + + $feature = $manager->loadPackage($module, TRUE); + if (empty($feature)) { + throw new \Exception(dt('No such feature is available: @module', array('@module' => $module))); + } + + $lines = $options['lines']; + $lines = isset($lines) ? $lines : 2; + + $formatter = new DiffFormatter(); + $formatter->leading_context_lines = $lines; + $formatter->trailing_context_lines = $lines; + $formatter->show_header = FALSE; + + if (drush_get_context('DRUSH_NOCOLOR')) { + $red = $green = "%s"; + } + else { + $red = "\033[31;40m\033[1m%s\033[0m"; + $green = "\033[0;32;40m\033[1m%s\033[0m"; + } + + $overrides = $manager->detectOverrides($feature); + $missing = $manager->reorderMissing($manager->detectMissing($feature)); + $overrides = array_merge($overrides, $missing); + + if (empty($overrides)) { + drush_print(dt('Active config matches stored config for @module.', array('@module' => $module))); + } + else { + $config_diff = $this->configDiff; + $active_storage = $this->configStorage; + + // Print key for colors. + drush_print(dt('Legend: ')); + drush_print(sprintf($red, dt('Code: drush features-import will replace the active config with the displayed code.'))); + drush_print(sprintf($green, dt('Active: drush features-export will update the exported feature with the displayed active config'))); + + foreach ($overrides as $name) { + $message = ''; + if (in_array($name, $missing)) { + $message = sprintf($red, t('(missing from active)')); + $extension = array(); + } else { + $active = $manager->getActiveStorage()->read($name); + $extension = $manager->getExtensionStorages()->read($name); + if (empty($extension)) { + $extension = array(); + $message = sprintf($green, t('(not exported)')); + } + $diff = $config_diff->diff($extension, $active); + $rows = explode("\n", $formatter->format($diff)); + } + drush_print(); + drush_print(dt("Config @name @message", array('@name' => $name, '@message' => $message))); + if (!empty($extension)) { + foreach ($rows as $row) { + if (strpos($row, '>') === 0) { + drush_print(sprintf($green, $row)); + } elseif (strpos($row, '<') === 0) { + drush_print(sprintf($red, $row)); + } else { + drush_print($row); + } + } + } + } + } } /** * Import a module config into your site. * * @command features:import - * @param $Feature A space delimited list of features or feature:component pairs to import. + * @param $feature A space delimited list of features or feature:component pairs to import. * @option force Force import even if config is not overridden. * @option bundle Use a specific bundle namespace. * @usage drush features-import foo:node.type.page foo:taxonomy.vocabulary.tags bar * Import node and taxonomy config of feature "foo". Import all config of feature "bar". * @aliases fim,fr,features-import */ - public function import($Feature, $options = ['force' => null, 'bundle' => null]) - { - // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a - // legacy command. + public function import(array $feature, $options = ['force' => null, 'bundle' => null]) { + if ($feature) { + // Determine if revert should be forced. + $force = $options['force']; + // Determine if -y was supplied. If so, we can filter out needless output + // from this command. + $skip_confirmation = drush_get_context('DRUSH_AFFIRMATIVE'); + $manager = $this->featuresManager; + + // Parse list of arguments. + $modules = array(); + foreach ($feature as $arg) { + $arg = explode(':', $arg); + $module = array_shift($arg); + $component = array_shift($arg); + + if (isset($module)) { + if (empty($component)) { + // If we received just a feature name, this means that we need all of + // its components. + $modules[$module] = TRUE; + } elseif ($modules[$module] !== TRUE) { + if (!isset($modules[$module])) { + $modules[$module] = array(); + } + $modules[$module][] = $component; + } + } + } + + // Process modules. + foreach ($modules as $module => $components_needed) { + + $dt_args['@module'] = $module; + /** @var \Drupal\features\Package $feature */ + $feature = $manager->loadPackage($module, TRUE); + if (empty($feature)) { + throw new \Exception(dt('No such feature is available: @module', $dt_args)); + } + + if ($feature->getStatus() != FeaturesManagerInterface::STATUS_INSTALLED) { + throw new \Exception(dt('No such feature is installed: @module', $dt_args)); + } + + // Forcefully revert all components of a feature. + if ($force) { + $components = $feature->getConfigOrig(); + } // Only revert components that are detected to be Overridden. + else { + $components = $manager->detectOverrides($feature); + $missing = $manager->reorderMissing($manager->detectMissing($feature)); + // Be sure to import missing components first. + $components = array_merge($missing, $components); + } + + if (!empty($components_needed) && is_array($components_needed)) { + $components = array_intersect($components, $components_needed); + } + + if (empty($components)) { + $this->logger()->info(dt('Current state already matches active config, aborting.')); + } else { + // Determine which config the user wants to import/revert. + $config_to_create = []; + foreach ($components as $component) { + $dt_args['@component'] = $component; + $confirmation_message = 'Do you really want to import @module : @component?'; + if ($skip_confirmation || $this->io()->confirm(dt($confirmation_message, $dt_args))) { + $config_to_create[$component] = ''; + } + } + + // Perform the import/revert. + $config_imported = $manager->createConfiguration($config_to_create); + + // List the results. + foreach ($components as $component) { + $dt_args['@component'] = $component; + if (isset($config_imported['new'][$component])) { + $this->logger()->info(dt('Imported @module : @component.', $dt_args)); + } elseif (isset($config_imported['updated'][$component])) { + $this->logger()->info(dt('Reverted @module : @component.', $dt_args)); + } elseif (!isset($config_to_create[$component])) { + $this->logger()->info(dt('Skipping @module : @component.', $dt_args)); + } else { + $this->logger()->error(dt('Error importing @module : @component.', $dt_args)); + } + } + } + } + } + else { + drush_invoke_process('@self', 'features-list-packages', [], $options); + } + } + + public function getAssigner($options) { + $assigner = $this->featuresAssigner; + $bundle_name = $options['bundle']; + if (!empty($bundle_name)) { + $bundle = $assigner->applyBundle($bundle_name); + if ($bundle->getMachineName() != $bundle_name) { + $this->logger()->warning(dt('Bundle @name not found. Using default.', array('@name' => $bundle_name))); + } + } + else { + $assigner->assignConfigPackages(); + } + return $assigner; + } + + /** + * Returns an array of full config names given a array[$type][$component]. + * + * @param array $items + * The items to return data for. + */ + function buildConfig(array $items) { + $result = array(); + foreach ($items as $config_type => $item) { + foreach ($item as $item_name => $title) { + $result[] = $this->featuresManager->getFullName($config_type, $item_name); + } + } + return $result; + } + + /** + * Returns a listing of all known components, indexed by source. + */ + function componentList() { + $result = array(); + $config = $this->featuresManager->getConfigCollection(); + foreach ($config as $item_name => $item) { + $result[$item->getType()][$item->getShortName()] = $item->getLabel(); + } + return $result; } + /** + * Filters components by patterns. + */ + function componentFilter($all_components, $patterns = array(), $options = array()) { + $options += array( + 'exported' => TRUE, + 'not exported' => TRUE, + 'provided by' => FALSE, + ); + $pool = array(); + // Maps exported components to feature modules. + $components_map = $this->componentMap(); + // First filter on exported state. + foreach ($all_components as $source => $components) { + foreach ($components as $name => $title) { + $exported = count($components_map[$source][$name]) > 0; + if ($exported) { + if ($options['exported']) { + $pool[$source][$name] = $title; + } + } + else { + if ($options['not exported']) { + $pool[$source][$name] = $title; + } + } + } + } + + $state_string = ''; + + if (!$options['exported']) { + $state_string = 'unexported'; + } + elseif (!$options['not exported']) { + $state_string = 'exported'; + } + + $selected = array(); + foreach ($patterns as $pattern) { + // Rewrite * to %. Let users use both as wildcard. + $pattern = strtr($pattern, array('*' => '%')); + $sources = array(); + list($source_pattern, $component_pattern) = explode(':', $pattern, 2); + // If source is empty, use a pattern. + if ($source_pattern == '') { + $source_pattern = '%'; + } + if ($component_pattern == '') { + $component_pattern = '%'; + } + + $preg_source_pattern = strtr(preg_quote($source_pattern, '/'), array('%' => '.*')); + $preg_component_pattern = strtr(preg_quote($component_pattern, '/'), array('%' => '.*')); + // If it isn't a pattern, but a simple string, we don't anchor the + // pattern. This allows for abbreviating. Otherwise, we do, as this seems + // more natural for patterns. + if (strpos($source_pattern, '%') !== FALSE) { + $preg_source_pattern = '^' . $preg_source_pattern . '$'; + } + if (strpos($component_pattern, '%') !== FALSE) { + $preg_component_pattern = '^' . $preg_component_pattern . '$'; + } + $matches = array(); + + // Find the sources. + $all_sources = array_keys($pool); + $matches = preg_grep('/' . $preg_source_pattern . '/', $all_sources); + if (count($matches) > 0) { + // If we have multiple matches and the source string wasn't a + // pattern, check if one of the matches is equal to the pattern, and + // use that, or error out. + if (count($matches) > 1 and $preg_source_pattern[0] != '^') { + if (in_array($source_pattern, $matches)) { + $matches = array($source_pattern); + } + else { + throw new \Exception(dt('Ambiguous source "@source", matches @matches', array( + '@source' => $source_pattern, + '@matches' => implode(', ', $matches), + ))); + } + } + // Loose the indexes preg_grep preserved. + $sources = array_values($matches); + } + else { + throw new \Exception(dt('No @state sources match "@source"', array('@state' => $state_string, '@source' => $source_pattern))); + } + + // Now find the components. + foreach ($sources as $source) { + // Find the components. + $all_components = array_keys($pool[$source]); + // See if there's any matches. + $matches = preg_grep('/' . $preg_component_pattern . '/', $all_components); + if (count($matches) > 0) { + // If we have multiple matches and the components string wasn't a + // pattern, check if one of the matches is equal to the pattern, and + // use that, or error out. + if (count($matches) > 1 and $preg_component_pattern[0] != '^') { + if (in_array($component_pattern, $matches)) { + $matches = array($component_pattern); + } + else { + throw new \Exception(dt('Ambiguous component "@component", matches @matches', array( + '@component' => $component_pattern, + '@matches' => implode(', ', $matches), + ))); + } + } + if (!is_array($selected[$source])) { + $selected[$source] = array(); + } + $selected[$source] += array_intersect_key($pool[$source], array_flip($matches)); + } + else { + // No matches. If the source was a pattern, just carry on, else + // error out. Allows for patterns like :*field* + if ($preg_source_pattern[0] != '^') { + throw new \Exception(dt('No @state @source components match "@component"', array( + '@state' => $state_string, + '@component' => $component_pattern, + '@source' => $source, + ))); + } + } + } + } + + // Lastly, provide feature module information on the selected components, if + // requested. + $provided_by = array(); + if ($options['provided by'] && $options['exported']) { + foreach ($selected as $source => $components) { + foreach ($components as $name => $title) { + $exported = count($components_map[$source][$name]) > 0; + if ($exported) { + $provided_by[$source . ':' . $name] = implode(', ', $components_map[$source][$name]); + } + } + } + } + + return array( + 'components' => $selected, + 'sources' => $provided_by, + ); + } + + /** + * Provides a component to feature map (port of features_get_component_map). + */ + function componentMap() { + $result = array(); + $manager = $this->featuresManager; + // Recalc full config list without running assignments. + $config = $manager->getConfigCollection(); + $packages = $manager->getPackages(); + + foreach ($config as $item_name => $item) { + $type = $item->getType(); + $short_name = $item->getShortName(); + $name = $item->getName(); + if (!isset($result[$type][$short_name])) { + $result[$type][$short_name] = array(); + } + if (!empty($item->getPackage())) { + $package = $packages[$item->getPackage()]; + $result[$type][$short_name][] = $package->getMachineName(); + } + } + + return $result; + } + + /** + * Prints a list of filtered components. + */ + function componentPrint($filtered_components) { + $rows = []; + foreach ($filtered_components['components'] as $source => $components) { + foreach ($components as $name => $value) { + $row = array('source' => $source . ':' . $name); + if (isset($filtered_components['sources'][$source . ':' . $name])) { + $row['source'] = dt('Provided by') . ': ' . $filtered_components['sources'][$source . ':' . $name]; + } + $rows[] = $row; + } + } + + return new RowsOfFields($rows); + } } From b2415bbdf1b73dff7295e9b3501178451dfc10e9 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Tue, 17 Oct 2017 14:53:39 +0200 Subject: [PATCH 03/12] Cleanup: fs command. --- src/Commands/FeaturesCommands.php | 76 +++++++++++++++++++------------ 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index dec8f1f..e2c1bb8 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -24,43 +24,62 @@ class FeaturesCommands extends DrushCommands { * * @var \Drupal\features\FeaturesAssignerInterface */ - protected $featuresAssigner; + protected $assigner; /** * The features.manager service. * * @var \Drupal\features\FeaturesManagerInterface */ - protected $featuresManager; + protected $manager; - protected $featuresGenerator; + /** + * The features_generator service. + * + * @var \Drupal\features\FeaturesGeneratorInterface + */ + protected $generator; + + /** + * The config_update.config_diff service. + * + * @var \Drupal\config_update\ConfigDiffInterface + */ protected $configDiff; - protected $configStorage; + /** + * The config.storage service. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $configStorage; /** * FeaturesCommands constructor. * * @param \Drupal\features\FeaturesAssignerInterface $assigner * @param \Drupal\features\FeaturesManagerInterface $manager + * @param \Drupal\features\FeaturesGeneratorInterface $generator + * @param \Drupal\config_update\ConfigDiffInterface $configDiff + * @param \Drupal\Core\Config\StorageInterface $configStorage */ public function __construct( - FeaturesAssignerInterface $featuresAssigner, - FeaturesManagerInterface $featuresManager, - FeaturesGeneratorInterface $featuresGenerator, + FeaturesAssignerInterface $assigner, + FeaturesManagerInterface $manager, + FeaturesGeneratorInterface $generator, ConfigDiffInterface $configDiff, StorageInterface $configStorage ) { parent::__construct(); - $this->featuresAssigner = $featuresAssigner; - $this->featuresManager = $featuresManager; - $this->featuresGenerator = $featuresGenerator; + $this->assigner = $assigner; $this->configDiff = $configDiff; $this->configStorage = $configStorage; + $this->generator = $generator; + $this->manager = $manager; } /** - * Applies global options for Features drush commands. + * Applies global options for Features drush commands, including the bundle. * * The option --name="bundle_name" sets the bundle namespace. * @@ -91,7 +110,6 @@ protected function getOption(array $options, $name, $default = NULL) { /** * Display current Features settings. * -<<<<<<< HEAD * @param string $keys * A possibly empty, comma-separated, list of config information to display. * @@ -127,12 +145,10 @@ public function status($keys = NULL, array $options = ['bundle' => null]) { if (!empty($keys)) { $config = $this->manager->getConfigCollection(); $keys = StringUtils::csvToArray($keys); - if (count($keys) > 1) { - $this->output()->writeln(print_r(array_keys($config), TRUE)); - } - else { - $this->output()->writeln($config[$keys[0]], TRUE); - } + $data = count($keys) > 1 + ? array_keys($config) + : $config[$keys[0]]; + $this->output()->writeln(print_r($data, TRUE)); } } @@ -159,7 +175,7 @@ public function listPackages($package_name = NULL, $options = ['format' => 'tabl $current_bundle = $assigner->getBundle(); $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); - $manager = $this->featuresManager; + $manager = $this->manager; $packages = $manager->getPackages(); $packages = $manager->filterPackages($packages, $namespace); @@ -210,7 +226,7 @@ public function importAll($options = ['bundle' => null]) { $current_bundle = $assigner->getBundle(); $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); - $manager = $this->featuresManager; + $manager = $this->manager; $packages = $manager->getPackages(); $packages = $manager->filterPackages($packages, $namespace); $overridden = array(); @@ -248,8 +264,8 @@ public function importAll($options = ['bundle' => null]) { */ public function export(array $packages, $options = ['add-profile' => null, 'bundle' => null]) { $assigner = $this->getAssigner($options); - $manager = $this->featuresManager; - $generator = $this->featuresGenerator; + $manager = $this->manager; + $generator = $this->generator; $current_bundle = $assigner->getBundle(); @@ -315,8 +331,8 @@ public function export(array $packages, $options = ['add-profile' => null, 'bund public function add($components = null, $options = ['bundle' => null]) { if ($components) { $assigner = $this->getAssigner($options); - $manager = $this->featuresManager; - $generator = $this->featuresGenerator; + $manager = $this->manager; + $generator = $this->generator; $current_bundle = $assigner->getBundle(); @@ -438,7 +454,7 @@ public function components(array $patterns, $options = ['format' => 'table', 'ex * @aliases fd,features-diff */ public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'bundle' => null]) { - $manager = $this->featuresManager; + $manager = $this->manager; $assigner = $this->getAssigner($options); $assigner->assignConfigPackages(); @@ -535,7 +551,7 @@ public function import(array $feature, $options = ['force' => null, 'bundle' => // Determine if -y was supplied. If so, we can filter out needless output // from this command. $skip_confirmation = drush_get_context('DRUSH_AFFIRMATIVE'); - $manager = $this->featuresManager; + $manager = $this->manager; // Parse list of arguments. $modules = array(); @@ -625,7 +641,7 @@ public function import(array $feature, $options = ['force' => null, 'bundle' => } public function getAssigner($options) { - $assigner = $this->featuresAssigner; + $assigner = $this->assigner; $bundle_name = $options['bundle']; if (!empty($bundle_name)) { $bundle = $assigner->applyBundle($bundle_name); @@ -649,7 +665,7 @@ function buildConfig(array $items) { $result = array(); foreach ($items as $config_type => $item) { foreach ($item as $item_name => $title) { - $result[] = $this->featuresManager->getFullName($config_type, $item_name); + $result[] = $this->manager->getFullName($config_type, $item_name); } } return $result; @@ -660,7 +676,7 @@ function buildConfig(array $items) { */ function componentList() { $result = array(); - $config = $this->featuresManager->getConfigCollection(); + $config = $this->manager->getConfigCollection(); foreach ($config as $item_name => $item) { $result[$item->getType()][$item->getShortName()] = $item->getLabel(); } @@ -822,7 +838,7 @@ function componentFilter($all_components, $patterns = array(), $options = array( */ function componentMap() { $result = array(); - $manager = $this->featuresManager; + $manager = $this->manager; // Recalc full config list without running assignments. $config = $manager->getConfigCollection(); $packages = $manager->getPackages(); From 1f0301c13023ca949c395d1509724936ace8877b Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Tue, 17 Oct 2017 15:49:26 +0200 Subject: [PATCH 04/12] fr / fra commands. --- src/Commands/FeaturesCommands.php | 494 ++++++++++++++++++------------ src/Exception/DomainException.php | 7 + 2 files changed, 305 insertions(+), 196 deletions(-) create mode 100644 src/Exception/DomainException.php diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index e2c1bb8..0f2efd2 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -6,6 +6,7 @@ use Drupal\Component\Diff\DiffFormatter; use Drupal\config_update\ConfigDiffInterface; use Drupal\Core\Config\StorageInterface; +use Drupal\features\Exception\DomainException; use Drupal\features\FeaturesAssignerInterface; use Drupal\features\FeaturesGeneratorInterface; use Drupal\features\FeaturesManagerInterface; @@ -84,6 +85,7 @@ public function __construct( * The option --name="bundle_name" sets the bundle namespace. * * @return \Drupal\features\FeaturesAssignerInterface + * The features.assigner with options applied. */ protected function featuresOptions(array $options) { $bundleName = $this->getOption($options, 'bundle'); @@ -101,6 +103,18 @@ protected function featuresOptions(array $options) { return $this->assigner; } + /** + * Get the value of an option. + * + * @param array $options + * The options array. + * @param string $name + * The option name. + * @param mixed $default + * The default value of the option. + * + * @return mixed|null + */ protected function getOption(array $options, $name, $default = NULL) { return isset($options[$name]) ? $options[$name] @@ -119,7 +133,7 @@ protected function getOption(array $options, $name, $default = NULL) { * * @aliases fs,features-status */ - public function status($keys = NULL, array $options = ['bundle' => null]) { + public function status($keys = NULL, array $options = ['bundle' => NULL]) { $this->featuresOptions($options); $currentBundle = $this->assigner->getBundle(); @@ -137,7 +151,8 @@ public function status($keys = NULL, array $options = ['bundle' => null]) { $this->output()->writeln(dt('Export folder: @folder', [ '@folder' => $export_settings['folder'], ])); - $this->output()->writeln(dt('The following assignment methods are enabled:')); + $this->output() + ->writeln(dt('The following assignment methods are enabled:')); $this->output()->writeln(dt(' @methods', [ '@methods' => implode(', ', array_keys($methods)), ])); @@ -153,15 +168,23 @@ public function status($keys = NULL, array $options = ['bundle' => null]) { } /** - * Display a list of all existing features and packages available to be generated. If a package name is provided as an argument, then all of the configuration objects assigned to that package will be listed. + * Display a list of all existing features and packages available to be + * generated. If a package name is provided as an argument, then all of the + * configuration objects assigned to that package will be listed. * * @command features:list:packages - * @param $package_name The package to list. Optional; if specified, lists all configuration objects assigned to that package. If no package is specified, lists all of the features. + * + * @param $package_name The package to list. Optional; if specified, lists + * all configuration objects assigned to that package. If no package is + * specified, lists all of the features. + * * @option bundle Use a specific bundle namespace. * @usage drush features-list-packages - * Display a list of all existing features and packages available to be generated. + * Display a list of all existing features and packages available to be + * generated. * @usage drush features-list-packages 'example_article' - * Display a list of all configuration objects assigned to the 'example_article' package. + * Display a list of all configuration objects assigned to the + * 'example_article' package. * @field-labels * name: Name * machine_name: Machine name @@ -170,8 +193,11 @@ public function status($keys = NULL, array $options = ['bundle' => null]) { * state: State * @aliases fl,features-list-packages */ - public function listPackages($package_name = NULL, $options = ['format' => 'table', 'bundle' => null]) { - $assigner = $this->getAssigner($options); + public function listPackages( + $package_name = NULL, + $options = ['format' => 'table', 'bundle' => NULL] + ) { + $assigner = $this->featuresOptions($options); $current_bundle = $assigner->getBundle(); $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); @@ -179,7 +205,7 @@ public function listPackages($package_name = NULL, $options = ['format' => 'tabl $packages = $manager->getPackages(); $packages = $manager->filterPackages($packages, $namespace); - $result = array(); + $result = []; // If no package was specified, list all packages. if (empty($package_name)) { @@ -190,7 +216,7 @@ public function listPackages($package_name = NULL, $options = ['format' => 'tabl $state = FeaturesManagerInterface::STATE_OVERRIDDEN; } - $result[$package->getMachineName()] = array( + $result[$package->getMachineName()] = [ 'name' => $package->getName(), 'machine_name' => $package->getMachineName(), 'status' => $manager->statusLabel($package->getStatus()), @@ -198,7 +224,7 @@ public function listPackages($package_name = NULL, $options = ['format' => 'tabl 'state' => ($state != FeaturesManagerInterface::STATE_DEFAULT) ? $manager->stateLabel($state) : '', - ); + ]; } return new RowsOfFields($result); } @@ -208,7 +234,8 @@ public function listPackages($package_name = NULL, $options = ['format' => 'tabl } // If no matching package found, return an error. - $this->logger()->warning(dt('Package "@package" not found.', array('@package' => $package_name))); + $this->logger()->warning(dt('Package "@package" not found.', + ['@package' => $package_name])); return FALSE; } @@ -216,20 +243,23 @@ public function listPackages($package_name = NULL, $options = ['format' => 'tabl * Import module config from all installed features. * * @command features:import:all + * * @option bundle Use a specific bundle namespace. + * * @usage drush features-import-all * Import module config from all installed features. + * * @aliases fra,fia,fim-all,features-import-all */ - public function importAll($options = ['bundle' => null]) { - $assigner = $this->getAssigner($options); - $current_bundle = $assigner->getBundle(); - $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); + public function importAll($options = ['bundle' => NULL]) { + $assigner = $this->featuresOptions($options); + $currentBundle = $assigner->getBundle(); + $namespace = $currentBundle->isDefault() ? '' : $currentBundle->getMachineName(); $manager = $this->manager; $packages = $manager->getPackages(); $packages = $manager->filterPackages($packages, $namespace); - $overridden = array(); + $overridden = []; foreach ($packages as $package) { $overrides = $manager->detectOverrides($package); @@ -251,7 +281,9 @@ public function importAll($options = ['bundle' => null]) { * Export the configuration on your site into a custom module. * * @command features:export + * * @param $packages A space delimited list of features to export. + * * @option add-profile Package features into an install profile. * @option bundle Use a specific bundle namespace. * @usage drush features-export @@ -262,8 +294,11 @@ public function importAll($options = ['bundle' => null]) { * Export all available packages and add them to an install profile. * @aliases fex,fu,fua,fu-all,features-export */ - public function export(array $packages, $options = ['add-profile' => null, 'bundle' => null]) { - $assigner = $this->getAssigner($options); + public function export( + array $packages, + $options = ['add-profile' => NULL, 'bundle' => NULL] + ) { + $assigner = $this->featuresOptions($options); $manager = $this->manager; $generator = $this->generator; @@ -279,23 +314,27 @@ public function export(array $packages, $options = ['add-profile' => null, 'bund $all_packages = $manager->getPackages(); foreach ($packages as $name) { if (!isset($all_packages[$name])) { - throw new \Exception(dt("The package @name does not exist.", array('@name' => $name))); + throw new \Exception(dt("The package @name does not exist.", + ['@name' => $name])); } } if (empty($packages)) { $packages = $all_packages; - $dt_args = array('@modules' => implode(', ', array_keys($packages))); - drush_print(dt('The following extensions will be exported: @modules', $dt_args)); + $dt_args = ['@modules' => implode(', ', array_keys($packages))]; + drush_print(dt('The following extensions will be exported: @modules', + $dt_args)); if (!$this->io()->confirm('Do you really want to continue?')) { throw new UserAbortException(); } } // If any packages exist, confirm before overwriting. - if ($existing_packages = $manager->listPackageDirectories($packages, $current_bundle)) { + if ($existing_packages = $manager->listPackageDirectories($packages, + $current_bundle)) { foreach ($existing_packages as $name => $directory) { - drush_print(dt("The extension @name already exists at @directory.", array('@name' => $name, '@directory' => $directory))); + drush_print(dt("The extension @name already exists at @directory.", + ['@name' => $name, '@directory' => $directory])); } // Apparently, format_plural is not always available. if (count($existing_packages) == 1) { @@ -311,7 +350,8 @@ public function export(array $packages, $options = ['add-profile' => null, 'bund // Use the write generation method. $method_id = FeaturesGenerationWrite::METHOD_ID; - $result = $generator->generatePackages($method_id, $current_bundle, array_keys($packages)); + $result = $generator->generatePackages($method_id, $current_bundle, + array_keys($packages)); foreach ($result as $message) { $method = $message['success'] ? 'success' : 'error'; @@ -324,13 +364,16 @@ public function export(array $packages, $options = ['add-profile' => null, 'bund * * @command features:add * @todo @param $feature Feature package to export and add config to. - * @param $components Patterns of config to add, see features-components for the format of patterns. + * + * @param $components Patterns of config to add, see features-components for + * the format of patterns. + * * @option bundle Use a specific bundle namespace. * @aliases fa,fe,features-add */ - public function add($components = null, $options = ['bundle' => null]) { + public function add($components = NULL, $options = ['bundle' => NULL]) { if ($components) { - $assigner = $this->getAssigner($options); + $assigner = $this->featuresOptions($options); $manager = $this->manager; $generator = $this->generator; @@ -341,22 +384,24 @@ public function add($components = null, $options = ['bundle' => null]) { throw new \Exception('No components supplied.'); } $components = $this->componentList(); - $options = array( + $options = [ 'exported' => FALSE, - ); + ]; - $filtered_components = $this->componentFilter($components, $args, $options); + $filtered_components = $this->componentFilter($components, $args, + $options); $items = $filtered_components['components']; if (empty($items)) { throw new \Exception('No components to add.'); } - $packages = array($module); + $packages = [$module]; // If any packages exist, confirm before overwriting. if ($existing_packages = $manager->listPackageDirectories($packages)) { foreach ($existing_packages as $name => $directory) { - drush_print(dt("The extension @name already exists at @directory.", array('@name' => $name, '@directory' => $directory))); + drush_print(dt("The extension @name already exists at @directory.", + ['@name' => $name, '@directory' => $directory])); } // Apparently, format_plural is not always available. if (count($existing_packages) == 1) { @@ -370,9 +415,12 @@ public function add($components = null, $options = ['bundle' => null]) { } } else { - $package = $manager->initPackage($module, NULL, '', 'module', $current_bundle); - list($full_name, $path) = $manager->getExportInfo($package, $current_bundle); - drush_print(dt('Will create a new extension @name in @directory', array('@name' => $full_name, '@directory' => $path))); + $package = $manager->initPackage($module, NULL, '', 'module', + $current_bundle); + list($full_name, $path) = $manager->getExportInfo($package, + $current_bundle); + drush_print(dt('Will create a new extension @name in @directory', + ['@name' => $full_name, '@directory' => $path])); if (!$this->io()->confirm(dt('Do you really want to continue?'))) { throw new UserAbortException(); } @@ -384,11 +432,13 @@ public function add($components = null, $options = ['bundle' => null]) { // Use the write generation method. $method_id = FeaturesGenerationWrite::METHOD_ID; - $result = $generator->generatePackages($method_id, $current_bundle, $packages); + $result = $generator->generatePackages($method_id, $current_bundle, + $packages); foreach ($result as $message) { $method = $message['success'] ? 'success' : 'error'; - $this->logger()->$method(dt($message['message'], $message['variables'])); + $this->logger()->$method(dt($message['message'], + $message['variables'])); } } else { @@ -400,7 +450,10 @@ public function add($components = null, $options = ['bundle' => null]) { * List features components. * * @command features:components - * @param $patterns The features components type to list. Omit this argument to list all components. + * + * @param $patterns The features components type to list. Omit this argument + * to list all components. + * * @option exported Show only components that have been exported. * @option not-exported Show only components that have not been exported. * @option bundle Use a specific bundle namespace. @@ -410,9 +463,17 @@ public function add($components = null, $options = ['bundle' => null]) { * * @return RowsOfFields */ - public function components(array $patterns, $options = ['format' => 'table', 'exported' => null, 'not-exported' => null, 'bundle' => null]) { + public function components( + array $patterns, + $options = [ + 'format' => 'table', + 'exported' => NULL, + 'not-exported' => NULL, + 'bundle' => NULL, + ] + ) { $args = $patterns; - $assigner = $this->getAssigner($options); + $assigner = $this->featuresOptions($options); $components = $this->componentList(); ksort($components); @@ -420,16 +481,18 @@ public function components(array $patterns, $options = ['format' => 'table', 'ex if (empty($args)) { $types = array_keys($components); array_unshift($types, 'all'); - $choice = $this->io()->choice('Enter a number to choose which component type to list.', $types); + $choice = $this->io() + ->choice('Enter a number to choose which component type to list.', + $types); if ($choice === FALSE) { return; } - $args = ($choice == 0) ? array('*') : array($types[$choice]); + $args = ($choice == 0) ? ['*'] : [$types[$choice]]; } - $options = array( + $options = [ 'provided by' => TRUE, - ); + ]; if ($options['exported']) { $options['not exported'] = FALSE; } @@ -444,18 +507,26 @@ public function components(array $patterns, $options = ['format' => 'table', 'ex } /** - * Show the difference between the active config and the default config stored in a feature package. + * Show the difference between the active config and the default config + * stored in a feature package. * * @command features:diff + * * @param $feature The feature in question. - * @option ctypes Comma separated list of component types to limit the output to. Defaults to all types. - * @option lines Generate diffs with lines of context instead of the usual two. + * + * @option ctypes Comma separated list of component types to limit the output + * to. Defaults to all types. + * @option lines Generate diffs with lines of context instead of the + * usual two. * @option bundle Use a specific bundle namespace. * @aliases fd,features-diff */ - public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'bundle' => null]) { + public function diff( + $feature, + $options = ['ctypes' => NULL, 'lines' => NULL, 'bundle' => NULL] + ) { $manager = $this->manager; - $assigner = $this->getAssigner($options); + $assigner = $this->featuresOptions($options); $assigner->assignConfigPackages(); $module = $feature; @@ -466,7 +537,8 @@ public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'b $feature = $manager->loadPackage($module, TRUE); if (empty($feature)) { - throw new \Exception(dt('No such feature is available: @module', array('@module' => $module))); + throw new \Exception(dt('No such feature is available: @module', + ['@module' => $module])); } $lines = $options['lines']; @@ -490,7 +562,8 @@ public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'b $overrides = array_merge($overrides, $missing); if (empty($overrides)) { - drush_print(dt('Active config matches stored config for @module.', array('@module' => $module))); + drush_print(dt('Active config matches stored config for @module.', + ['@module' => $module])); } else { $config_diff = $this->configDiff; @@ -498,33 +571,39 @@ public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'b // Print key for colors. drush_print(dt('Legend: ')); - drush_print(sprintf($red, dt('Code: drush features-import will replace the active config with the displayed code.'))); - drush_print(sprintf($green, dt('Active: drush features-export will update the exported feature with the displayed active config'))); + drush_print(sprintf($red, + dt('Code: drush features-import will replace the active config with the displayed code.'))); + drush_print(sprintf($green, + dt('Active: drush features-export will update the exported feature with the displayed active config'))); foreach ($overrides as $name) { $message = ''; if (in_array($name, $missing)) { $message = sprintf($red, t('(missing from active)')); - $extension = array(); - } else { + $extension = []; + } + else { $active = $manager->getActiveStorage()->read($name); $extension = $manager->getExtensionStorages()->read($name); if (empty($extension)) { - $extension = array(); + $extension = []; $message = sprintf($green, t('(not exported)')); } $diff = $config_diff->diff($extension, $active); $rows = explode("\n", $formatter->format($diff)); } drush_print(); - drush_print(dt("Config @name @message", array('@name' => $name, '@message' => $message))); + drush_print(dt("Config @name @message", + ['@name' => $name, '@message' => $message])); if (!empty($extension)) { foreach ($rows as $row) { if (strpos($row, '>') === 0) { drush_print(sprintf($green, $row)); - } elseif (strpos($row, '<') === 0) { + } + elseif (strpos($row, '<') === 0) { drush_print(sprintf($red, $row)); - } else { + } + else { drush_print($row); } } @@ -536,123 +615,137 @@ public function diff($feature, $options = ['ctypes' => null, 'lines' => null, 'b /** * Import a module config into your site. * + * @param $feature + * A comma-delimited list of features or feature:component pairs to import. + * * @command features:import - * @param $feature A space delimited list of features or feature:component pairs to import. + * * @option force Force import even if config is not overridden. * @option bundle Use a specific bundle namespace. - * @usage drush features-import foo:node.type.page foo:taxonomy.vocabulary.tags bar - * Import node and taxonomy config of feature "foo". Import all config of feature "bar". + * + * @usage drush features-import foo:node.type.page + * foo:taxonomy.vocabulary.tags bar Import node and taxonomy config of + * feature "foo". Import all config of feature "bar". + * * @aliases fim,fr,features-import + * + * @throws \Exception */ - public function import(array $feature, $options = ['force' => null, 'bundle' => null]) { - if ($feature) { - // Determine if revert should be forced. - $force = $options['force']; - // Determine if -y was supplied. If so, we can filter out needless output - // from this command. - $skip_confirmation = drush_get_context('DRUSH_AFFIRMATIVE'); - $manager = $this->manager; + public function import( + $feature, + $options = [ + 'force' => FALSE, + 'bundle' => FALSE, + ] + ) { + $this->featuresOptions($options); - // Parse list of arguments. - $modules = array(); - foreach ($feature as $arg) { - $arg = explode(':', $arg); - $module = array_shift($arg); - $component = array_shift($arg); - - if (isset($module)) { - if (empty($component)) { - // If we received just a feature name, this means that we need all of - // its components. - $modules[$module] = TRUE; - } elseif ($modules[$module] !== TRUE) { - if (!isset($modules[$module])) { - $modules[$module] = array(); - } - $modules[$module][] = $component; - } - } + $features = StringUtils::csvToArray($feature); + if (empty($features)) { + drush_invoke_process('@self', 'features:list:packages', [], $options); + return; + } + + // Determine if revert should be forced. + $force = $this->getOption($options, 'force'); + + // Determine if -y was supplied. If so, we can filter out needless output + // from this command. + $skip_confirmation = drush_get_context('DRUSH_AFFIRMATIVE'); + $manager = $this->manager; + + // Parse list of arguments. + $modules = []; + foreach ($features as $featureString) { + list($module, $component) = explode(':', $featureString); + + // We cannot use just a component name without its module. + if (empty($module)) { + continue; } - // Process modules. - foreach ($modules as $module => $components_needed) { + // We received just a feature name, meaning we need all of its components. + if (empty($component)) { + $modules[$module] = TRUE; + continue; + } - $dt_args['@module'] = $module; - /** @var \Drupal\features\Package $feature */ - $feature = $manager->loadPackage($module, TRUE); - if (empty($feature)) { - throw new \Exception(dt('No such feature is available: @module', $dt_args)); - } + if (empty($modules[$module])) { + $modules[$module] = []; + } - if ($feature->getStatus() != FeaturesManagerInterface::STATUS_INSTALLED) { - throw new \Exception(dt('No such feature is installed: @module', $dt_args)); - } + if ($modules[$module] !== TRUE) { + $modules[$module][] = $component; + } + } - // Forcefully revert all components of a feature. - if ($force) { - $components = $feature->getConfigOrig(); - } // Only revert components that are detected to be Overridden. - else { - $components = $manager->detectOverrides($feature); - $missing = $manager->reorderMissing($manager->detectMissing($feature)); - // Be sure to import missing components first. - $components = array_merge($missing, $components); - } + // Process modules. + foreach ($modules as $module => $componentsNeeded) { + // Reset the arguments on each loop pass. + $dt_args = ['@module' => $module]; - if (!empty($components_needed) && is_array($components_needed)) { - $components = array_intersect($components, $components_needed); - } + /** @var \Drupal\features\Package $feature */ + $feature = $manager->loadPackage($module, TRUE); + if (empty($feature)) { + throw new DomainException(dt('No such feature is available: @module', $dt_args)); + } - if (empty($components)) { - $this->logger()->info(dt('Current state already matches active config, aborting.')); - } else { - // Determine which config the user wants to import/revert. - $config_to_create = []; - foreach ($components as $component) { - $dt_args['@component'] = $component; - $confirmation_message = 'Do you really want to import @module : @component?'; - if ($skip_confirmation || $this->io()->confirm(dt($confirmation_message, $dt_args))) { - $config_to_create[$component] = ''; - } - } + if ($feature->getStatus() != FeaturesManagerInterface::STATUS_INSTALLED) { + throw new DomainException(dt('No such feature is installed: @module', $dt_args)); + } - // Perform the import/revert. - $config_imported = $manager->createConfiguration($config_to_create); - - // List the results. - foreach ($components as $component) { - $dt_args['@component'] = $component; - if (isset($config_imported['new'][$component])) { - $this->logger()->info(dt('Imported @module : @component.', $dt_args)); - } elseif (isset($config_imported['updated'][$component])) { - $this->logger()->info(dt('Reverted @module : @component.', $dt_args)); - } elseif (!isset($config_to_create[$component])) { - $this->logger()->info(dt('Skipping @module : @component.', $dt_args)); - } else { - $this->logger()->error(dt('Error importing @module : @component.', $dt_args)); - } - } + // Forcefully revert all components of a feature. + if ($force) { + $components = $feature->getConfigOrig(); + } + // Only revert components that are detected to be Overridden. + else { + $overrides = $manager->detectOverrides($feature); + $missing = $manager->reorderMissing($manager->detectMissing($feature)); + + // Be sure to import missing components first. + $components = array_merge($missing, $overrides); + } + + if (!empty($componentsNeeded) && is_array($componentsNeeded)) { + $components = array_intersect($components, $componentsNeeded); + } + + if (empty($components)) { + $this->logger()->info(dt('Current state already matches active config, aborting.')); + continue; + } + + // Determine which config the user wants to import/revert. + $configToCreate = []; + foreach ($components as $component) { + $dt_args['@component'] = $component; + $confirmation_message = 'Do you really want to import @module : @component?'; + if ($skip_confirmation || $this->io()->confirm(dt($confirmation_message, $dt_args))) { + $configToCreate[$component] = ''; } } - } - else { - drush_invoke_process('@self', 'features-list-packages', [], $options); - } - } - public function getAssigner($options) { - $assigner = $this->assigner; - $bundle_name = $options['bundle']; - if (!empty($bundle_name)) { - $bundle = $assigner->applyBundle($bundle_name); - if ($bundle->getMachineName() != $bundle_name) { - $this->logger()->warning(dt('Bundle @name not found. Using default.', array('@name' => $bundle_name))); + // Perform the import/revert. + $importedConfig = $manager->createConfiguration($configToCreate); + + // List the results. + foreach ($components as $component) { + $dt_args['@component'] = $component; + if (isset($importedConfig['new'][$component])) { + $this->logger()->info(dt('Imported @module : @component.', $dt_args)); + } + elseif (isset($importedConfig['updated'][$component])) { + $this->logger()->info(dt('Reverted @module : @component.', $dt_args)); + } + elseif (!isset($configToCreate[$component])) { + $this->logger()->info(dt('Skipping @module : @component.', $dt_args)); + } + else { + $this->logger()->error(dt('Error importing @module : @component.', $dt_args)); + } } } - else { - $assigner->assignConfigPackages(); - } - return $assigner; } /** @@ -662,7 +755,7 @@ public function getAssigner($options) { * The items to return data for. */ function buildConfig(array $items) { - $result = array(); + $result = []; foreach ($items as $config_type => $item) { foreach ($item as $item_name => $title) { $result[] = $this->manager->getFullName($config_type, $item_name); @@ -675,7 +768,7 @@ function buildConfig(array $items) { * Returns a listing of all known components, indexed by source. */ function componentList() { - $result = array(); + $result = []; $config = $this->manager->getConfigCollection(); foreach ($config as $item_name => $item) { $result[$item->getType()][$item->getShortName()] = $item->getLabel(); @@ -686,13 +779,13 @@ function componentList() { /** * Filters components by patterns. */ - function componentFilter($all_components, $patterns = array(), $options = array()) { - $options += array( + function componentFilter($all_components, $patterns = [], $options = []) { + $options += [ 'exported' => TRUE, 'not exported' => TRUE, 'provided by' => FALSE, - ); - $pool = array(); + ]; + $pool = []; // Maps exported components to feature modules. $components_map = $this->componentMap(); // First filter on exported state. @@ -721,11 +814,11 @@ function componentFilter($all_components, $patterns = array(), $options = array( $state_string = 'exported'; } - $selected = array(); + $selected = []; foreach ($patterns as $pattern) { // Rewrite * to %. Let users use both as wildcard. - $pattern = strtr($pattern, array('*' => '%')); - $sources = array(); + $pattern = strtr($pattern, ['*' => '%']); + $sources = []; list($source_pattern, $component_pattern) = explode(':', $pattern, 2); // If source is empty, use a pattern. if ($source_pattern == '') { @@ -735,8 +828,10 @@ function componentFilter($all_components, $patterns = array(), $options = array( $component_pattern = '%'; } - $preg_source_pattern = strtr(preg_quote($source_pattern, '/'), array('%' => '.*')); - $preg_component_pattern = strtr(preg_quote($component_pattern, '/'), array('%' => '.*')); + $preg_source_pattern = strtr(preg_quote($source_pattern, '/'), + ['%' => '.*']); + $preg_component_pattern = strtr(preg_quote($component_pattern, '/'), + ['%' => '.*']); // If it isn't a pattern, but a simple string, we don't anchor the // pattern. This allows for abbreviating. Otherwise, we do, as this seems // more natural for patterns. @@ -746,7 +841,7 @@ function componentFilter($all_components, $patterns = array(), $options = array( if (strpos($component_pattern, '%') !== FALSE) { $preg_component_pattern = '^' . $preg_component_pattern . '$'; } - $matches = array(); + $matches = []; // Find the sources. $all_sources = array_keys($pool); @@ -757,20 +852,22 @@ function componentFilter($all_components, $patterns = array(), $options = array( // use that, or error out. if (count($matches) > 1 and $preg_source_pattern[0] != '^') { if (in_array($source_pattern, $matches)) { - $matches = array($source_pattern); + $matches = [$source_pattern]; } else { - throw new \Exception(dt('Ambiguous source "@source", matches @matches', array( - '@source' => $source_pattern, - '@matches' => implode(', ', $matches), - ))); + throw new \Exception(dt('Ambiguous source "@source", matches @matches', + [ + '@source' => $source_pattern, + '@matches' => implode(', ', $matches), + ])); } } // Loose the indexes preg_grep preserved. $sources = array_values($matches); } else { - throw new \Exception(dt('No @state sources match "@source"', array('@state' => $state_string, '@source' => $source_pattern))); + throw new \Exception(dt('No @state sources match "@source"', + ['@state' => $state_string, '@source' => $source_pattern])); } // Now find the components. @@ -778,36 +875,40 @@ function componentFilter($all_components, $patterns = array(), $options = array( // Find the components. $all_components = array_keys($pool[$source]); // See if there's any matches. - $matches = preg_grep('/' . $preg_component_pattern . '/', $all_components); + $matches = preg_grep('/' . $preg_component_pattern . '/', + $all_components); if (count($matches) > 0) { // If we have multiple matches and the components string wasn't a // pattern, check if one of the matches is equal to the pattern, and // use that, or error out. if (count($matches) > 1 and $preg_component_pattern[0] != '^') { if (in_array($component_pattern, $matches)) { - $matches = array($component_pattern); + $matches = [$component_pattern]; } else { - throw new \Exception(dt('Ambiguous component "@component", matches @matches', array( - '@component' => $component_pattern, - '@matches' => implode(', ', $matches), - ))); + throw new \Exception(dt('Ambiguous component "@component", matches @matches', + [ + '@component' => $component_pattern, + '@matches' => implode(', ', $matches), + ])); } } if (!is_array($selected[$source])) { - $selected[$source] = array(); + $selected[$source] = []; } - $selected[$source] += array_intersect_key($pool[$source], array_flip($matches)); + $selected[$source] += array_intersect_key($pool[$source], + array_flip($matches)); } else { // No matches. If the source was a pattern, just carry on, else // error out. Allows for patterns like :*field* if ($preg_source_pattern[0] != '^') { - throw new \Exception(dt('No @state @source components match "@component"', array( - '@state' => $state_string, - '@component' => $component_pattern, - '@source' => $source, - ))); + throw new \Exception(dt('No @state @source components match "@component"', + [ + '@state' => $state_string, + '@component' => $component_pattern, + '@source' => $source, + ])); } } } @@ -815,29 +916,30 @@ function componentFilter($all_components, $patterns = array(), $options = array( // Lastly, provide feature module information on the selected components, if // requested. - $provided_by = array(); + $provided_by = []; if ($options['provided by'] && $options['exported']) { foreach ($selected as $source => $components) { foreach ($components as $name => $title) { $exported = count($components_map[$source][$name]) > 0; if ($exported) { - $provided_by[$source . ':' . $name] = implode(', ', $components_map[$source][$name]); + $provided_by[$source . ':' . $name] = implode(', ', + $components_map[$source][$name]); } } } } - return array( + return [ 'components' => $selected, 'sources' => $provided_by, - ); + ]; } /** * Provides a component to feature map (port of features_get_component_map). */ function componentMap() { - $result = array(); + $result = []; $manager = $this->manager; // Recalc full config list without running assignments. $config = $manager->getConfigCollection(); @@ -848,7 +950,7 @@ function componentMap() { $short_name = $item->getShortName(); $name = $item->getName(); if (!isset($result[$type][$short_name])) { - $result[$type][$short_name] = array(); + $result[$type][$short_name] = []; } if (!empty($item->getPackage())) { $package = $packages[$item->getPackage()]; @@ -866,7 +968,7 @@ function componentPrint($filtered_components) { $rows = []; foreach ($filtered_components['components'] as $source => $components) { foreach ($components as $name => $value) { - $row = array('source' => $source . ':' . $name); + $row = ['source' => $source . ':' . $name]; if (isset($filtered_components['sources'][$source . ':' . $name])) { $row['source'] = dt('Provided by') . ': ' . $filtered_components['sources'][$source . ':' . $name]; } diff --git a/src/Exception/DomainException.php b/src/Exception/DomainException.php new file mode 100644 index 0000000..c496d3b --- /dev/null +++ b/src/Exception/DomainException.php @@ -0,0 +1,7 @@ + Date: Wed, 18 Oct 2017 11:43:57 +0200 Subject: [PATCH 05/12] Fixed --bundle option not accepting values on fr command. --- src/Commands/FeaturesCommands.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 0f2efd2..5351768 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -248,7 +248,7 @@ public function listPackages( * * @usage drush features-import-all * Import module config from all installed features. - * + * * @aliases fra,fia,fim-all,features-import-all */ public function importAll($options = ['bundle' => NULL]) { @@ -634,8 +634,8 @@ public function diff( public function import( $feature, $options = [ - 'force' => FALSE, - 'bundle' => FALSE, + 'force' => NULL, + 'bundle' => NULL, ] ) { $this->featuresOptions($options); From 589961c6a69ef3dbcf97628352d6bc7234edf7e3 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 18 Oct 2017 14:25:35 +0200 Subject: [PATCH 06/12] Coding standards, nothing more. --- src/Commands/FeaturesCommands.php | 111 +++++++++++++++++++----------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 5351768..369631f 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -59,10 +59,15 @@ class FeaturesCommands extends DrushCommands { * FeaturesCommands constructor. * * @param \Drupal\features\FeaturesAssignerInterface $assigner + * The features_assigner service. * @param \Drupal\features\FeaturesManagerInterface $manager + * The features.manager service. * @param \Drupal\features\FeaturesGeneratorInterface $generator + * The features_generator service. * @param \Drupal\config_update\ConfigDiffInterface $configDiff + * The config_update.config_diff service. * @param \Drupal\Core\Config\StorageInterface $configStorage + * The config.storage service. */ public function __construct( FeaturesAssignerInterface $assigner, @@ -114,6 +119,7 @@ protected function featuresOptions(array $options) { * The default value of the option. * * @return mixed|null + * The option value, defaulting to NULL. */ protected function getOption(array $options, $name, $default = NULL) { return isset($options[$name]) @@ -168,29 +174,37 @@ public function status($keys = NULL, array $options = ['bundle' => NULL]) { } /** - * Display a list of all existing features and packages available to be - * generated. If a package name is provided as an argument, then all of the - * configuration objects assigned to that package will be listed. + * Display a list of all generate-able existing features and packages. * - * @command features:list:packages + * If a package name is provided as an argument, then all of the configuration + * objects assigned to that package will be listed. + * + * @param string $package_name + * The package to list. Optional; if specified, lists all configuration + * objects assigned to that package. If no package is specified, lists all + * of the features. * - * @param $package_name The package to list. Optional; if specified, lists - * all configuration objects assigned to that package. If no package is - * specified, lists all of the features. + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields|bool + * The command output, or FALSE if a requested package was not found. + * + * @command features:list:packages * * @option bundle Use a specific bundle namespace. - * @usage drush features-list-packages + * + * @usage drush features-list -packages * Display a list of all existing features and packages available to be * generated. * @usage drush features-list-packages 'example_article' * Display a list of all configuration objects assigned to the * 'example_article' package. + * * @field-labels * name: Name * machine_name: Machine name * status: Status * version: Version * state: State + * * @aliases fl,features-list-packages */ public function listPackages( @@ -216,14 +230,16 @@ public function listPackages( $state = FeaturesManagerInterface::STATE_OVERRIDDEN; } + $packageState = ($state != FeaturesManagerInterface::STATE_DEFAULT) + ? $manager->stateLabel($state) + : ''; + $result[$package->getMachineName()] = [ 'name' => $package->getName(), 'machine_name' => $package->getMachineName(), 'status' => $manager->statusLabel($package->getStatus()), 'version' => $package->getVersion(), - 'state' => ($state != FeaturesManagerInterface::STATE_DEFAULT) - ? $manager->stateLabel($state) - : '', + 'state' => $packageState, ]; } return new RowsOfFields($result); @@ -280,19 +296,25 @@ public function importAll($options = ['bundle' => NULL]) { /** * Export the configuration on your site into a custom module. * - * @command features:export + * @param array $packages + * A list of features to export. * - * @param $packages A space delimited list of features to export. + * @command features:export * * @option add-profile Package features into an install profile. * @option bundle Use a specific bundle namespace. + * * @usage drush features-export * Export all available packages. * @usage drush features-export example_article example_page * Export the example_article and example_page packages. * @usage drush features-export --add-profile * Export all available packages and add them to an install profile. + * * @aliases fex,fu,fua,fu-all,features-export + * + * @throws \Drush\Exceptions\UserAbortException + * @throws \Exception */ public function export( array $packages, @@ -362,14 +384,18 @@ public function export( /** * Add a config item to a feature package. * + * @param array|null $components + * Patterns of config to add, see features:components for the format to use. + * * @command features:add * @todo @param $feature Feature package to export and add config to. * - * @param $components Patterns of config to add, see features-components for - * the format of patterns. - * * @option bundle Use a specific bundle namespace. + * * @aliases fa,fe,features-add + * + * @throws \Drush\Exceptions\UserAbortException + * @throws \Exception */ public function add($components = NULL, $options = ['bundle' => NULL]) { if ($components) { @@ -449,19 +475,22 @@ public function add($components = NULL, $options = ['bundle' => NULL]) { /** * List features components. * - * @command features:components + * @param array $patterns + * The components types to list. Omit this argument to list them all. * - * @param $patterns The features components type to list. Omit this argument - * to list all components. + * @command features:components * * @option exported Show only components that have been exported. * @option not-exported Show only components that have not been exported. * @option bundle Use a specific bundle namespace. + * * @aliases fc,features-components + * * @field-labels * source: Available sources * - * @return RowsOfFields + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields|null + * The command output. May be empty. */ public function components( array $patterns, @@ -473,7 +502,7 @@ public function components( ] ) { $args = $patterns; - $assigner = $this->featuresOptions($options); + $this->featuresOptions($options); $components = $this->componentList(); ksort($components); @@ -482,10 +511,9 @@ public function components( $types = array_keys($components); array_unshift($types, 'all'); $choice = $this->io() - ->choice('Enter a number to choose which component type to list.', - $types); + ->choice('Enter a number to choose which component type to list.', $types); if ($choice === FALSE) { - return; + return NULL; } $args = ($choice == 0) ? ['*'] : [$types[$choice]]; @@ -507,19 +535,22 @@ public function components( } /** - * Show the difference between the active config and the default config - * stored in a feature package. + * Show the difference between active|default config from a feature package. * - * @command features:diff + * @param string $feature + * The feature in question. * - * @param $feature The feature in question. + * @command features:diff * * @option ctypes Comma separated list of component types to limit the output * to. Defaults to all types. * @option lines Generate diffs with lines of context instead of the * usual two. * @option bundle Use a specific bundle namespace. + * * @aliases fd,features-diff + * + * @throws \Exception */ public function diff( $feature, @@ -567,7 +598,6 @@ public function diff( } else { $config_diff = $this->configDiff; - $active_storage = $this->configStorage; // Print key for colors. drush_print(dt('Legend: ')); @@ -579,7 +609,7 @@ public function diff( foreach ($overrides as $name) { $message = ''; if (in_array($name, $missing)) { - $message = sprintf($red, t('(missing from active)')); + $message = sprintf($red, dt('(missing from active)')); $extension = []; } else { @@ -587,7 +617,7 @@ public function diff( $extension = $manager->getExtensionStorages()->read($name); if (empty($extension)) { $extension = []; - $message = sprintf($green, t('(not exported)')); + $message = sprintf($green, dt('(not exported)')); } $diff = $config_diff->diff($extension, $active); $rows = explode("\n", $formatter->format($diff)); @@ -615,7 +645,7 @@ public function diff( /** * Import a module config into your site. * - * @param $feature + * @param string $feature * A comma-delimited list of features or feature:component pairs to import. * * @command features:import @@ -754,7 +784,7 @@ public function import( * @param array $items * The items to return data for. */ - function buildConfig(array $items) { + protected function buildConfig(array $items) { $result = []; foreach ($items as $config_type => $item) { foreach ($item as $item_name => $title) { @@ -767,10 +797,10 @@ function buildConfig(array $items) { /** * Returns a listing of all known components, indexed by source. */ - function componentList() { + protected function componentList() { $result = []; $config = $this->manager->getConfigCollection(); - foreach ($config as $item_name => $item) { + foreach ($config as $item) { $result[$item->getType()][$item->getShortName()] = $item->getLabel(); } return $result; @@ -779,7 +809,7 @@ function componentList() { /** * Filters components by patterns. */ - function componentFilter($all_components, $patterns = [], $options = []) { + protected function componentFilter($all_components, $patterns = [], $options = []) { $options += [ 'exported' => TRUE, 'not exported' => TRUE, @@ -901,7 +931,7 @@ function componentFilter($all_components, $patterns = [], $options = []) { } else { // No matches. If the source was a pattern, just carry on, else - // error out. Allows for patterns like :*field* + // error out. Allows for patterns like ":*field*". if ($preg_source_pattern[0] != '^') { throw new \Exception(dt('No @state @source components match "@component"', [ @@ -938,17 +968,16 @@ function componentFilter($all_components, $patterns = [], $options = []) { /** * Provides a component to feature map (port of features_get_component_map). */ - function componentMap() { + protected function componentMap() { $result = []; $manager = $this->manager; // Recalc full config list without running assignments. $config = $manager->getConfigCollection(); $packages = $manager->getPackages(); - foreach ($config as $item_name => $item) { + foreach ($config as $item) { $type = $item->getType(); $short_name = $item->getShortName(); - $name = $item->getName(); if (!isset($result[$type][$short_name])) { $result[$type][$short_name] = []; } @@ -964,7 +993,7 @@ function componentMap() { /** * Prints a list of filtered components. */ - function componentPrint($filtered_components) { + protected function componentPrint($filtered_components) { $rows = []; foreach ($filtered_components['components'] as $source => $components) { foreach ($components as $name => $value) { From d5de1cc79a140a21633e40876fdd30271c9aeaae Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 18 Oct 2017 14:47:21 +0200 Subject: [PATCH 07/12] Accept feature names on fu command. --- src/Commands/FeaturesCommands.php | 16 +++++++++++----- src/Exception/InvalidArgumentException.php | 7 +++++++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 src/Exception/InvalidArgumentException.php diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 369631f..393ef05 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -7,6 +7,7 @@ use Drupal\config_update\ConfigDiffInterface; use Drupal\Core\Config\StorageInterface; use Drupal\features\Exception\DomainException; +use Drupal\features\Exception\InvalidArgumentException; use Drupal\features\FeaturesAssignerInterface; use Drupal\features\FeaturesGeneratorInterface; use Drupal\features\FeaturesManagerInterface; @@ -313,6 +314,8 @@ public function importAll($options = ['bundle' => NULL]) { * * @aliases fex,fu,fua,fu-all,features-export * + * @throws \Drupal\features\Exception\DomainException + * @throws \Drupal\features\Exception\InvalidArgumentException * @throws \Drush\Exceptions\UserAbortException * @throws \Exception */ @@ -328,7 +331,7 @@ public function export( if ($options['add-profile']) { if ($current_bundle->isDefault) { - throw new \Exception((dt("Must specify a profile name with --name"))); + throw new InvalidArgumentException(dt("Must specify a profile name with --name")); } $current_bundle->setIsProfile(TRUE); } @@ -336,8 +339,9 @@ public function export( $all_packages = $manager->getPackages(); foreach ($packages as $name) { if (!isset($all_packages[$name])) { - throw new \Exception(dt("The package @name does not exist.", - ['@name' => $name])); + throw new DomainException(dt("The package @name does not exist.", [ + '@name' => $name, + ])); } } @@ -350,6 +354,9 @@ public function export( throw new UserAbortException(); } } + else { + $packages = array_combine($packages, $packages); + } // If any packages exist, confirm before overwriting. if ($existing_packages = $manager->listPackageDirectories($packages, @@ -372,8 +379,7 @@ public function export( // Use the write generation method. $method_id = FeaturesGenerationWrite::METHOD_ID; - $result = $generator->generatePackages($method_id, $current_bundle, - array_keys($packages)); + $result = $generator->generatePackages($method_id, $current_bundle, array_keys($packages)); foreach ($result as $message) { $method = $message['success'] ? 'success' : 'error'; diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..199d51e --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,7 @@ + Date: Wed, 25 Oct 2017 13:32:03 +0200 Subject: [PATCH 08/12] fl command. --- src/Commands/FeaturesCommands.php | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 393ef05..461a924 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -2,6 +2,8 @@ namespace Drupal\features\Commands; +use Consolidation\OutputFormatters\StructuredData\ListDataFromKeys; +use Consolidation\OutputFormatters\StructuredData\PropertyList; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drupal\Component\Diff\DiffFormatter; use Drupal\config_update\ConfigDiffInterface; @@ -146,21 +148,22 @@ public function status($keys = NULL, array $options = ['bundle' => NULL]) { $currentBundle = $this->assigner->getBundle(); $export_settings = $this->manager->getExportSettings(); $methods = $this->assigner->getEnabledAssigners(); + $output = $this->output(); if ($currentBundle->isDefault()) { - $this->output()->writeln(dt('Current bundle: none')); + $output->writeln(dt('Current bundle: none')); } else { - $this->output()->writeln(dt('Current bundle: @name (@machine_name)', [ + $output->writeln(dt('Current bundle: @name (@machine_name)', [ '@name' => $currentBundle->getName(), '@machine_name' => $currentBundle->getMachineName(), ])); } - $this->output()->writeln(dt('Export folder: @folder', [ + $output->writeln(dt('Export folder: @folder', [ '@folder' => $export_settings['folder'], ])); - $this->output() + $output ->writeln(dt('The following assignment methods are enabled:')); - $this->output()->writeln(dt(' @methods', [ + $output->writeln(dt(' @methods', [ '@methods' => implode(', ', array_keys($methods)), ])); @@ -170,7 +173,7 @@ public function status($keys = NULL, array $options = ['bundle' => NULL]) { $data = count($keys) > 1 ? array_keys($config) : $config[$keys[0]]; - $this->output()->writeln(print_r($data, TRUE)); + $output->writeln(print_r($data, TRUE)); } } @@ -192,14 +195,15 @@ public function status($keys = NULL, array $options = ['bundle' => NULL]) { * * @option bundle Use a specific bundle namespace. * - * @usage drush features-list -packages + * @usage drush features:list:packages * Display a list of all existing features and packages available to be * generated. - * @usage drush features-list-packages 'example_article' + * @usage drush features:list:packages 'example_article' * Display a list of all configuration objects assigned to the * 'example_article' package. * * @field-labels + * config: Config * name: Name * machine_name: Machine name * status: Status @@ -245,15 +249,24 @@ public function listPackages( } return new RowsOfFields($result); } - // If a valid package was listed, list its configuration. - else { - // @todo. I suggest changing this command to return YAML with config as nested values. - } + + // A valid package was listed. + $package = $this->manager->findPackage($package_name); // If no matching package found, return an error. - $this->logger()->warning(dt('Package "@package" not found.', - ['@package' => $package_name])); - return FALSE; + if (empty($package)) { + $this->logger()->warning(dt('Package "@package" not found.', [ + '@package' => $package_name, + ])); + return FALSE; + } + + // This is a valid package, list its configuration. + $config = array_map(function ($name) { + return ['config' => $name]; + }, $package->getConfig()); + + return new RowsOfFields($config); } /** From af631fdbc644245dcc27ccdab9ab227611355667 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 25 Oct 2017 14:07:55 +0200 Subject: [PATCH 09/12] fd command. --- src/Commands/FeaturesCommands.php | 124 ++++++++++++++++++------------ 1 file changed, 73 insertions(+), 51 deletions(-) diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 461a924..79807b6 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -2,8 +2,6 @@ namespace Drupal\features\Commands; -use Consolidation\OutputFormatters\StructuredData\ListDataFromKeys; -use Consolidation\OutputFormatters\StructuredData\PropertyList; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drupal\Component\Diff\DiffFormatter; use Drupal\config_update\ConfigDiffInterface; @@ -23,6 +21,39 @@ */ class FeaturesCommands extends DrushCommands { + const OPTIONS =[ + 'bundle' => NULL, + ]; + + const OPTIONS_ADD = self::OPTIONS; + + const OPTIONS_COMPONENTS = self::OPTIONS + [ + 'exported' => NULL, + 'format' => 'table', + 'not-exported' => NULL, + ]; + + const OPTIONS_DIFF = self::OPTIONS + [ + 'ctypes' => NULL, + 'lines' => NULL, + ]; + + const OPTIONS_EXPORT = self::OPTIONS + [ + 'add-profile' => NULL, + ]; + + const OPTIONS_IMPORT = self::OPTIONS + [ + 'force' => NULL, + ]; + + const OPTIONS_IMPORT_ALL = self::OPTIONS; + + const OPTIONS_LIST = self::OPTIONS + [ + 'format' => 'table', + ]; + + const OPTIONS_STATUS = self::OPTIONS; + /** * The features_assigner service. * @@ -142,7 +173,7 @@ protected function getOption(array $options, $name, $default = NULL) { * * @aliases fs,features-status */ - public function status($keys = NULL, array $options = ['bundle' => NULL]) { + public function status($keys = NULL, array $options = self::OPTIONS_STATUS) { $this->featuresOptions($options); $currentBundle = $this->assigner->getBundle(); @@ -212,10 +243,7 @@ public function status($keys = NULL, array $options = ['bundle' => NULL]) { * * @aliases fl,features-list-packages */ - public function listPackages( - $package_name = NULL, - $options = ['format' => 'table', 'bundle' => NULL] - ) { + public function listPackages($package_name = NULL, $options = self::OPTIONS_LIST) { $assigner = $this->featuresOptions($options); $current_bundle = $assigner->getBundle(); $namespace = $current_bundle->isDefault() ? '' : $current_bundle->getMachineName(); @@ -281,7 +309,7 @@ public function listPackages( * * @aliases fra,fia,fim-all,features-import-all */ - public function importAll($options = ['bundle' => NULL]) { + public function importAll($options = self::OPTIONS_IMPORT_ALL) { $assigner = $this->featuresOptions($options); $currentBundle = $assigner->getBundle(); $namespace = $currentBundle->isDefault() ? '' : $currentBundle->getMachineName(); @@ -332,10 +360,7 @@ public function importAll($options = ['bundle' => NULL]) { * @throws \Drush\Exceptions\UserAbortException * @throws \Exception */ - public function export( - array $packages, - $options = ['add-profile' => NULL, 'bundle' => NULL] - ) { + public function export(array $packages, $options = self::OPTIONS_EXPORT) { $assigner = $this->featuresOptions($options); $manager = $this->manager; $generator = $this->generator; @@ -407,6 +432,7 @@ public function export( * Patterns of config to add, see features:components for the format to use. * * @command features:add + * * @todo @param $feature Feature package to export and add config to. * * @option bundle Use a specific bundle namespace. @@ -416,7 +442,7 @@ public function export( * @throws \Drush\Exceptions\UserAbortException * @throws \Exception */ - public function add($components = NULL, $options = ['bundle' => NULL]) { + public function add($components = NULL, $options = self::OPTIONS_ADD) { if ($components) { $assigner = $this->featuresOptions($options); $manager = $this->manager; @@ -511,15 +537,7 @@ public function add($components = NULL, $options = ['bundle' => NULL]) { * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields|null * The command output. May be empty. */ - public function components( - array $patterns, - $options = [ - 'format' => 'table', - 'exported' => NULL, - 'not-exported' => NULL, - 'bundle' => NULL, - ] - ) { + public function components(array $patterns, $options = self::OPTIONS_COMPONENTS) { $args = $patterns; $this->featuresOptions($options); @@ -561,7 +579,7 @@ public function components( * * @command features:diff * - * @option ctypes Comma separated list of component types to limit the output + * @option ctypes Comma-separated list of component types to limit the output * to. Defaults to all types. * @option lines Generate diffs with lines of context instead of the * usual two. @@ -571,24 +589,24 @@ public function components( * * @throws \Exception */ - public function diff( - $feature, - $options = ['ctypes' => NULL, 'lines' => NULL, 'bundle' => NULL] - ) { + public function diff($feature, $options = self::OPTIONS_DIFF) { $manager = $this->manager; $assigner = $this->featuresOptions($options); $assigner->assignConfigPackages(); $module = $feature; - $filter_ctypes = $options["ctypes"]; + + // @FIXME Actually do something with the "ctypes" option. + $filter_ctypes = $options['ctypes']; if ($filter_ctypes) { $filter_ctypes = explode(',', $filter_ctypes); } $feature = $manager->loadPackage($module, TRUE); if (empty($feature)) { - throw new \Exception(dt('No such feature is available: @module', - ['@module' => $module])); + throw new DomainException(dt('No such feature is available: @module', [ + '@module' => $module, + ])); } $lines = $options['lines']; @@ -611,25 +629,28 @@ public function diff( $missing = $manager->reorderMissing($manager->detectMissing($feature)); $overrides = array_merge($overrides, $missing); + $output = $this->output(); + if (empty($overrides)) { - drush_print(dt('Active config matches stored config for @module.', - ['@module' => $module])); + $output->writeln(dt('Active config matches stored config for @module.', [ + '@module' => $module, + ])); } else { $config_diff = $this->configDiff; // Print key for colors. - drush_print(dt('Legend: ')); - drush_print(sprintf($red, - dt('Code: drush features-import will replace the active config with the displayed code.'))); - drush_print(sprintf($green, - dt('Active: drush features-export will update the exported feature with the displayed active config'))); + $output->writeln(dt('Legend: ')); + $output->writeln(sprintf($red, + dt('Code: drush features-import will replace the active config with the displayed code.'))); + $output->writeln(sprintf($green, + dt('Active: drush features-export will update the exported feature with the displayed active config'))); foreach ($overrides as $name) { $message = ''; if (in_array($name, $missing)) { - $message = sprintf($red, dt('(missing from active)')); $extension = []; + $message = sprintf($red, dt('(missing from active)')); } else { $active = $manager->getActiveStorage()->read($name); @@ -641,19 +662,23 @@ public function diff( $diff = $config_diff->diff($extension, $active); $rows = explode("\n", $formatter->format($diff)); } - drush_print(); - drush_print(dt("Config @name @message", - ['@name' => $name, '@message' => $message])); + + $output->writeln(''); + $output->writeln(dt("Config @name @message", [ + '@name' => $name, + '@message' => $message, + ])); + if (!empty($extension)) { foreach ($rows as $row) { if (strpos($row, '>') === 0) { - drush_print(sprintf($green, $row)); + $output->writeln(sprintf($green, $row)); } elseif (strpos($row, '<') === 0) { - drush_print(sprintf($red, $row)); + $output->writeln(sprintf($red, $row)); } else { - drush_print($row); + $output->writeln($row); } } } @@ -680,13 +705,7 @@ public function diff( * * @throws \Exception */ - public function import( - $feature, - $options = [ - 'force' => NULL, - 'bundle' => NULL, - ] - ) { + public function import($feature, $options = self::OPTIONS_IMPORT) { $this->featuresOptions($options); $features = StringUtils::csvToArray($feature); @@ -802,6 +821,9 @@ public function import( * * @param array $items * The items to return data for. + * + * @return array + * An array of config items. */ protected function buildConfig(array $items) { $result = []; From 47c49c2010c82013498202bd48bfe981104c0148 Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 25 Oct 2017 16:43:17 +0200 Subject: [PATCH 10/12] config_update is also a Composer-level dependency. --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ba55895..da5439e 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,8 @@ "license": "GPL-2.0+", "minimum-stability": "dev", "name": "drupal/features", - "require": { }, + "require": { + "drupal/config_update": "^1.4" + }, "type": "drupal-module" } From fad5b2982b00dc518b8bb3a3e7c87a5385f999e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20G=2E=20MARAND?= Date: Fri, 10 Nov 2017 18:16:27 +0100 Subject: [PATCH 11/12] Logger access outside getter Issue #3 by pguillard. --- src/Commands/FeaturesCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/FeaturesCommands.php b/src/Commands/FeaturesCommands.php index 79807b6..c8249ae 100644 --- a/src/Commands/FeaturesCommands.php +++ b/src/Commands/FeaturesCommands.php @@ -331,7 +331,7 @@ public function importAll($options = self::OPTIONS_IMPORT_ALL) { $this->import($overridden); } else { - $this->logger->info(dt('Current state already matches active config, aborting.')); + $this->logger()->info(dt('Current state already matches active config, aborting.')); } } From bfbbbaec09a574cd80e45d563cd5c3ed0144e55c Mon Sep 17 00:00:00 2001 From: "Frederic G. MARAND" Date: Wed, 29 Nov 2017 10:18:42 +0100 Subject: [PATCH 12/12] Support the newly required extra.drush.services section in composer.json. --- composer.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/composer.json b/composer.json index da5439e..161614f 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,12 @@ { "description": "Provides drush commands for Features", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + }, "license": "GPL-2.0+", "minimum-stability": "dev", "name": "drupal/features",