class FeaturesEditForm in Features 8.4
Same name and namespace in other branches
- 8.3 modules/features_ui/src/Form/FeaturesEditForm.php \Drupal\features_ui\Form\FeaturesEditForm
Defines the features settings form.
Hierarchy
- class \Drupal\Core\Form\FormBase implements ContainerInjectionInterface, FormInterface uses DependencySerializationTrait, LoggerChannelTrait, MessengerTrait, LinkGeneratorTrait, RedirectDestinationTrait, UrlGeneratorTrait, StringTranslationTrait
- class \Drupal\features_ui\Form\FeaturesEditForm
Expanded class hierarchy of FeaturesEditForm
1 string reference to 'FeaturesEditForm'
- features_ui.routing.yml in modules/
features_ui/ features_ui.routing.yml - modules/features_ui/features_ui.routing.yml
File
- modules/
features_ui/ src/ Form/ FeaturesEditForm.php, line 20
Namespace
Drupal\features_ui\FormView source
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;
/**
* Constructs a FeaturesEditForm object.
*
* @param \Drupal\features\FeaturesManagerInterface $features_manager
* The features manager.
* @param \Drupal\features\FeaturesAssignerInterface $assigner
* The feature assigner.
* @param \Drupal\features\FeaturesGeneratorInterface $generator
* The features generator.
* @param \Drupal\config_update\ConfigRevertInterface $config_revert
* The config revert.
*/
public function __construct(FeaturesManagerInterface $features_manager, FeaturesAssignerInterface $assigner, FeaturesGeneratorInterface $generator, ConfigRevertInterface $config_revert) {
$this->featuresManager = $features_manager;
$this->assigner = $assigner;
$this->generator = $generator;
$this->configRevert = $config_revert;
$this->excluded = [];
$this->required = [];
$this->conflicts = [];
$this->missing = [];
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container
->get('features.manager'), $container
->get('features_assigner'), $container
->get('features_generator'), $container
->get('features.config_update'));
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'features_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $featurename = '') {
$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,
];
list($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 (\Drupal::currentUser()
->hasPermission('export configuration')) {
// Offer available generation methods.
$generation_info = $this->generator
->getGenerationMethods();
// Sort generation methods by weight.
uasort($generation_info, '\\Drupal\\Component\\Utility\\SortArray::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 || \Drupal::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',
],
],
];
$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',
'#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>',
];
$element['features_legend'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Legend'),
'#tree' => FALSE,
'#prefix' => '<div id="features-legend">',
'#suffix' => '</div>',
];
$element['features_legend']['legend'] = [
'#markup' => "<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> ",
];
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;
}
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;
}
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('(') . '__',
];
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
DependencySerializationTrait:: |
protected | property | An array of entity type IDs keyed by the property name of their storages. | |
DependencySerializationTrait:: |
protected | property | An array of service IDs keyed by property name used for serialization. | |
DependencySerializationTrait:: |
public | function | 1 | |
DependencySerializationTrait:: |
public | function | 2 | |
FeaturesEditForm:: |
protected | property | Determine if conflicts are allowed to be added. | |
FeaturesEditForm:: |
protected | property | The package assigner. | |
FeaturesEditForm:: |
protected | property | Current bundle machine name. | |
FeaturesEditForm:: |
protected | property | The config reverter. | |
FeaturesEditForm:: |
protected | property | Config referenced by other packages. | |
FeaturesEditForm:: |
protected | property | Config to be specifically excluded. | |
FeaturesEditForm:: |
protected | property | The features manager. | |
FeaturesEditForm:: |
protected | property | The package generator. | |
FeaturesEditForm:: |
protected | property | Config missing from active site. | |
FeaturesEditForm:: |
protected | property | Previous bundle name for ajax processing. | |
FeaturesEditForm:: |
protected | property | Current package being edited. | |
FeaturesEditForm:: |
protected | property | Config to be specifically required. | |
FeaturesEditForm:: |
protected | function | Returns the render array elements for the Components selection on the Edit form. | |
FeaturesEditForm:: |
public | function |
Form constructor. Overrides FormInterface:: |
|
FeaturesEditForm:: |
protected | function | Returns a formatted and sanitized label for a config item. | |
FeaturesEditForm:: |
public static | function |
Instantiates a new instance of this class. Overrides FormBase:: |
|
FeaturesEditForm:: |
protected | function | Decodes a given key. | |
FeaturesEditForm:: |
protected | function | Decodes an array of option values that have been encoded by features_dom_encode_options(). | |
FeaturesEditForm:: |
protected | function | Encodes a given key. | |
FeaturesEditForm:: |
protected | function | Returns encoding map for decode and encode options. | |
FeaturesEditForm:: |
public | function | Callback for machine_name exists() | |
FeaturesEditForm:: |
protected | function | Returns the full feature export array based upon user selections in form_state. | |
FeaturesEditForm:: |
public | function |
Returns a unique string identifying the form. Overrides FormInterface:: |
|
FeaturesEditForm:: |
protected | function | Imports the configuration missing from the active store. | |
FeaturesEditForm:: |
public | function |
Form submission handler. Overrides FormInterface:: |
|
FeaturesEditForm:: |
public | function | Provides an ajax callback for handling switching the bundle selector. | |
FeaturesEditForm:: |
protected | function | Returns a list of constrained (excluded or required) configuration. | |
FeaturesEditForm:: |
protected | function | Updates the list of excluded config. | |
FeaturesEditForm:: |
public | function | Provides an ajax callback for handling conflict checkbox. | |
FeaturesEditForm:: |
protected | function | Updates the config stored in the package from the current edit form. | |
FeaturesEditForm:: |
protected | function | Updates the list of required config. | |
FeaturesEditForm:: |
public | function | Constructs a FeaturesEditForm object. | |
FormBase:: |
protected | property | The config factory. | 1 |
FormBase:: |
protected | property | The request stack. | 1 |
FormBase:: |
protected | property | The route match. | |
FormBase:: |
protected | function | Retrieves a configuration object. | |
FormBase:: |
protected | function | Gets the config factory for this form. | 1 |
FormBase:: |
private | function | Returns the service container. | |
FormBase:: |
protected | function | Gets the current user. | |
FormBase:: |
protected | function | Gets the request object. | |
FormBase:: |
protected | function | Gets the route match. | |
FormBase:: |
protected | function | Gets the logger for a specific channel. | |
FormBase:: |
protected | function |
Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait:: |
|
FormBase:: |
public | function | Resets the configuration factory. | |
FormBase:: |
public | function | Sets the config factory for this form. | |
FormBase:: |
public | function | Sets the request stack object to use. | |
FormBase:: |
public | function |
Form validation handler. Overrides FormInterface:: |
62 |
LinkGeneratorTrait:: |
protected | property | The link generator. | 1 |
LinkGeneratorTrait:: |
protected | function | Returns the link generator. | |
LinkGeneratorTrait:: |
protected | function | Renders a link to a route given a route name and its parameters. | |
LinkGeneratorTrait:: |
public | function | Sets the link generator service. | |
LoggerChannelTrait:: |
protected | property | The logger channel factory service. | |
LoggerChannelTrait:: |
protected | function | Gets the logger for a specific channel. | |
LoggerChannelTrait:: |
public | function | Injects the logger channel factory. | |
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
RedirectDestinationTrait:: |
protected | property | The redirect destination service. | 1 |
RedirectDestinationTrait:: |
protected | function | Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url. | |
RedirectDestinationTrait:: |
protected | function | Returns the redirect destination service. | |
RedirectDestinationTrait:: |
public | function | Sets the redirect destination service. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. | |
UrlGeneratorTrait:: |
protected | property | The url generator. | |
UrlGeneratorTrait:: |
protected | function | Returns the URL generator service. | |
UrlGeneratorTrait:: |
public | function | Sets the URL generator service. | |
UrlGeneratorTrait:: |
protected | function | Generates a URL or path for a specific route based on the given parameters. |