module_builder-8.x-3.x-dev/src/Form/ModuleHooksForm.php
src/Form/ModuleHooksForm.php
<?php
namespace Drupal\module_builder\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\module_builder\ExceptionHandler;
use DrupalCodeBuilder\Exception\SanityException;
use MutableTypedData\Data\DataItem;
/**
* Form for selecting hooks.
*/
class ModuleHooksForm extends ComponentSectionForm {
/**
* {@inheritdoc}
*/
protected function getFormComponentProperties(DataItem $data) {
$properties = parent::getFormComponentProperties($data);
// Remove the 'hooks' property, as this form handles it directly. It has to
// be declared in the entity annotation so that the 'misc' form doesn't pick
// it up.
$key = array_search('hooks', $properties);
if ($key) {
unset($properties[$key]);
}
return $properties;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
// $entity_component_data = $this->getComponentDataObject();
$entity_component_data = $form_state->get('data');
$form['hooks'] = [
'#type' => 'container',
];
$procedural_hooks_element = [
'#type' => 'container',
];
$procedural_hooks_element['filter'] = [
'#type' => 'search',
'#title' => $this->t('Filter'),
'#title_display' => 'invisible',
'#size' => 60,
'#placeholder' => $this->t('Filter by hook name'),
'#attributes' => [
'class' => ['hooks-filter-text'],
'data-container' => '.module-builder-hooks',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the hook name to filter by.'),
],
];
// Get the Task handler.
// No need to catch DCB exceptions; create() has already done that.
// TODO: inject.
$mb_task_handler_report = \Drupal::service('module_builder.drupal_code_builder')->getTask('ReportHookData');
// Call a method in the Task handler to perform the operation.
$hook_info = $mb_task_handler_report->listHookOptionsStructured();
$procedural_hooks_element['groups'] = [
'#type' => 'container',
'#attributes' => ['class' => ['module-builder-hooks']],
];
// Create a fieldset for each group, containing checkboxes.
foreach ($hook_info as $group => $hook_group_info) {
$procedural_hooks_element['groups'][$group] = array(
'#type' => 'details',
'#title' => $group,
//'#open' => TRUE,
);
$hook_names = array_keys($hook_group_info);
// Need to differentiate the key, otherwise FormAPI treats this as an
// error on submit.
$group_default_value = isset($entity_component_data['hooks']) ? array_intersect($hook_names, $entity_component_data['hooks']) : [];
$procedural_hooks_element['groups'][$group][$group . '_hooks'] = array(
'#type' => 'checkboxes',
'#options' => array_combine($hook_names, array_column($hook_group_info, 'name')),
'#default_value' => $group_default_value,
);
if (!empty($group_default_value)) {
$procedural_hooks_element['groups'][$group]['#open'] = TRUE;
}
foreach ($hook_group_info as $hook => $hook_info_single) {
$description = $hook_info_single['description'];
if ($hook_info_single['core']) {
// Get MAJOR, MINOR and PATCH components from the current version
// string.
[$major, $minor, $patch] = explode('.', \Drupal::VERSION);
// Documentation URLs on api.drupal.org changed from 9 onwards.
if ($major >= 9) {
// MAJOR version string.
$url_version_suffix = $major;
}
else {
// MAJOR.MINOR.x version string.
$url_version_suffix = implode('.', [$major, $minor, 'x']);
}
// External Uri.
$url = Url::fromUri('https://api.drupal.org/api/function/' . $hook . '/' . $url_version_suffix);
$description .= ' ' . Link::fromTextAndUrl(t('[documentation]'), $url)->toString();
}
$procedural_hooks_element['groups'][$group][$group . '_hooks'][$hook]['#description'] = $description;
}
}
$form['hooks']['procedural_hooks'] = $procedural_hooks_element;
$form_state->set('module_builder_groups', array_keys($hook_info));
$form['#attached']['library'][] = 'module_builder/hooks';
return $form;
}
/**
* {@inheritdoc}
*/
protected function buildComplexFormElement(array $element, FormStateInterface $form_state, DataItem $data): array {
$element = parent::buildComplexFormElement($element, $form_state, $data);
// Streamline hook implementation mutable data elements.
// Remove the notice about 'no additional properties' as it's quite noisy
// for hooks.
unset($element['count_notice']);
// Remove the button to update variants, as it takes up a lot of space and
// it's just as quick to remove the hook and add a new one.
unset($element[':update_variant']);
return $element;
}
/**
* {@inheritdoc}
*/
protected function buildMultipleDeltaFormElement(array $element, FormStateInterface $form_state, DataItem $data): array {
if ($data->getName() == 'hook_methods') {
return $this->builderHookMethodsTableFormElement($element, $form_state, $data);
}
else {
return parent::buildMultipleDeltaFormElement($element, $form_state, $data);
}
}
/**
* Builds a table form element for the hook_methods property.
*/
protected function builderHookMethodsTableFormElement(array $element, FormStateInterface $form_state, DataItem $data): array {
// Set up a wrapper for AJAX.
$wrapper_id = Html::getId($data->getAddress() . '-add-more-wrapper');
$element += [
'#type' => 'table',
'#caption' => $data->getDescription(),
'#attributes' => [
'id' => $wrapper_id,
'class' => ['hook_methods'],
],
'#header' => [
$this->t('Hook'),
$this->t('Token replacements'),
$this->t('Actions'),
],
];
$element['#empty'] = $this->t('There are no hook methods in this class. Add one by selecting a hook below.');
foreach ($data as $delta => $delta_item) {
$element[$delta] = [];
$element[$delta]['hook_name'] = [
'#type' => 'value',
'#value' => $delta_item->hook_name->value,
];
// This has to go inside the hidden value element, as otherwise the
// table element will treat the two as different cells.
// See https://www.drupal.org/project/drupal/issues/3520847.
$element[$delta]['hook_name'][':dummy_hook_name'] = [
'#markup' => $delta_item->hook_name->value,
];
if ($delta_item->hasProperty('hook_name_parameters')) {
$element[$delta]['hook_name_parameters'] = [
'#type' => 'textfield',
'#default_value' => implode(', ', $delta_item->hook_name_parameters->values()),
'#attributes' => [
'placeholder' => $this->t('Separate multiple tokens with a comma'),
],
];
}
else {
$element[$delta][':empty'] = [];
}
$element[$delta][':remove_button'] = [
'#type' => 'submit',
// Needs to be full address for uniquess in the whole form.
'#name' => $delta_item->getAddress() . '_remove_item',
'#data_address' => $delta_item->getAddress(),
'#ajax_parent_slice' => 2,
'#value' => $this->t('Remove hook method @human-index', [
'@human-index' => $delta + 1,
]),
'#limit_validation_errors' => [],
'#submit' => ['::removeItemSubmit'],
'#ajax' => [
'callback' => '::itemButtonAjax',
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
}
if ($data->mayAddItem()) {
$element = $this->buildAddItemButton($element, $form_state, $data);
$element[':variant_add'][':type_property']['#wrapper_attributes'] = ['colspan' => 2];
$element[':variant_add'][':add_button']['#value'] = $this->t('Add hook method');
}
return $element;
}
/**
* Copies top-level form values to entity properties
*
* This should not change existing entity properties that are not being edited
* by this form.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// Call the parent to handle all values that are beneath 'module'.
parent::copyFormValuesToEntity($entity, $form, $form_state);
// Handle the 'hooks' values separately.
$data = $form_state->get('data');
$entity_data_values = $entity->get('data');
$form_values = $form_state->getValues();
// We can't just iterate over $form_values, because of a core bug with
// EntityForm, so we need to know which keys to look at.
// See https://www.drupal.org/node/2665714.
$groups = $form_state->get('module_builder_groups');
$hooks = [];
foreach ($groups as $group) {
$group_values = $form_values[$group . '_hooks'];
// Filter out empty values. (FormAPI *still* doesn't do this???)
$group_hooks = array_filter($group_values);
// Store as a numeric array.
$group_hooks = array_keys($group_hooks);
$hooks = array_merge($group_hooks, $hooks);;
}
$entity_data_values['hooks'] = $hooks;
$entity->set('data', $entity_data_values);
}
}
