features-8.x-3.11/modules/features_ui/src/Form/FeaturesEditForm.php
modules/features_ui/src/Form/FeaturesEditForm.php
<?php
namespace Drupal\features_ui\Form;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SortArray;
use Drupal\Component\Utility\Xss;
use Drupal\features\FeaturesAssignerInterface;
use Drupal\features\FeaturesGeneratorInterface;
use Drupal\features\FeaturesManagerInterface;
use Drupal\features\ConfigurationItem;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\config_update\ConfigRevertInterface;
/**
* Defines the features settings form.
*/
class FeaturesEditForm extends FormBase {
/**
* The features manager.
*
* @var array
*/
protected $featuresManager;
/**
* The package assigner.
*
* @var array
*/
protected $assigner;
/**
* The package generator.
*
* @var array
*/
protected $generator;
/**
* Current package being edited.
*
* @var \Drupal\features\Package
*/
protected $package;
/**
* Current bundle machine name.
*
* NOTE: D8 cannot serialize objects within forms so you can't directly
* store the entire Bundle object here.
*
* @var string
*/
protected $bundle;
/**
* Previous bundle name for ajax processing.
*
* @var string
*/
protected $oldBundle;
/**
* Config to be specifically excluded.
*
* @var array
*/
protected $excluded = [];
/**
* Config to be specifically required.
*
* @var array
*/
protected $required = [];
/**
* Config referenced by other packages.
*
* @var array
*/
protected $conflicts = [];
/**
* Determine if conflicts are allowed to be added.
*
* @var bool
*/
protected $allowConflicts;
/**
* Config missing from active site.
*
* @var array
*/
protected $missing = [];
/**
* The config reverter.
*
* @var \Drupal\config_update\ConfigRevertInterface
*/
protected $configRevert;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->featuresManager = $container->get('features.manager');
$instance->assigner = $container->get('features_assigner');
$instance->generator = $container->get('features_generator');
$instance->configRevert = $container->get('features.config_update');
$instance->moduleHandler = $container->get('module_handler');
$instance->currentUser = $container->get('current_user');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'features_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $featurename = '') {
if ($this->getRequest()->hasSession()) {
$session = $this->getRequest()->getSession();
}
$trigger = $form_state->getTriggeringElement();
if (isset($trigger['#name']) && $trigger['#name'] == 'package') {
// Save current bundle name for later ajax callback.
$this->oldBundle = $this->bundle;
}
elseif (isset($trigger['#name']) && $trigger['#name'] == 'conflicts') {
if (isset($session)) {
$session->set('features_allow_conflicts', $form_state->getValue('conflicts'));
}
}
if (!$form_state->isValueEmpty('package')) {
$bundle_name = $form_state->getValue('package');
$bundle = $this->assigner->getBundle($bundle_name);
}
else {
$bundle = $this->assigner->loadBundle();
}
// Only store bundle name, not full object.
$this->bundle = $bundle->getMachineName();
$this->allowConflicts = FALSE;
if (isset($session)) {
$this->allowConflicts = $session->get('features_allow_conflicts', FALSE);
}
// Pass the $force argument as TRUE because we want to include any excluded
// configuration items. These should show up as automatically assigned, but
// not selected, thus allowing the admin to reselect if desired.
// @see FeaturesManagerInterface::assignConfigPackage()
$this->assigner->assignConfigPackages(TRUE);
$packages = $this->featuresManager->getPackages();
if (empty($packages[$featurename])) {
$featurename = str_replace(['-', ' '], '_', $featurename);
$this->package = $this->featuresManager->initPackage($featurename, NULL, '', 'module', $bundle);
}
else {
$this->package = $packages[$featurename];
}
if (!empty($packages[$featurename]) && $this->package->getBundle() !== $this->bundle && $form_state->isValueEmpty('package')) {
// Make sure the current bundle matches what is stored in the package.
// But only do this if the Package value hasn't been manually changed.
$bundle = $this->assigner->getBundle($this->package->getBundle());
if (empty($bundle)) {
// Create bundle if it doesn't exist yet.
$bundle = $this->assigner->createBundleFromDefault($this->package->getBundle());
}
$this->bundle = $bundle->getMachineName();
$this->assigner->reset();
$this->assigner->assignConfigPackages(TRUE);
$packages = $this->featuresManager->getPackages();
$this->package = $packages[$featurename];
}
$form = [
'#show_operations' => FALSE,
'#prefix' => '<div id="features-edit-wrapper" class="features-edit-wrapper clearfix">',
'#suffix' => '</div>',
];
$form['info'] = [
'#type' => 'fieldset',
'#title' => $this->t('General Information'),
'#tree' => FALSE,
'#weight' => 2,
'#prefix' => '<div id="features-export-info" class="features-export-info">',
'#suffix' => '</div>',
];
$form['info']['name'] = [
'#title' => $this->t('Name'),
'#description' => $this->t('Example: Image gallery') . ' (' . $this->t('Do not begin name with numbers.') . ')',
'#type' => 'textfield',
'#default_value' => $this->package->getName(),
];
if (!$bundle->isDefault()) {
$form['info']['name']['#description'] .= '<br/>' .
$this->t('The namespace "@name_" will be prepended to the machine name', ['@name' => $bundle->getMachineName()]);
}
$form['info']['machine_name'] = [
'#type' => 'machine_name',
'#title' => $this->t('Machine-readable name'),
'#description' => $this->t('Example: image_gallery') . ' ' . $this->t('May only contain lowercase letters, numbers and underscores.'),
'#required' => TRUE,
'#default_value' => $bundle->getShortName($this->package->getMachineName()),
'#machine_name' => [
'source' => ['info', 'name'],
'exists' => [$this, 'featureExists'],
],
];
if (!$bundle->isDefault()) {
$form['info']['machine_name']['#description'] .= '<br/>' .
$this->t('NOTE: Do NOT include the namespace prefix "@name_"; it will be added automatically.', ['@name' => $bundle->getMachineName()]);
}
$form['info']['description'] = [
'#title' => $this->t('Description'),
'#description' => $this->t('Provide a short description of what users should expect when they install your feature.'),
'#type' => 'textarea',
'#rows' => 3,
'#default_value' => $this->package->getDescription(),
];
$form['info']['package'] = [
'#title' => $this->t('Bundle'),
'#type' => 'select',
'#options' => $this->assigner->getBundleOptions(),
'#default_value' => $bundle->getMachineName(),
'#ajax' => [
'callback' => '::updateBundle',
'wrapper' => 'features-export-info',
],
];
$form['info']['version'] = [
'#title' => $this->t('Version'),
'#description' => $this->t('Examples: 8.x-1.0, 3.1.4'),
'#type' => 'textfield',
'#required' => FALSE,
'#default_value' => $this->package->getVersion(),
'#size' => 30,
];
[$full_name, $path] = $this->featuresManager->getExportInfo($this->package, $bundle);
$form['info']['directory'] = [
'#title' => $this->t('Path'),
'#description' => $this->t('Path to export package using Write action, relative to root directory.'),
'#type' => 'textfield',
'#required' => FALSE,
'#default_value' => $path,
'#size' => 30,
];
$require_all = $this->package->getRequiredAll();
$form['info']['require_all'] = [
'#type' => 'checkbox',
'#title' => $this->t('Mark all config as required'),
'#default_value' => $this->package->getRequiredAll(),
'#description' => $this->t('Required config will be assigned to this feature regardless of other assignment plugins.'),
];
$form['conflicts'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow conflicts'),
'#default_value' => $this->allowConflicts,
'#description' => $this->t('Allow configuration to be exported to more than one feature.'),
'#weight' => 8,
'#ajax' => [
'callback' => '::updateForm',
'wrapper' => 'features-edit-wrapper',
],
'#wrapper_attributes' => [
'class' => ['features-ui-conflicts'],
],
];
$generation_info = [];
if ($this->currentUser->hasPermission('export configuration')) {
// Offer available generation methods.
$generation_info = $this->generator->getGenerationMethods();
// Sort generation methods by weight.
uasort($generation_info, [SortArray::class, 'sortByWeightElement']);
}
$form['actions'] = ['#type' => 'actions', '#tree' => TRUE];
foreach ($generation_info as $method_id => $method) {
$form['actions'][$method_id] = [
'#type' => 'submit',
'#name' => $method_id,
'#value' => $this->t('@name', ['@name' => $method['name']]),
'#attributes' => [
'title' => Html::escape($method['description']),
],
];
}
// Build the Component Listing panel on the right.
$form['export'] = $this->buildComponentList($form_state);
if (!empty($this->missing)) {
if ($this->allowConflicts) {
$form['actions']['#prefix'] = '<strong>' .
$this->t('WARNING: Package contains configuration missing from site.') . '<br>' .
$this->t('This configuration will be removed if you export it.') .
'</strong>';
}
else {
foreach ($generation_info as $method_id => $method) {
unset($form['actions'][$method_id]);
}
$form['actions']['#prefix'] = '<strong>' .
$this->t('Package contains configuration missing from site.') . '<br>' .
$this->t('Import the feature to create the missing config before you can export it.') . '<br>' .
$this->t('Or, enable the Allow Conflicts option above.') .
'</strong>';
}
$form['actions']['import_missing'] = [
'#type' => 'submit',
'#name' => 'import_missing',
'#value' => $this->t('Import Missing'),
'#attributes' => [
'title' => $this->t('Import only the missing configuration items.'),
],
];
}
$form['#attached'] = [
'library' => [
'features_ui/drupal.features_ui.admin',
],
'drupalSettings' => [
'features' => [
'excluded' => $this->excluded,
'required' => $this->required,
'conflicts' => $this->conflicts,
'autodetect' => TRUE,
],
],
];
return $form;
}
/**
* Provides an ajax callback for handling conflict checkbox.
*/
public function updateForm($form, FormStateInterface $form_state) {
return $form;
}
/**
* Provides an ajax callback for handling switching the bundle selector.
*/
public function updateBundle($form, FormStateInterface $form_state) {
$old_bundle = $this->assigner->getBundle($this->oldBundle);
$bundle_name = $form_state->getValue('package');
$bundle = $this->assigner->getBundle($bundle_name);
if (isset($bundle) && isset($old_bundle)) {
$short_name = $old_bundle->getShortName($this->package->getMachineName());
if ($bundle->isDefault()) {
$short_name = $old_bundle->getFullName($short_name);
}
$this->package->setMachineName($bundle->getFullName($short_name));
$form['info']['machine_name']['#value'] = $bundle->getShortName($this->package->getMachineName());
}
return $form['info'];
}
/**
* Callback for machine_name exists()
*
* @param $value
* @param $element
* @param $form_state
*
* @return bool
*/
public function featureExists($value, $element, $form_state) {
$bundle = $this->assigner->getBundle($this->bundle);
$value = $bundle->getFullName($value);
$packages = $this->featuresManager->getPackages();
// A package may conflict only if it's been exported.
return (isset($packages[$value]) && ($packages[$value]->getState() !== FeaturesManagerInterface::STATUS_NO_EXPORT)) || $this->moduleHandler->moduleExists($value);
}
/**
* Returns the render array elements for the Components selection on the Edit
* form.
*/
protected function buildComponentList(FormStateInterface $form_state) {
$element = [
'#type' => 'fieldset',
'#title' => $this->t('Components'),
'#description' => $this->t('Expand each component section and select which items should be included in this feature export.'),
'#tree' => FALSE,
'#prefix' => '<div id="features-export-wrapper" class="features-export-wrapper js-features-export-wrapper">',
'#suffix' => '</div>',
'#weight' => 1,
];
// Filter field used in JavaScript, so JavaScript will unhide it.
$element['features_filter_wrapper'] = [
'#type' => 'fieldgroup',
'#title' => $this->t('Filters'),
'#title_display' => 'invisible',
'#tree' => FALSE,
'#prefix' => '<div id="features-filter" class="features-filter js-features-filter visually-hidden">',
'#suffix' => '</div>',
'#weight' => -10,
'#attributes' => [
'class' => ['features-filter__fieldset', 'container-inline'],
],
];
$element['features_filter_wrapper']['features_filter'] = [
'#type' => 'textfield',
'#title' => $this->t('Search'),
'#hidden' => TRUE,
'#default_value' => '',
'#attributes' => ['class' => ['js-features-filter-input']],
'#suffix' => "<span class='features-filter-clear js-features-filter-clear'>" . $this->t('Clear') . "</span>",
];
$element['features_filter_wrapper']['checkall'] = [
'#type' => 'checkbox',
'#default_value' => FALSE,
'#hidden' => TRUE,
'#title' => $this->t('Select all'),
'#attributes' => [
'class' => [
'features-checkall',
'js-features-checkall',
'features-filter',
'js-features-filter',
],
],
'#label_attributes' => [
'title' => $this->t('Select all currently expanded configurations'),
],
];
$element['features_filter_wrapper']['toggle-components'] = [
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => $this->t('Expand all'),
'#attributes' => [
'href' => '#',
'title' => $this->t('Expand/collapse components in order to "Select all".'),
'class' => [
'features-toggle-components',
'features-filter',
'js-features-filter',
],
],
];
$element['features_filter_wrapper']['hide-components'] = [
'#type' => 'select',
'#title' => $this->t('Hide'),
'#options' => [
'included' => $this->t('Normal'),
'included+groups' => $this->t('Normal (and empty groups)'),
'added' => $this->t('Added'),
'detected' => $this->t('Auto detected'),
'conflict' => $this->t('Conflict'),
],
'#empty_option' => $this->t('- None -'),
'#default_value' => '',
'#attributes' => [
'class' => [
'features-hide-component',
'features-filter',
'js-features-filter',
],
'title' => $this->t('Hide specific Features components'),
],
];
$element['features_filter_wrapper']['features_legend'] = [
'#type' => 'fieldset',
'#title' => $this->t('Legend'),
'#tree' => FALSE,
'#prefix' => '<div id="features-legend">',
'#suffix' => '</div>',
];
$element['features_filter_wrapper']['features_legend']['legend'] = [
'#markup' => implode('', [
"<span class='features-legend-component features-legend-component--included'>" . $this->t('Normal') . '</span> ',
"<span class='features-legend-component features-legend-component--added'>" . $this->t('Added') . '</span> ',
"<span class='features-legend-component features-legend-component--detected'>" . $this->t('Auto detected') . '</span> ',
"<span class='features-legend-component features-legend-component--conflict'>" . $this->t('Conflict') . '</span> ',
]),
];
$sections = ['included', 'detected', 'added'];
$config_types = $this->featuresManager->listConfigTypes();
// Generate the export array for the current feature and user selections.
$export = $this->getComponentList($form_state);
foreach ($export['components'] as $component => $component_info) {
$component_items_count = count($component_info['_features_options']['sources']);
$label = new FormattableMarkup('@component (<span class="component-count js-component-count">@count</span>)',
[
'@component' => $config_types[$component],
'@count' => $component_items_count,
]
);
$count = 0;
foreach ($sections as $section) {
$count += count($component_info['_features_options'][$section]);
}
$extra_class = ($count == 0) ? 'features-export-empty' : '';
$component_name = str_replace('_', '-', Html::escape($component));
if ($count + $component_items_count > 0) {
$element[$component] = [
'#markup' => '',
'#tree' => TRUE,
];
$element[$component]['sources'] = [
'#type' => 'details',
'#title' => $label,
'#tree' => TRUE,
'#open' => FALSE,
'#attributes' => [
'class' => [
'features-export-component',
'js-features-export-component',
],
],
'#prefix' => "<div class='features-export-parent js-features-export-parent js-component--name-$component'>",
];
$element[$component]['sources']['selected'] = [
'#type' => 'checkboxes',
'#id' => "edit-sources-$component_name",
'#options' => $this->domDecodeOptions($component_info['_features_options']['sources']),
'#default_value' => $this->domDecodeOptions($component_info['_features_selected']['sources'], FALSE),
'#attributes' => ['class' => ['component-select']],
'#prefix' => "<span class='components-select js-components-select'>",
'#suffix' => '</span>',
];
$element[$component]['before-list'] = [
'#markup' => "<div class='component-list js-component-list features-export-list js-features-export-list $extra_class'>",
];
foreach ($sections as $section) {
$element[$component][$section] = [
'#type' => 'checkboxes',
'#options' => !empty($component_info['_features_options'][$section]) ? $this->domDecodeOptions($component_info['_features_options'][$section]) : [],
'#default_value' => !empty($component_info['_features_selected'][$section]) ? $this->domDecodeOptions($component_info['_features_selected'][$section], FALSE) : [],
'#attributes' => [
'class' => ['component-' . $section, 'js-component-' . $section],
],
'#prefix' => "<span class='components-$section js-components-$section'>",
'#suffix' => '</span>',
];
}
// Close both the before-list as well as the sources div.
$element[$component]['after-list'] = [
'#markup' => "</div></div>",
];
}
}
$element['features_missing'] = [
'#theme' => 'item_list',
'#wrapper_attributes' => [
'class' => [
'features-missing-items',
],
],
'#items' => $export['missing'],
'#title' => $this->t('Configuration missing from active site:'),
'#suffix' => '<div class="description">' . $this->t('Import the feature to create the missing config listed above.') . '</div>',
'#access' => !empty($export['missing']),
];
return $element;
}
/**
* Returns the full feature export array based upon user selections in
* form_state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Optional form_state information for user selections. Can be updated to
* reflect new selection status.
*
* @return \Drupal\features\Package
* New export array to be exported
* array['components'][$component_name] = $component_info
* $component_info['_features_options'][$section] is list of available options
* $component_info['_features_selected'][$section] is option state TRUE/FALSE
* $section = array('sources', included', 'detected', 'added')
* sources - options that are available to be added to the feature
* included - options that have been previously exported to the feature
* detected - options that have been auto-detected
* added - newly added options to the feature
*
* NOTE: This routine gets a bit complex to handle all of the different
* possible user checkbox selections and de-selections.
* Cases to test:
* 1a) uncheck Included item -> mark as Added but unchecked
* 1b) re-check unchecked Added item -> return it to Included check item
* 2a) check Sources item -> mark as Added and checked
* 2b) uncheck Added item -> return it to Sources as unchecked
* 3a) uncheck Included item that still exists as auto-detect -> mark as
* Detected but unchecked
* 3b) re-check Detected item -> return it to Included and checked
* 4a) check Sources item should also add any auto-detect items as Detected
* and checked
* 4b) uncheck Sources item with auto-detect and auto-detect items should
* return to Sources and unchecked
* 5a) uncheck a Detected item -> refreshing page should keep it as
* unchecked Detected
* 6) when nothing changes, refresh should not change any state
* 7) should never see an unchecked Included item
*/
protected function getComponentList(FormStateInterface $form_state) {
$config = $this->featuresManager->getConfigCollection();
$package_name = $this->package->getMachineName();
// Auto-detect dependencies for included config.
$package_config = $this->package->getConfig();
if (!empty($this->package->getConfigOrig())) {
$package_config = array_unique(array_merge($package_config, $this->package->getConfigOrig()));
}
if (!empty($package_config)) {
$this->featuresManager->assignConfigDependents($package_config, $package_name);
}
$packages = $this->featuresManager->getPackages();
// Re-fetch the package in case config was updated with Dependents above.
$this->package = $packages[$package_name];
// Make a map of all config data.
$components = [];
$this->conflicts = [];
foreach ($config as $item_name => $item) {
if (($item->getPackage() != $package_name) &&
!empty($packages[$item->getPackage()]) && ($packages[$item->getPackage()]->getStatus() != FeaturesManagerInterface::STATUS_NO_EXPORT)) {
$this->conflicts[$item->getType()][$item->getShortName()] = $item->getLabel();
}
if ($this->allowConflicts
|| !isset($this->conflicts[$item->getType()][$item->getShortName()])
|| ($this->package->getConfigOrig() && in_array($item_name, $this->package->getConfigOrig()))) {
$components[$item->getType()][$item->getShortName()] = $item->getLabel();
}
}
// Make a map of the config data already exported to the Feature.
$this->missing = [];
$exported_features_info = [];
foreach ($this->package->getConfigOrig() as $item_name) {
// Make sure the extension provided item exists in the active
// configuration storage.
if (isset($config[$item_name])) {
$item = $config[$item_name];
// Remove any conflicts if those are not being allowed.
// if ($this->allowConflicts || !isset($this->conflicts[$item['type']][$item['name_short']])) {
$exported_features_info[$item->getType()][$item->getShortName()] = $item->getLabel();
// }
}
else {
$this->missing[] = $item_name;
}
}
$exported_features_info['dependencies'] = $this->package->getDependencyInfo();
// Make a map of any config specifically excluded and/or required.
foreach (['excluded', 'required'] as $constraint) {
$this->{$constraint} = [];
$info = !empty($this->package->{'get' . $constraint}()) ? $this->package->{'get' . $constraint}() : [];
// $info may be boolean.
if (is_array($info)) {
foreach ($info as $item_name) {
if (!isset($config[$item_name])) {
continue;
}
$item = $config[$item_name];
$this->{$constraint}[$item->getType()][$item->getShortName()] = $item->getLabel();
}
}
}
// Make a map of the config data to be exported within the Feature.
$new_features_info = [];
foreach ($this->package->getConfig() as $item_name) {
$item = $config[$item_name];
$new_features_info[$item->getType()][$item->getShortName()] = $item->getLabel();
}
$new_features_info['dependencies'] = $this->package->getDependencies();
// Assemble the combined component list.
$config_new = [];
$sections = ['sources', 'included', 'detected', 'added'];
// Generate list of config to be exported.
$config_count = [];
foreach ($components as $component => $component_info) {
// User-selected components take precedence.
$config_new[$component] = [];
$config_count[$component] = 0;
// Add selected items from Sources checkboxes.
if (!$form_state->isValueEmpty([$component, 'sources', 'selected'])) {
// Don't use the array_merge function, otherwise configs like
// "metatag.metatag_defaults.404" will have the key "404" be reindexed.
$config_new[$component] = $config_new[$component] + $this->domDecodeOptions(array_filter($form_state->getValue([
$component,
'sources',
'selected',
])));
$config_count[$component]++;
}
// Add selected items from already Included, newly Added, auto-detected
// checkboxes.
foreach (['included', 'added', 'detected'] as $section) {
if (!$form_state->isValueEmpty([$component, $section])) {
$config_new[$component] = $config_new[$component] + $this->domDecodeOptions(array_filter($form_state->getValue([$component, $section])));
$config_count[$component]++;
}
}
// Only fallback to an existing feature's values if there are no export
// options for the component.
if ($component == 'dependencies') {
if (($config_count[$component] == 0) && !empty($exported_features_info['dependencies'])) {
$config_new[$component] = array_combine($exported_features_info['dependencies'], $exported_features_info['dependencies']);
}
}
elseif (($config_count[$component] == 0) && !empty($exported_features_info[$component])) {
$config_names = array_keys($exported_features_info[$component]);
$config_new[$component] = array_combine($config_names, $config_names);
}
}
// Generate new populated feature.
$export['package'] = $this->package;
$export['config_new'] = $config_new;
// Now fill the $export with categorized sections of component options
// based upon user selections and de-selections.
foreach ($components as $component => $component_info) {
$component_export = $component_info;
foreach ($sections as $section) {
$component_export['_features_options'][$section] = [];
$component_export['_features_selected'][$section] = [];
}
if (!empty($component_info)) {
$exported_components = !empty($exported_features_info[$component]) ? $exported_features_info[$component] : [];
$new_components = !empty($new_features_info[$component]) ? $new_features_info[$component] : [];
foreach ($component_info as $key => $label) {
$config_name = $this->featuresManager->getFullName($component, $key);
// If checkbox in Sources is checked, move it to Added section.
if (!$form_state->isValueEmpty([$component, 'sources', 'selected', $key])) {
$form_state->setValue([$component, 'sources', 'selected', $key], FALSE);
$form_state->setValue([$component, 'added', $key], 1);
$component_export['_features_options']['added'][$key] = $this->configLabel($component, $key, $label);
$component_export['_features_selected']['added'][$key] = $key;
// If this was previously excluded, we don't need to set it as
// required because it was automatically assigned.
if (isset($this->excluded[$component][$key])) {
unset($this->excluded[$component][$key]);
}
else {
$this->required[$component][$key] = $key;
}
}
elseif (isset($new_components[$key]) || isset($config_new[$component][$key])) {
// Option is in the New exported array.
if (isset($exported_components[$key])) {
// Option was already previously exported so it's part of the
// Included checkboxes.
$section = 'included';
$default_value = $key;
// If Included item was un-selected (removed from export
// $config_new) but was re-detected in the $new_components
// means it was an auto-detect that was previously part of the
// export and is now de-selected in UI.
if ($form_state->isSubmitted() &&
($form_state->hasValue([$component, 'included', $key]) ||
($form_state->isValueEmpty([$component, 'detected', $key]))) &&
empty($config_new[$component][$key])) {
$section = 'detected';
$default_value = FALSE;
}
// Unless it's unchecked in the form, then move it to Newly
// disabled item.
elseif ($form_state->isSubmitted() &&
$form_state->isValueEmpty([$component, 'added', $key]) &&
$form_state->isValueEmpty([$component, 'detected', $key]) &&
$form_state->isValueEmpty([$component, 'included', $key])) {
$section = 'added';
$default_value = FALSE;
}
}
else {
// Option was in New exported array, but NOT in already exported
// so it's a user-selected or an auto-detect item.
$section = 'detected';
$default_value = NULL;
// Check for item explicitly excluded.
if (isset($this->excluded[$component][$key]) && !$form_state->isSubmitted()) {
$default_value = FALSE;
}
else {
$default_value = $key;
}
// If it's already checked in Added or Sources, leave it in Added
// as checked.
if ($form_state->isSubmitted() &&
(!$form_state->isValueEmpty([$component, 'added', $key]) ||
!$form_state->isValueEmpty([$component, 'sources', 'selected', $key]))) {
$section = 'added';
$default_value = $key;
}
// If it's already been unchecked, leave it unchecked.
elseif ($form_state->isSubmitted() &&
$form_state->isValueEmpty([$component, 'sources', 'selected', $key]) &&
$form_state->isValueEmpty([$component, 'detected', $key]) &&
!$form_state->hasValue([$component, 'added', $key])) {
$section = 'detected';
$default_value = FALSE;
}
}
$component_export['_features_options'][$section][$key] = $this->configLabel($component, $key, $label);
$component_export['_features_selected'][$section][$key] = $default_value;
// Save which dependencies are specifically excluded from
// auto-detection.
if (($section == 'detected') && ($default_value === FALSE)) {
// If this was previously required, we don't need to set it as
// excluded because it wasn't automatically assigned.
if (!isset($this->required[$component][$key]) || ($this->package->getRequired() === TRUE)) {
$this->excluded[$component][$key] = $key;
}
unset($this->required[$component][$key]);
// Remove excluded item from export.
if ($component == 'dependencies') {
$export['package']->removeDependency($key);
}
else {
$export['package']->removeConfig($config_name);
}
}
else {
unset($this->excluded[$component][$key]);
}
// Remove the 'input' and set the 'values' so Drupal stops looking
// at 'input'.
if ($form_state->isSubmitted()) {
if (!$default_value) {
$form_state->setValue([$component, $section, $key], FALSE);
}
else {
$form_state->setValue([$component, $section, $key], 1);
}
}
}
elseif (!$form_state->isSubmitted() && isset($exported_components[$key])) {
// Component is not part of new export, but was in original export.
// Mark component as Added when creating initial form.
$component_export['_features_options']['added'][$key] = $this->configLabel($component, $key, $label);
$component_export['_features_selected']['added'][$key] = $key;
}
else {
// Option was not part of the new export.
$added = FALSE;
foreach (['included', 'added'] as $section) {
// Restore any user-selected checkboxes.
if (!$form_state->isValueEmpty([$component, $section, $key])) {
$component_export['_features_options'][$section][$key] = $this->configLabel($component, $key, $label);
$component_export['_features_selected'][$section][$key] = $key;
$added = TRUE;
}
}
if (!$added) {
// If not Included or Added, then put it back in the unchecked
// Sources checkboxes.
$component_export['_features_options']['sources'][$key] = $this->configLabel($component, $key, $label);
$component_export['_features_selected']['sources'][$key] = FALSE;
}
}
}
}
$export['components'][$component] = $component_export;
}
$export['features_exclude'] = $this->excluded;
$export['features_require'] = $this->required;
$export['conflicts'] = $this->conflicts;
$export['missing'] = $this->missing;
return $export;
}
/**
* Returns a formatted and sanitized label for a config item.
*
* @param string $type
* The config type.
* @param string $key
* The short machine name of the item.
* @param string $label
* The human label for the item.
*/
protected function configLabel($type, $key, $label) {
$value = Html::escape($label);
if ($key != $label) {
$value .= ' <span class="config-name">(' . Html::escape($key) . ')</span>';
}
if (isset($this->conflicts[$type][$key])) {
// Show what package the conflict is stored in.
$config = $this->featuresManager->getConfigCollection();
$config_name = $this->featuresManager->getFullName($type, $key);
$package_name = isset($config[$config_name]) ? $config[$config_name]->getPackage() : '';
// Get the full machine name instead of the short name.
$packages = $this->featuresManager->getPackages();
if (isset($packages[$package_name])) {
$package_name = $packages[$package_name]->getMachineName();
}
$value .= ' <span class="config-name">[' . $this->t('in') . ' ' . Html::escape($package_name) . ']</span>';
}
return Xss::filterAdmin($value);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$bundle = $this->assigner->getBundle($this->bundle);
$this->assigner->assignConfigPackages();
$this->package->setName($form_state->getValue('name'));
$this->package->setMachineName($form_state->getValue('machine_name'));
$this->package->setDescription($form_state->getValue('description'));
$this->package->setVersion($form_state->getValue('version'));
$this->package->setDirectory($form_state->getValue('directory'));
$this->package->setBundle($bundle->getMachineName());
// Save it first just to create it in case it's a new package.
$this->featuresManager->setPackage($this->package);
$config = $this->updatePackageConfig($form_state);
$this->featuresManager->assignConfigPackage($this->package->getMachineName(), $config, TRUE);
$this->package->setExcluded($this->updateExcluded());
if ($form_state->getValue('require_all')) {
$this->package->setRequired(TRUE);
}
else {
$required = $this->updateRequired();
$this->package->setRequired($required);
}
// Now save it with the selected config data.
$this->featuresManager->setPackage($this->package);
$method_id = NULL;
$trigger = $form_state->getTriggeringElement();
$op = $form_state->getValue('op');
if (!empty($trigger) && empty($op)) {
$method_id = $trigger['#name'];
}
// Set default redirect, but allow generators to change it later.
$form_state->setRedirect('features.edit', ['featurename' => $this->package->getMachineName()]);
if ($method_id == 'import_missing') {
$this->importMissing();
}
elseif (!empty($method_id)) {
$packages = [$this->package->getMachineName()];
$this->generator->generatePackages($method_id, $bundle, $packages);
$this->generator->applyExportFormSubmit($method_id, $form, $form_state);
}
$this->assigner->setCurrent($bundle);
}
/**
* Updates the config stored in the package from the current edit form.
*
* @return array
* Config array to be exported.
*/
protected function updatePackageConfig(FormStateInterface $form_state) {
$config = [];
$components = $this->getComponentList($form_state);
foreach ($components['config_new'] as $config_type => $items) {
foreach ($items as $name) {
$config[] = $this->featuresManager->getFullName($config_type, $name);
}
}
return $config;
}
/**
* Imports the configuration missing from the active store.
*/
protected function importMissing() {
$config = $this->featuresManager->getConfigCollection();
$missing = $this->featuresManager->reorderMissing($this->missing);
foreach ($missing as $config_name) {
if (!isset($config[$config_name])) {
$item = $this->featuresManager->getConfigType($config_name);
$type = ConfigurationItem::fromConfigStringToConfigType($item['type']);
try {
$this->configRevert->import($type, $item['name_short']);
$this->messenger()->addStatus($this->t('Imported @name', ['@name' => $config_name]));
} catch (\Exception $e) {
$this->messenger()->addError($this->t('Error importing @name : @message',
['@name' => $config_name, '@message' => $e->getMessage()]));
}
}
}
}
/**
* Updates the list of excluded config.
*
* @return array
* The list of excluded config in a simple array of full config names
* suitable for storing in the info.yml file.
*/
protected function updateExcluded() {
return $this->updateConstrained('excluded');
}
/**
* Updates the list of required config.
*
* @return array
* The list of required config in a simple array of full config names
* suitable for storing in the info.yml file.
*/
protected function updateRequired() {
return $this->updateConstrained('required');
}
/**
* Returns a list of constrained (excluded or required) configuration.
*
* @param string $constraint
* The constraint (excluded or required).
*
* @return array
* The list of constrained config in a simple array of full config names
* suitable for storing in the info.yml file.
*/
protected function updateConstrained($constraint) {
$constrained = [];
foreach ($this->{$constraint} as $type => $item) {
foreach ($item as $name => $value) {
$constrained[] = $this->featuresManager->getFullName($type, $name);
}
}
return $constrained;
}
/**
* Encodes a given key.
*
* @param string $key
* The key to encode.
*
* @return string
* The encoded key.
*/
protected function domEncode($key) {
$replacements = $this->domEncodeMap();
return strtr($key, $replacements);
}
/**
* Decodes a given key.
*
* @param string $key
* The key to decode.
*
* @return string
* The decoded key.
*/
protected function domDecode($key) {
$replacements = array_flip($this->domEncodeMap());
return strtr($key, $replacements);
}
/**
* Decodes an array of option values that have been encoded by
* features_dom_encode_options().
*
* @param array $options
* The key to encode.
* @param bool $keys_only
* Whether to decode only the keys.
*
* @return array
* An array of encoded options.
*/
protected function domDecodeOptions(array $options, $keys_only = FALSE) {
$replacements = array_flip($this->domEncodeMap());
$encoded = [];
foreach ($options as $key => $value) {
$encoded[strtr($key, $replacements)] = $keys_only ? $value : strtr($value, $replacements);
}
return $encoded;
}
/**
* Returns encoding map for decode and encode options.
*
* @return array
* An encoding map.
*/
protected function domEncodeMap() {
return [
':' => '__' . ord(':') . '__',
'/' => '__' . ord('/') . '__',
',' => '__' . ord(',') . '__',
'.' => '__' . ord('.') . '__',
'<' => '__' . ord('<') . '__',
'>' => '__' . ord('>') . '__',
'%' => '__' . ord('%') . '__',
')' => '__' . ord(')') . '__',
'(' => '__' . ord('(') . '__',
];
}
}
