View source
<?php
namespace Drupal\features\Commands;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Component\Diff\DiffFormatter;
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\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;
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;
protected $assigner;
protected $manager;
protected $generator;
protected $configDiff;
protected $configStorage;
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;
}
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;
}
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));
}
}
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 (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);
}
$package = $this->manager
->findPackage($package_name);
if (empty($package)) {
$this
->logger()
->warning(dt('Package "@package" not found.', [
'@package' => $package_name,
]));
return FALSE;
}
$config = array_map(function ($name) {
return [
'config' => $name,
];
}, $package
->getConfig());
return new RowsOfFields($config);
}
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.'));
}
}
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 ($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,
]));
}
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();
}
}
$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']));
}
}
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 ($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,
]));
}
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);
$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);
$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.');
}
}
public function components(array $patterns, $options = self::OPTIONS_COMPONENTS) {
$args = $patterns;
$this
->featuresOptions($options);
$components = $this
->componentList();
ksort($components);
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);
}
}
public function diff($feature, $options = self::OPTIONS_DIFF) {
$manager = $this->manager;
$assigner = $this
->featuresOptions($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 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 = "\33[31;40m\33[1m%s\33[0m";
$green = "\33[0;32;40m\33[1m%s\33[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;
$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);
}
}
}
}
}
}
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;
}
$force = $this
->getOption($options, 'force');
$skip_confirmation = $options['yes'];
$manager = $this->manager;
$modules = [];
foreach ($features as $featureString) {
list($module, $component) = explode(':', $featureString);
if (empty($module)) {
continue;
}
if (empty($component)) {
$modules[$module] = TRUE;
continue;
}
if (empty($modules[$module])) {
$modules[$module] = [];
}
if ($modules[$module] !== TRUE) {
$modules[$module][] = $component;
}
}
foreach ($modules as $module => $componentsNeeded) {
$dt_args = [
'@module' => $module,
];
$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));
}
if ($force) {
$components = $feature
->getConfigOrig();
}
else {
$overrides = $manager
->detectOverrides($feature);
$missing = $manager
->reorderMissing($manager
->detectMissing($feature));
$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;
}
$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] = '';
}
}
$importedConfig = $manager
->createConfiguration($configToCreate);
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));
}
}
}
}
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;
}
protected function componentList() {
$result = [];
$config = $this->manager
->getConfigCollection();
foreach ($config as $item) {
$result[$item
->getType()][$item
->getShortName()] = $item
->getLabel();
}
return $result;
}
protected function componentFilter($all_components, $patterns = [], $options = []) {
$options += [
'exported' => TRUE,
'not exported' => TRUE,
'provided by' => FALSE,
];
$pool = [];
$components_map = $this
->componentMap();
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) {
$pattern = strtr($pattern, [
'*' => '%',
]);
$sources = [];
list($source_pattern, $component_pattern) = explode(':', $pattern, 2);
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 (strpos($source_pattern, '%') !== FALSE) {
$preg_source_pattern = '^' . $preg_source_pattern . '$';
}
if (strpos($component_pattern, '%') !== FALSE) {
$preg_component_pattern = '^' . $preg_component_pattern . '$';
}
$matches = [];
$all_sources = array_keys($pool);
$matches = preg_grep('/' . $preg_source_pattern . '/', $all_sources);
if (count($matches) > 0) {
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),
]));
}
}
$sources = array_values($matches);
}
else {
throw new \Exception(dt('No @state sources match "@source"', [
'@state' => $state_string,
'@source' => $source_pattern,
]));
}
foreach ($sources as $source) {
$all_components = array_keys($pool[$source]);
$matches = preg_grep('/' . $preg_component_pattern . '/', $all_components);
if (count($matches) > 0) {
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 {
if ($preg_source_pattern[0] != '^') {
throw new \Exception(dt('No @state @source components match "@component"', [
'@state' => $state_string,
'@component' => $component_pattern,
'@source' => $source,
]));
}
}
}
}
$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,
];
}
protected function componentMap() {
$result = [];
$manager = $this->manager;
$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;
}
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);
}
}