features-8.x-3.11/src/Drush/Commands/FeaturesCommands.php
src/Drush/Commands/FeaturesCommands.php
<?php namespace Drupal\features\Drush\Commands; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drupal\Component\Diff\DiffFormatter; use Drupal\config_update\ConfigDiffInterface; use Drupal\Core\Config\StorageInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\features\Exception\DomainException; use Drupal\features\Exception\InvalidArgumentException; use Drupal\features\FeaturesAssignerInterface; use Drupal\features\FeaturesBundleInterface; use Drupal\features\FeaturesGeneratorInterface; use Drupal\features\FeaturesManagerInterface; use Drupal\features\Plugin\FeaturesGeneration\FeaturesGenerationWrite; use Drush\Commands\DrushCommands; use Drush\Exceptions\UserAbortException; use Drush\Utils\StringUtils; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Drush commands for Features. */ class FeaturesCommands extends DrushCommands implements ContainerInjectionInterface { 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. * * @var \Drupal\features\FeaturesAssignerInterface */ protected $assigner; /** * The features.manager service. * * @var \Drupal\features\FeaturesManagerInterface */ protected $manager; /** * The features_generator service. * * @var \Drupal\features\FeaturesGeneratorInterface */ protected $generator; /** * The config_update.config_diff service. * * @var \Drupal\config_update\ConfigDiffInterface */ protected $configDiff; /** * The config.storage service. * * @var \Drupal\Core\Config\StorageInterface */ protected $configStorage; /** * 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, FeaturesManagerInterface $manager, FeaturesGeneratorInterface $generator, ConfigDiffInterface $configDiff, StorageInterface $configStorage) { parent::__construct(); $this->assigner = $assigner; $this->configDiff = $configDiff; $this->configStorage = $configStorage; $this->generator = $generator; $this->manager = $manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('features_assigner'), $container->get('features.manager'), $container->get('features_generator'), $container->get('config_update.config_diff'), $container->get('config.storage') ); } /** * Applies global options for Features drush commands, including the bundle. * * 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'); 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; } /** * 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 * The option value, defaulting to NULL. */ 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 = self::OPTIONS_STATUS) { $this->featuresOptions($options); $currentBundle = $this->assigner->getBundle(); $export_settings = $this->manager->getExportSettings(); $methods = $this->assigner->getEnabledAssigners(); $output = $this->output(); if ($currentBundle->isDefault()) { $output->writeln(dt('Current bundle: none')); } else { $output->writeln(dt('Current bundle: @name (@machine_name)', [ '@name' => $currentBundle->getName(), '@machine_name' => $currentBundle->getMachineName(), ])); } $output->writeln(dt('Export folder: @folder', [ '@folder' => $export_settings['folder'], ])); $output ->writeln(dt('The following assignment methods are enabled:')); $output->writeln(dt(' @methods', [ '@methods' => implode(', ', array_keys($methods)), ])); if (!empty($keys)) { $config = $this->manager->getConfigCollection(); $keys = StringUtils::csvToArray($keys); $data = count($keys) > 1 ? array_keys($config) : $config[$keys[0]]; $output->writeln(print_r($data, TRUE)); } } /** * Display a list of all generate-able existing features and 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. * * @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 * 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 * config: Config * name: Name * machine_name: Machine name * status: Status * version: Version * state: State * * @aliases fl,features-list-packages */ public function listPackages($package_name = NULL, $options = self::OPTIONS_LIST) { $assigner = $this->featuresOptions($options); $current_bundle = $assigner->getBundle(); $namespace = $current_bundle->isDefault() ? FeaturesBundleInterface::DEFAULT_BUNDLE : $current_bundle->getMachineName(); $manager = $this->manager; $packages = $manager->getPackages(); $packages = $manager->filterPackages($packages, $namespace); $result = []; // 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; } $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' => $packageState, ]; } return new RowsOfFields($result); } // A valid package was listed. $package = $this->manager->findPackage($package_name); // If no matching package found, return an error. 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); } /** * 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 = self::OPTIONS_IMPORT_ALL) { $assigner = $this->featuresOptions($options); $currentBundle = $assigner->getBundle(); $namespace = $currentBundle->isDefault() ? FeaturesBundleInterface::DEFAULT_BUNDLE : $currentBundle->getMachineName(); $manager = $this->manager; $packages = $manager->getPackages(); $packages = $manager->filterPackages($packages, $namespace); $overridden = []; 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. * * @param array $packages * A 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 \Drupal\features\Exception\DomainException * @throws \Drupal\features\Exception\InvalidArgumentException * @throws \Drush\Exceptions\UserAbortException * @throws \Exception */ public function export(array $packages, $options = self::OPTIONS_EXPORT) { $assigner = $this->featuresOptions($options); $manager = $this->manager; $generator = $this->generator; $current_bundle = $assigner->getBundle(); if ($options['add-profile']) { if ($current_bundle->isDefault) { throw new InvalidArgumentException(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 DomainException(dt("The package @name does not exist.", [ '@name' => $name, ])); } } if (empty($packages)) { $packages = $all_packages; $dt_args = ['@modules' => implode(', ', array_keys($packages))]; $this->output()->writeln(dt('The following extensions will be exported: @modules', $dt_args)); if (!$this->io()->confirm('Do you really want to continue?')) { throw new UserAbortException(); } } else { $packages = array_combine($packages, $packages); } // If any packages exist, confirm before overwriting. if ($existing_packages = $manager->listPackageDirectories($packages, $current_bundle)) { foreach ($existing_packages as $name => $directory) { $this->output()->writeln(dt("The extension @name already exists at @directory.", ['@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. * * @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. * * @option bundle Use a specific bundle namespace. * * @aliases fa,fe,features-add * * @throws \Drush\Exceptions\UserAbortException * @throws \Exception */ public function add($components = NULL, $options = self::OPTIONS_ADD) { if ($components) { $assigner = $this->featuresOptions($options); $manager = $this->manager; $generator = $this->generator; $current_bundle = $assigner->getBundle(); $module = array_shift($args); if (empty($args)) { throw new \Exception('No components supplied.'); } $components = $this->componentList(); $options = [ 'exported' => FALSE, ]; $filtered_components = $this->componentFilter($components, $args, $options); $items = $filtered_components['components']; if (empty($items)) { throw new \Exception('No components to add.'); } $packages = [$module]; // If any packages exist, confirm before overwriting. if ($existing_packages = $manager->listPackageDirectories($packages)) { foreach ($existing_packages as $name => $directory) { $this->output()->writeln(dt("The extension @name already exists at @directory.", ['@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); [$full_name, $path] = $manager->getExportInfo($package, $current_bundle); $this->output()->writeln(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(); } } $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. * * @param array $patterns * The components types to list. Omit this argument to list them all. * * @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 \Consolidation\OutputFormatters\StructuredData\RowsOfFields|null * The command output. May be empty. */ public function components(array $patterns, $options = self::OPTIONS_COMPONENTS) { $args = $patterns; $this->featuresOptions($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 NULL; } $args = ($choice == 0) ? ['*'] : [$types[$choice]]; } $options = [ '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 active|default config from a feature package. * * @param string $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 <n> 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, $options = self::OPTIONS_DIFF) { $manager = $this->manager; $assigner = $this->featuresOptions($options); $assigner->assignConfigPackages(); $module = $feature; // @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 DomainException(dt('No such feature is available: @module', [ '@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 ($this->output()->isDecorated()) { $red = "\033[31;40m\033[1m%s\033[0m"; $green = "\033[0;32;40m\033[1m%s\033[0m"; } else { $red = '%s'; $green = "%s"; } $overrides = $manager->detectOverrides($feature); $missing = $manager->reorderMissing($manager->detectMissing($feature)); $overrides = array_merge($overrides, $missing); $output = $this->output(); if (empty($overrides)) { $output->writeln(dt('Active config matches stored config for @module.', [ '@module' => $module, ])); } else { $config_diff = $this->configDiff; // Print key for colors. $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)) { $extension = []; $message = sprintf($red, dt('(missing from active)')); } else { $active = $manager->getActiveStorage()->read($name); $extension = $manager->getExtensionStorages()->read($name); if (empty($extension)) { $extension = []; $message = sprintf($green, dt('(not exported)')); } $diff = $config_diff->diff($extension, $active); $rows = explode("\n", $formatter->format($diff)); } $output->writeln(''); $output->writeln(dt("Config @name @message", [ '@name' => $name, '@message' => $message, ])); if (!empty($extension)) { foreach ($rows as $row) { if (strpos($row, '>') === 0) { $output->writeln(sprintf($green, $row)); } elseif (strpos($row, '<') === 0) { $output->writeln(sprintf($red, $row)); } else { $output->writeln($row); } } } } } } /** * Import a module config into your site. * * @param string $feature * A comma-delimited list of features or feature:component pairs to import. * * @command features: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 * * @throws \Exception */ public function import($feature, $options = self::OPTIONS_IMPORT) { $this->featuresOptions($options); $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'); $manager = $this->manager; // Parse list of arguments. $modules = []; foreach ($features as $featureString) { // Make sure there will actually be a component before exploding. [$module, $component] = explode(':', "$featureString:"); // We cannot use just a component name without its module. if (empty($module)) { continue; } // We received just a feature name, meaning we need all of its components. if (empty($component)) { $modules[$module] = TRUE; continue; } if (empty($modules[$module])) { $modules[$module] = []; } if ($modules[$module] !== TRUE) { $modules[$module][] = $component; } } // Process modules. foreach ($modules as $module => $componentsNeeded) { // Reset the arguments on each loop pass. $dt_args = ['@module' => $module]; /** @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 ($feature->getStatus() != FeaturesManagerInterface::STATUS_INSTALLED) { throw new DomainException(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 { $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 ($this->io()->confirm(dt($confirmation_message, $dt_args))) { $configToCreate[$component] = ''; } } // 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)); } } } } /** * Returns an array of full config names given a array[$type][$component]. * * @param array $items * The items to return data for. * * @return array * An array of config items. */ protected function buildConfig(array $items) { $result = []; foreach ($items as $config_type => $item) { foreach ($item as $item_name => $title) { $result[] = $this->manager->getFullName($config_type, $item_name); } } return $result; } /** * Returns a listing of all known components, indexed by source. */ protected function componentList() { $result = []; $config = $this->manager->getConfigCollection(); foreach ($config as $item) { $result[$item->getType()][$item->getShortName()] = $item->getLabel(); } return $result; } /** * Filters components by patterns. */ protected function componentFilter($all_components, $patterns = [], $options = []) { $options += [ 'exported' => TRUE, 'not exported' => TRUE, 'provided by' => FALSE, ]; $pool = []; // 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 = []; foreach ($patterns as $pattern) { // Rewrite * to %. Let users use both as wildcard. $pattern = strtr($pattern, ['*' => '%']); $sources = []; [$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, '/'), ['%' => '.*']); $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. if (strpos($source_pattern, '%') !== FALSE) { $preg_source_pattern = '^' . $preg_source_pattern . '$'; } if (strpos($component_pattern, '%') !== FALSE) { $preg_component_pattern = '^' . $preg_component_pattern . '$'; } $matches = []; // 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 = [$source_pattern]; } else { 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"', ['@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 = [$component_pattern]; } else { throw new \Exception(dt('Ambiguous component "@component", matches @matches', [ '@component' => $component_pattern, '@matches' => implode(', ', $matches), ])); } } if (!is_array($selected[$source])) { $selected[$source] = []; } $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"', [ '@state' => $state_string, '@component' => $component_pattern, '@source' => $source, ])); } } } } // Lastly, provide feature module information on the selected components, if // requested. $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]); } } } } return [ 'components' => $selected, 'sources' => $provided_by, ]; } /** * Provides a component to feature map (port of features_get_component_map). */ protected function componentMap() { $result = []; $manager = $this->manager; // Recalc full config list without running assignments. $config = $manager->getConfigCollection(); $packages = $manager->getPackages(); foreach ($config as $item) { $type = $item->getType(); $short_name = $item->getShortName(); if (!isset($result[$type][$short_name])) { $result[$type][$short_name] = []; } if (!empty($item->getPackage())) { $package = $packages[$item->getPackage()]; $result[$type][$short_name][] = $package->getMachineName(); } } return $result; } /** * Prints a list of filtered components. */ protected function componentPrint($filtered_components) { $rows = []; foreach ($filtered_components['components'] as $source => $components) { foreach ($components as $name => $value) { $row = ['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); } }