association-1.0.0-alpha2/src/Behavior/Form/ConfigureManifestBehaviorForm.php
src/Behavior/Form/ConfigureManifestBehaviorForm.php
<?php namespace Drupal\association\Behavior\Form; use Drupal\association\Entity\AssociationTypeInterface; use Drupal\association\EntityAdapterManagerInterface; use Drupal\association\Plugin\AssociationPluginFormInterface; use Drupal\association\Plugin\BehaviorInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\SortArray; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\RemoveCommand; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\SubformStateInterface; use Drupal\Core\Plugin\PluginFormBase; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\toolshed\Strategy\Exception\StrategyException; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin form for editing the EntityManifestBehavior configurations. */ class ConfigureManifestBehaviorForm extends PluginFormBase implements AssociationPluginFormInterface, ContainerInjectionInterface { use StringTranslationTrait; use DependencySerializationTrait; /** * The plugin being configured by this plugin form. * * @var \Drupal\association\Plugin\BehaviorInterface */ protected $plugin; /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Association entity adapter plugin manager. * * @var \Drupal\association\EntityAdapterManagerInterface */ protected $adapterManager; /** * The form element to use for multi-select. * * @var string */ protected $multiselectType = 'select'; /** * The entity bundle options that can be used for the tag allowed bundles. * * Array of values meant to be used as available options for a select form * element, when building out the association link tag definitions. * * @var \Drupal\Core\StringTranslation\TranslatableMarkup[]|string[] */ protected $entityBundleOpts; /** * Create a new instance of the ConfigureManifestBehaviorPluginForm form. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\association\EntityAdapterManagerInterface $entity_adapter_manager * Association entity adapter plugin manager. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityAdapterManagerInterface $entity_adapter_manager) { $this->entityTypeManager = $entity_type_manager; $this->adapterManager = $entity_adapter_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { $instance = new static( $container->get('entity_type.manager'), $container->get('plugin.manager.association.entity_adapter') ); // Prefer select2 if it is available. Otherwise Chosen and Core select // element both work using "select" as the element type. $moduleHandler = $container->get('module_handler'); if ($moduleHandler->moduleExists('select2')) { $instance->setMultiselectType('select2'); } elseif ($moduleHandler->moduleExists('a11y_autocomplete_element')) { $instance->setMultiselectType('a11y_autocomplete'); } return $instance; } /** * Set the form element type to use for multiple select. * * Use "select2" if the select2 module is enabled. The "select" type works * for the Drupal core and chosenjs . * * @param string $select_type * The type of select element type to use when creating multi-select form * elements for behavior configurations. */ public function setMultiselectType($select_type) { $this->multiselectType = $select_type; } /** * Callback function to check if entity association link tag already exists. * * @param string $tag * The suggested machine name to use for this association link tag. * @param array $element * The machine name form element. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state, build info and values. * * @return bool * Returns TRUE if this tag is already being used in this entity manifest, * or will return FALSE otherwise. */ public static function linkTagExists($tag, array $element, FormStateInterface $form_state): bool { $parents = array_slice($element['#parents'], 0, -2); $values = $form_state->getValue($parents); return isset($values['manifest'][$tag]); } /** * Get the entity and bundle combinations that can be used in a manifest. * * @return array * The entity and bundle options that can be used in the select options * for the tags. */ protected function getEntityBundleOptions(): array { if (!isset($this->entityBundleOpts)) { $this->entityBundleOpts = []; foreach ($this->adapterManager->getEntityTypes() as $entityTypeId) { try { if ($adapter = $this->adapterManager->getAdapterByEntityType($entityTypeId)) { $entityTypeLabel = (string) $adapter->getLabel(); foreach ($adapter->getBundles() as $bundle => $bundleLabel) { $this->entityBundleOpts[$entityTypeLabel]["$entityTypeId:$bundle"] = $bundleLabel; } } } catch (StrategyException $e) { // Issue retrieving or loading the entity adapter plugin, skip this // type as the adapter provider might be missing or uninstalled. } } } return $this->entityBundleOpts; } /** * {@inheritdoc} */ public function buildConfigurationForm(array $elements, FormStateInterface $form_state, AssociationTypeInterface $association_type = NULL): array { $form_state->set('association_type', $association_type->id()); $hasData = $association_type->hasData(); $configuration = $form_state->getValues(); if (!$configuration) { $configuration = $this->plugin->getConfiguration(); // Convert from the plugin configuration format to one easier to // manipulate and represent with form elements. foreach ($configuration['manifest'] ?? [] as &$tagDef) { $entityBundles = []; foreach ($tagDef['entity_types'] ?? [] as $type => $bundles) { foreach ($bundles as $bundle) { $entityBundles["$type:$bundle"] = "$type:$bundle"; } } $tagDef['entity_types'] = $entityBundles; } unset($tagDef); } $elements['#id'] = 'association-entity-manifest-behavior-configuration'; // Create the table management for the manifests table. $elements['manifest'] = [ '#type' => 'table', '#tree' => TRUE, '#empty' => $this->t('No entity types have been configured yet. Add one below.'), '#header' => [ 'label' => $this->t('Label'), 'entity_info' => $this->t('Allowed bundle'), 'limit' => $this->t('Limit'), 'weight' => $this->t('Sort order'), ], '#tabledrag' => [ 'sort' => [ 'action' => 'order', 'relationship' => 'sibling', 'group' => 'entity_tag__weight', ], ], ]; if (!$hasData) { // Operations are enabled when there is existing data. $elements['manifest']['#header']['op'] = $this->t('Actions'); } foreach ($configuration['manifest'] ?? [] as $tag => $tagDef) { $elements['manifest'][$tag] = $this->buildEntityRow($tag, $tagDef, $hasData); } $elements['__add_tag'] = [ '#type' => 'details', '#title' => $this->t('Add entity definition'), '#open' => TRUE, '#process' => [static::class . '::addTagElementsProcess'], 'label' => [ '#type' => 'textfield', '#title' => $this->t('Label'), '#size' => 32, '#maxlength' => 32, '#default_value' => '', ], 'tag' => [ '#type' => 'machine_name', '#title' => $this->t('Machine name'), '#required' => FALSE, '#size' => 20, '#maxlength' => 32, '#machine_name' => [ 'exists' => static::class . '::linkTagExists', ], ], 'entity_types' => [ '#type' => $this->multiselectType, '#title' => $this->t('Allowed entity types'), '#options' => $this->getEntityBundleOptions(), '#multiple' => TRUE, ], 'add_submit' => [ '#type' => 'submit', '#value' => $this->t('Add'), '#ajax' => [ 'wrapper' => $elements['#id'], 'callback' => static::class . '::addTagAjax', ], '#validate' => [static::class . '::addTagValidate'], '#submit' => [static::class . '::addTagSubmit'], ], ]; return $elements; } /** * Convert an entry of entity manifest into a table row configuration. * * @param string $tag * The identifier for the entity entry for this manifest object. * @param array $entry * The relatable entity definition from the manifest. * @param bool $has_data * Does the association type (bundle) already have data? * * @return array * A table row of elements for the relatable entity tags. */ protected function buildEntityRow($tag, array $entry, $has_data) { $range = range(1, 10); $range = array_combine($range, $range); $rowId = 'manifest-behavior-row-id-' . Html::cleanCssIdentifier($tag); $row = [ '#attributes' => [ 'id' => $rowId, 'class' => ['draggable'], ], 'label' => [ '#type' => 'textfield', '#default_value' => $entry['label'], ], 'entity_types' => [ '#type' => $this->multiselectType, '#required' => TRUE, '#multiple' => TRUE, '#options' => $this->getEntityBundleOptions(), '#default_value' => $entry['entity_types'], ], 'limit' => [ '#type' => 'select', '#disabled' => $has_data, '#options' => [ BehaviorInterface::CARDINALITY_UNLIMITED => $this->t('Unlimited'), ] + $range, '#default_value' => $entry['limit'], ], 'weight' => [ '#type' => 'number', '#default_value' => $entry['weight'], '#attributes' => ['class' => ['entity_tag__weight']], ], ]; if (!$has_data) { $row['actions'] = [ '#type' => 'submit', '#value' => $this->t('Remove'), '#access' => !$has_data, '#name' => $rowId . '-remove-op', '#tagId' => $tag, '#validate' => [], '#submit' => [static::class . '::removeTagSubmit'], '#ajax' => [ 'wrapper' => $rowId, 'callback' => static::class . '::removeTagAjax', ], ]; } return $row; } /** * Add element adjust parents and validation scope of the add entity elements. * * At the original time of processing, the #array_parents and #parents * information is not available yet. Since we can't assuming the nesting * depth of these form elements, ee need wait for the form processing step * of the form lifecycle before making these adjustments. * * @param array $element * The add new entity elements to update. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state, build and values information. * @param array $complete_form * Reference to the entire form elements. * * @return array * Modified form elements. */ public static function addTagElementsProcess(array $element, FormStateInterface $form_state, array &$complete_form) { // At the time of form build, the #array_parents aren't available yet, // and we won't know the depth that the plugin form is being embedded into, // so we update the "source" in as the form element is built. $element['tag']['#machine_name']['source'] = array_merge($element['#array_parents'], ['label']); // Limit the validation errors to only the add submit values, and the // current definition manifest values. $valParents = $element['#parents']; array_pop($valParents); $element['add_submit']['#limit_validation_errors'][] = array_merge($valParents, ['__add_tag']); $element['add_submit']['#limit_validation_errors'][] = array_merge($valParents, ['manifest']); return $element; } /** * Form validation callback to ensure requirements for a new entity entry. * * @param array $form * Reference to the complete form structure and elements. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state, build info and values. */ public static function addTagValidate(array &$form, FormStateInterface $form_state) { $trigger = $form_state->getTriggeringElement(); $parents = array_slice($trigger['#parents'], 0, -1); $values = $form_state instanceof SubformStateInterface ? $form_state->getValue('__add_tag') : $form_state->getValue($parents); $elements = NestedArray::getValue($form, array_slice($trigger['#array_parents'], 0, -1)); if (empty($values['tag'])) { $form_state->setError($elements['tag'], t('An idenfifier is required to add a new entity association.')); } if (empty($values['entity_types'])) { $form_state->setError($elements['entity_types'], t('Entity types selections are required.')); } } /** * Form submit callback to add a relatable entity type to the manifest. * * @param array $form * Reference to the entire form structure for the edit form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state, build and values. */ public static function addTagSubmit(array &$form, FormStateInterface $form_state) { $trigger = $form_state->getTriggeringElement(); $parents = array_slice($trigger['#parents'], 0, -2); $form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state; $values = $form_state->getValue($parents); $addValues = $values['__add_tag']; // Reset the values, so the prefilled don't get repopulated. $addTagParents = array_merge($parents, ['__add_tag']); NestedArray::unsetValue($form_state->getUserInput(), $addTagParents); unset($values['__add_tag']); // Make sure that the current manifest setting is an array // and not a string or NULL. if (!(isset($values['manifest']) && is_array($values['manifest']))) { $values['manifest'] = []; } // Transfer the newly added entity into the manifest list. $values['manifest'][$addValues['tag']] = [ 'label' => $addValues['label'], 'entity_types' => $addValues['entity_types'], 'required' => 0, 'limit' => 1, 'weight' => 10, ]; $form_state->setValue($parents, $values); $form_state->setRebuild(TRUE); } /** * AJAX callback to update the association entity manifest. * * @param array $form * Reference to the entire form structure for the edit form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state, build and values. * * @return array * Renderable array with the manifest table updated elements. */ public static function addTagAjax(array &$form, FormStateInterface $form_state) { $element = $form_state->getTriggeringElement(); $parents = array_slice($element['#array_parents'], 0, -2); // Return the whole configuration table without the external wrappers. $render = NestedArray::getValue($form, $parents); unset($render['#prefix'], $render['#suffix']); return $render; } /** * Form submit callback to remove a association entity from the manifest. * * @param array $form * Reference to the entire form structure for the edit form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state, build and values. */ public static function removeTagSubmit(array &$form, FormStateInterface $form_state) { $trigger = $form_state->getTriggeringElement(); $parents = array_slice($trigger['#parents'], 0, -2); // Ensure that we are working with the complete form_state. $form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state; // Remove the entity from the manifest table. $values = $form_state->getValue($parents); unset($values[$trigger['#tagId']]); $form_state->setValue($parents, $values); $form_state->setRebuild(TRUE); } /** * AJAX callback to update the association entity manifest after a removal. * * @param array $form * Reference to the entire form structure for the edit form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state, build and values. * * @return \Drupal\Core\Ajax\AjaxResponse * An AJAX response which updates after the removal of a relatable entity. */ public static function removeTagAjax(array &$form, FormStateInterface $form_state) { $trigger = $form_state->getTriggeringElement(); $rowId = preg_replace('#-remove-op$#', '', $trigger['#name']); $response = new AjaxResponse(); if ($rowId) { $response->addCommand(new RemoveCommand("#{$rowId}")); } return $response; } /** * {@inheritdoc} */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { // @todo Ensure that the validation for manifest prevents the addition // of configurations that could cause data inconsistencies. $trigger = $form_state->getTriggeringElement(); $parents = array_slice($trigger['#parents'], 0, -2); $values = $form_state instanceof SubformStateInterface ? $form_state->getValues() : $form_state->getValue($parents); if (empty($values['manifest']) || !is_array($values['manifest'])) { $form_state->setErrorByName(implode('][', $parents), $this->t('Entity manigest cannot be empty.')); } } /** * {@inheritdoc} */ public function submitConfigurationForm(array &$element, FormStateInterface $form_state) { $trigger = $form_state->getTriggeringElement(); $parents = array_slice($trigger['#parents'], 0, -2); $values = $form_state instanceof SubformStateInterface ? $form_state->getValues() : $form_state->getValue($parents); if (empty($values['manifest']) || !is_array($values['manifest'])) { $values['manifest'] = []; } // Reorder the manifest based on the configured weights of the entries. uasort($values['manifest'], SortArray::class . '::sortByWeightElement'); // Renumber these weights so it'll be easier to manage in the future. $weight = 0; foreach ($values['manifest'] as &$entry) { $entry['required'] = 0; $entry['weight'] = $weight++; $entityTypes = []; foreach ($entry['entity_types'] as $def) { [$type, $bundle] = explode(':', $def, 2); $entityTypes[$type][$bundle] = $bundle; } $entry['entity_types'] = $entityTypes; } $this->plugin->setConfiguration([ 'manifest' => $values['manifest'], ]); } }