contacts_events-8.x-1.x-dev/modules/villages/src/Plugin/Field/FieldWidget/CampingVillageGroupWidget.php
modules/villages/src/Plugin/Field/FieldWidget/CampingVillageGroupWidget.php
<?php
namespace Drupal\contacts_events_villages\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\contacts_events\Entity\EventInterface;
use Drupal\contacts_events_villages\Plugin\VillageGroupTypeManager;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Widget for village group on bookings.
*
* @FieldWidget(
* id = "village_group_camping",
* label = @Translation("Camping Village Group"),
* field_types = {
* "entity_reference"
* }
* )
*/
class CampingVillageGroupWidget extends WidgetBase implements ContainerFactoryPluginInterface {
/**
* The village group storage service.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $villageGroupStorage;
/**
* The village group type plugin manager.
*
* @var \Drupal\contacts_events_villages\Plugin\VillageGroupTypeManager
*/
protected $villageTypeManager;
/**
* Constructs the Camping widget object.
*
* @param string $plugin_id
* The plugin_id for the widget.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the widget is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\contacts_events_villages\Plugin\VillageGroupTypeManager $village_type_manager
* The village group type plugin manager.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, VillageGroupTypeManager $village_type_manager) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->villageGroupStorage = $entity_type_manager->getStorage('c_events_village_group');
$this->villageTypeManager = $village_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$widget = new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('entity_type.manager'),
$container->get('plugin.manager.c_events_village_group_type')
);
$widget->setMessenger($container->get('messenger'));
return $widget;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items */
$booking = $items->getEntity();
$event = $booking->get('event')->entity;
if ($event->get('village_group_types')->isEmpty()) {
return FALSE;
}
// Store the event ID for ::massageFormValues.
$element['#event_id'] = $event->id();
$element['target_id'] = [
'#type' => 'radios',
'#title' => $element['#title'],
'#title_display' => 'invisible',
'#default_value' => $items->getValue()[0]['target_id'] ?? NULL,
'#required' => $element['#required'] ?? FALSE,
];
$options = [];
$subform = [];
$unexpanded_types = [];
$types = array_map(function ($item) {
return $item['value'];
}, $event->get('village_group_types')->getValue());
foreach ($types as $type) {
$definition = $this->villageTypeManager->getDefinition($type);
// Make sure the plugin exists.
if (!$definition) {
continue;
}
/** @var \Drupal\contacts_events_villages\Plugin\VillageGroupTypeInterface $instance */
$instance = $this->villageTypeManager->createInstance($type);
if ($instance->isExpanded()) {
foreach ($this->getEventGroups($event, $type) as $group) {
$options[$group->id()] = $group->label();
$element['target_id'][$group->id()]['#description'] = $group->getDescription();
}
}
else {
$unexpanded_types[$type] = $type;
$options[$type] = $definition['label'];
$element['target_id'][$type]['#description'] = $definition['description'] ?? '';
$subform[$type] = $instance->form($items);
}
}
$element['target_id']['#options'] = $options;
$element['#theme_wrappers'] = ['form_element'];
$element['#description_display'] = 'before';
// Check default target type.
if ($element['target_id']['#default_value']) {
$target_entity = $this->villageGroupStorage->load($element['target_id']['#default_value']);
if ($target_entity && in_array($target_entity->bundle(), $unexpanded_types)) {
$element['target_id']['#default_value'] = $target_entity->bundle();
}
}
if (!empty($subform)) {
$element['subform'] = $subform;
$element['subform']['#weight'] = 99;
$element['#process'][] = [$this, 'processElement'];
}
return $element;
}
/**
* Process callback to add widget option states to subforms.
*
* @param array $element
* The widget element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $complete_form
* The complete form.
*
* @return array
* The widget element.
*/
public function processElement(array $element, FormStateInterface $form_state, array &$complete_form) {
$nested_path = $this->getElementPathNotated($element);
foreach (Element::children($element['subform']) as $plugin) {
// Make the subform only visible when the appropriate option is selected.
if (!isset($element['subform'][$plugin]['#type'])) {
$element['subform'][$plugin]['#type'] = 'container';
}
$element['subform'][$plugin]['#states'] = [
'visible' => [
':input[name="' . $nested_path . '[target_id]"]' => ['value' => $plugin],
],
];
// Allow subform to do additional processing.
/** @var \Drupal\contacts_events_villages\Plugin\VillageGroupTypeInterface $plugin_instance */
$plugin_instance = $this->villageTypeManager->createInstance($plugin);
$plugin_instance->process($element['subform'][$plugin], $form_state, $complete_form, $nested_path);
// Setup validation of the subform.
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$element['subform'][$plugin]['#element_validate'][] = [$this, 'validateSubForm'];
}
// Workaround for massageFormValues() not getting $element.
$element['array_parents'] = [
'#type' => 'value',
'#value' => $element['#array_parents'],
];
return $element;
}
/**
* Performs validation of village group subforms.
*
* @param array $subform
* The sub-form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Current form state.
* @param array $complete_form
* The complete form.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function validateSubForm(array &$subform, FormStateInterface $form_state, array &$complete_form) {
$plugin_id = end($subform['#parents']);
// Traverse up the parent hierarchy to find the selected option.
// Remove the last 2 elements from parents (the plugin id, and the subform
// container) to get the path to the radio list.
$relevant_parents = $subform['#parents'];
$relevant_parents = array_splice($relevant_parents, 0, -2);
$selected_option = $form_state->getValue($relevant_parents)['target_id'];
// If the selected option is the one that controls the current subform
// then allow the subform to validate.
if ($selected_option == $plugin_id) {
/** @var \Drupal\contacts_events_villages\Plugin\VillageGroupTypeInterface $plugin */
$plugin = $this->villageTypeManager->createInstance($plugin_id);
// Invoke validate only if this form is the currently selected plugin.
$plugin->validate($subform, $form_state, $complete_form);
}
}
/**
* Get the notated element path.
*
* @param array $element
* Form element.
*
* @return string
* Concatenated and notated path like "wrapper[field][0]".
*
* @todo Surely there is a Utility method in core for this?
*/
protected function getElementPathNotated(array $element) {
$parents = $element['#parents'];
$widget_path = array_shift($parents);
foreach ($parents as $parent) {
$widget_path .= "[$parent]";
}
return $widget_path;
}
/**
* Get available group entities for the given event.
*
* @param \Drupal\contacts_events\Entity\EventInterface $event
* The event to check for.
* @param string|null $type
* (Optional) The group type to filter by.
*
* @return \Drupal\contacts_events_villages\Entity\VillageGroupInterface[]
* Array of relevant group entities keyed by entity id.
*/
protected function getEventGroups(EventInterface $event, $type = NULL) {
$query = $this->villageGroupStorage->getQuery()
->condition('event', $event->id())
->sort('name', 'ASC');
$query->accessCheck(TRUE);
if ($type) {
$query->condition('type', $type);
}
return $this->villageGroupStorage->loadMultiple($query->execute());
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
foreach ($values as &$item) {
$element = NestedArray::getValue($form_state->getCompleteForm(), $item['array_parents']);
if (!empty($item['target_id']) && !is_numeric($item['target_id'])) {
$village_type = $item['target_id'];
/** @var \Drupal\contacts_events_villages\Plugin\VillageGroupTypeInterface $type_plugin */
$type_plugin = $this->villageTypeManager->createInstance($village_type);
// Build a dummy entity to compare to.
/** @var \Drupal\contacts_events_villages\Entity\VillageGroup $dummy_group */
$dummy_group = $this->villageGroupStorage->create(['type' => $village_type]);
$dummy_group->setEvent($element['#event_id']);
$dummy_group = $type_plugin->buildFormEntity($item['subform'][$village_type], $dummy_group);
$existing = $type_plugin->getExisting($dummy_group);
if ($existing->isNew()) {
$existing->save();
}
$item['target_id'] = $existing->id();
}
}
return $values;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
// Only allow for order items on the contacts booking bundle of orders.
return $field_definition->getTargetEntityTypeId() == 'commerce_order'
&& $field_definition->getTargetBundle() == 'contacts_booking'
&& $field_definition->getName() == 'village_group';
}
}
