custom_field-1.0.x-dev/src/Plugin/Field/FieldFormatter/BaseFormatter.php
src/Plugin/Field/FieldFormatter/BaseFormatter.php
<?php
declare(strict_types=1);
namespace Drupal\custom_field\Plugin\Field\FieldFormatter;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\custom_field\Event\PreFormatEvent;
use Drupal\custom_field\Plugin\CustomFieldFormatterInterface;
use Drupal\custom_field\Plugin\CustomFieldFormatterManagerInterface;
use Drupal\custom_field\Plugin\CustomFieldTypeInterface;
use Drupal\custom_field\Plugin\CustomFieldTypeManagerInterface;
use Drupal\custom_field\TagManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* The base formatter for custom_field.
*/
abstract class BaseFormatter extends FormatterBase implements BaseFormatterInterface {
/**
* The custom field type manager.
*
* @var \Drupal\custom_field\Plugin\CustomFieldTypeManagerInterface
*/
protected CustomFieldTypeManagerInterface $customFieldManager;
/**
* The custom field formatter manager.
*
* @var \Drupal\custom_field\Plugin\CustomFieldFormatterManagerInterface
*/
protected CustomFieldFormatterManagerInterface $customFieldFormatterManager;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected EntityRepositoryInterface $entityRepository;
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The tag manager service.
*
* @var \Drupal\custom_field\TagManagerInterface
*/
protected TagManagerInterface $tagManager;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->customFieldManager = $container->get('plugin.manager.custom_field_type');
$instance->customFieldFormatterManager = $container->get('plugin.manager.custom_field_formatter');
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->entityRepository = $container->get('entity.repository');
$instance->eventDispatcher = $container->get('event_dispatcher');
$instance->moduleHandler = $container->get('module_handler');
$instance->tagManager = $container->get('custom_field.tag_manager');
$instance->renderer = $container->get('renderer');
return $instance;
}
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
return [
'fields' => [],
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$form = parent::settingsForm($form, $form_state);
$field_name = $this->fieldDefinition->getName();
$form['fields'] = [
'#type' => 'table',
'#header' => [
$this->t('Field'),
$this->t('Settings'),
$this->t('Weight'),
],
'#tableselect' => FALSE,
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'field-settings-order-weight',
],
],
'#weight' => 10,
];
$field_settings = $this->getSetting('fields') ?? [];
$custom_items = $this->sortFields($field_settings);
foreach ($custom_items as $name => $custom_item) {
$settings = $field_settings[$name] ?? [];
$formatter_settings = $settings['formatter_settings'] ?? [];
$wrapper_settings = $settings['wrappers'] ?? [];
$type = $custom_item->getPluginId();
$formatter_options = $this->customFieldFormatterManager->getOptions($custom_item);
$default_format = $custom_item->getDefaultFormatter();
if (isset($settings['format_type']) && isset($formatter_options[$settings['format_type']])) {
$default_format = $settings['format_type'];
}
if ($form_state instanceof SubformStateInterface) {
$values = $form_state->getCompleteFormState()->getValues();
}
else {
$values = $form_state->getValues();
}
$value_keys = $this->customFieldFormatterManager->getFormatterValueKeys($form_state, $field_name, $name);
$parents = array_slice($value_keys, 0, -1);
$format_type = NestedArray::getValue($values, $value_keys) ?? $default_format;
$visibility_path = $this->customFieldFormatterManager->getInputPathForStatesApi($form_state, $field_name, $name);
$root_visibility_path = $visibility_path;
// Strip the last [formatter_settings] to get root path.
if (str_ends_with($visibility_path, '[formatter_settings]')) {
$root_visibility_path = substr($visibility_path, 0, -strlen('[formatter_settings]'));
}
$form['#visibility_path'] = $visibility_path;
$form['#field_parents'] = [...$parents, 'formatter_settings'];
$wrapper_id = 'field-' . $field_name . '-' . $name . '-ajax';
$weight = $settings['weight'] ?? 0;
$form['fields'][$name] = [
'#attributes' => [
'class' => ['draggable'],
],
'#weight' => $weight,
];
$form['fields'][$name]['name'] = [
'#type' => 'markup',
'#markup' => $custom_item->getLabel(),
];
$form['fields'][$name]['content'] = [
'#type' => 'details',
'#title' => $this->t('Settings'),
'#parents' => $parents,
];
if (!empty($formatter_options)) {
$form['fields'][$name]['content']['format_type'] = [
'#type' => 'select',
'#title' => $this->t('Format type'),
'#options' => $formatter_options,
'#default_value' => $format_type,
'#ajax' => [
'callback' => [$this, 'actionCallback'],
'wrapper' => $wrapper_id,
'method' => 'replace',
],
];
$form['fields'][$name]['content']['formatter_settings'] = [
'#type' => 'container',
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
];
$formatter = [];
$options = $this->customFieldFormatterManager->createOptionsForInstance($custom_item, $format_type, $formatter_settings, $this->viewMode);
// Get the formatter settings form.
$format = $this->customFieldFormatterManager->getInstance($options);
if (!is_null($format)) {
$formatter = $format->settingsForm($form, $form_state);
}
$form['fields'][$name]['content']['formatter_settings'] += $formatter;
$form['fields'][$name]['content']['formatter_settings']['label_display'] = [
'#type' => 'select',
'#title' => $this->t('Label display'),
'#options' => $this->fieldLabelOptions(),
'#default_value' => $formatter_settings['label_display'] ?? 'above',
'#weight' => 10,
'#access' => !($type === 'boolean' || $format_type === 'hidden'),
];
$form['fields'][$name]['content']['formatter_settings']['field_label'] = [
'#type' => 'textfield',
'#title' => $this->t('Field label'),
'#description' => $this->t('The label for viewing this field. Leave blank to use the default field label.'),
'#default_value' => $formatter_settings['field_label'] ?? '',
'#weight' => 11,
'#maxlength' => 255,
'#access' => $format_type !== 'hidden',
'#states' => [
'visible' => [
':input[name="' . $visibility_path . '[label_display]"]' => ['!value' => 'hidden'],
],
],
];
// HTML wrapper settings.
$tag_options = $this->tagManager->getTagOptions();
$form['fields'][$name]['content']['wrappers'] = [
'#type' => 'details',
'#title' => $this->t('Style settings'),
'#states' => [
'visible' => [
':input[name="' . $root_visibility_path . '[format_type]"]' => ['!value' => 'hidden'],
],
],
];
$form['fields'][$name]['content']['wrappers']['field_wrapper_tag'] = [
'#type' => 'select',
'#title' => $this->t('Field wrapper tag'),
'#description' => $this->t('Choose the HTML element to wrap around this field and label.'),
'#options' => $tag_options,
'#empty_option' => $this->t('- Use default -'),
'#default_value' => $wrapper_settings['field_wrapper_tag'] ?? '',
];
$form['fields'][$name]['content']['wrappers']['field_wrapper_classes'] = [
'#type' => 'textfield',
'#title' => $this->t('Field wrapper classes'),
'#description' => $this->t('Enter additional classes, separated by space.'),
'#default_value' => $wrapper_settings['field_wrapper_classes'] ?? '',
'#states' => [
'invisible' => [
':input[name="' . $root_visibility_path . '[wrappers][field_wrapper_tag]"]' => ['value' => 'none'],
],
],
];
$form['fields'][$name]['content']['wrappers']['field_tag'] = [
'#type' => 'select',
'#title' => $this->t('Field tag'),
'#description' => $this->t('Choose the HTML element to wrap around this field.'),
'#options' => $tag_options,
'#empty_option' => $this->t('- Use default -'),
'#default_value' => $wrapper_settings['field_tag'] ?? '',
];
$form['fields'][$name]['content']['wrappers']['field_classes'] = [
'#type' => 'textfield',
'#title' => $this->t('Field classes'),
'#description' => $this->t('Enter additional classes, separated by space.'),
'#default_value' => $wrapper_settings['field_classes'] ?? '',
'#states' => [
'invisible' => [
':input[name="' . $root_visibility_path . '[wrappers][field_tag]"]' => ['value' => 'none'],
],
],
];
$form['fields'][$name]['content']['wrappers']['label_tag'] = [
'#type' => 'select',
'#title' => $this->t('Label tag'),
'#description' => $this->t('Choose the HTML element to wrap around this label.'),
'#options' => $tag_options,
'#empty_option' => $this->t('- Use default -'),
'#default_value' => $wrapper_settings['label_tag'] ?? '',
'#states' => [
'visible' => [
':input[name="' . $visibility_path . '[label_display]"]' => ['!value' => 'hidden'],
],
],
];
$form['fields'][$name]['content']['wrappers']['label_classes'] = [
'#type' => 'textfield',
'#title' => $this->t('Label classes'),
'#description' => $this->t('Enter additional classes, separated by space.'),
'#default_value' => $wrapper_settings['label_classes'] ?? '',
'#states' => [
'visible' => [
':input[name="' . $visibility_path . '[label_display]"]' => ['!value' => 'hidden'],
],
'invisible' => [
':input[name="' . $root_visibility_path . '[wrappers][label_tag]"]' => ['value' => 'none'],
],
],
];
$form['fields'][$name]['weight'] = [
'#type' => 'weight',
'#title' => $this->t('Weight for @label', ['@label' => $custom_item->getLabel()]),
'#title_display' => 'invisible',
'#default_value' => $weight,
'#attributes' => ['class' => ['field-settings-order-weight']],
];
}
}
return $form;
}
/**
* Helper function to sort field settings by weight.
*
* @param array $settings
* The field settings.
*
* @return array<string, CustomFieldTypeInterface>
* The sorted custom items.
*/
protected function sortFields(array $settings): array {
$custom_items = $this->getCustomFieldItems();
// Sort items by weight.
uasort($custom_items, function (CustomFieldTypeInterface $a, CustomFieldTypeInterface $b) use ($settings) {
$weight_a = $settings[$a->getName()]['weight'] ?? 0;
$weight_b = $settings[$b->getName()]['weight'] ?? 0;
return $weight_a <=> $weight_b;
});
return $custom_items;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
$summary = parent::settingsSummary();
$settings = $this->getSetting('fields') ?? [];
// Sort items by weight.
$custom_items = $this->sortFields($settings);
foreach ($custom_items as $id => $custom_field) {
$formatter_options = $this->customFieldFormatterManager->getOptions($custom_field);
$format_type = $custom_field->getDefaultFormatter();
if (isset($settings[$id]['format_type']) && isset($formatter_options[$settings[$id]['format_type']])) {
$format_type = $settings[$id]['format_type'];
}
try {
$definition = $this->customFieldFormatterManager->getDefinition($format_type);
}
catch (\Exception $exception) {
// Silent fail, for now.
continue;
}
$formatted_summary = new FormattableMarkup(
'<strong>@label</strong>: @format_label', [
'@label' => $custom_field->getLabel(),
'@format_label' => $definition['label'],
]
);
$summary[] = $this->t('@summary', ['@summary' => $formatted_summary]);
}
return $summary;
}
/**
* Ajax callback for changing format type.
*
* Selects and returns the fieldset with the names in it.
*
* @param array<string, mixed> $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The updated form element.
*/
public function actionCallback(array $form, FormStateInterface $form_state): AjaxResponse {
$trigger = $form_state->getTriggeringElement();
$wrapper_id = $trigger['#ajax']['wrapper'];
// Get the current parent array for this widget.
$parents = $trigger['#array_parents'];
$sliced_parents = array_slice($parents, 0, -1, TRUE);
// Get the updated element from the form structure.
$updated_element = NestedArray::getValue($form, $sliced_parents)['formatter_settings'];
// Create an AjaxResponse.
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#' . $wrapper_id, $updated_element));
return $response;
}
/**
* Builds a renderable array for a field value.
*
* @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\custom_field\Plugin\Field\FieldType\CustomItem> $items
* The field values to be rendered.
* @param string $langcode
* The language that should be used to render the field.
*
* @return array<int, mixed>
* A renderable array for $items, as an array of child elements keyed by
* consecutive numeric indexes starting from 0.
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = $this->viewValue($item, $langcode);
}
return $elements;
}
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\custom_field\Plugin\Field\FieldType\CustomItem>[] $entities_items
* An array with the field values from the multiple entities being rendered.
*/
public function prepareView(array $entities_items): void {
$ids = [];
$custom_items = $this->getCustomFieldItems();
foreach ($entities_items as $items) {
foreach ($items as $item) {
foreach ($custom_items as $custom_item) {
$target_type = $custom_item->getTargetType();
$value = $item->{$custom_item->getName()};
if (!empty($target_type) && !empty($value)) {
$ids[$target_type][] = $value;
}
}
}
}
if ($ids) {
foreach ($ids as $target_type => $entity_ids) {
try {
$target_entities[$target_type] = $this->entityTypeManager->getStorage($target_type)->loadMultiple($entity_ids);
}
catch (\Exception $exception) {
// Silent fail, for now.
}
}
}
foreach ($entities_items as $items) {
foreach ($items as $item) {
foreach ($custom_items as $custom_item) {
$target_type = $custom_item->getTargetType();
$value = $item->{$custom_item->getName()};
if (!empty($target_type) && !empty($value)) {
if (isset($target_entities[$target_type][$value])) {
$item->{$custom_item->getName()} = ['entity' => $target_entities[$target_type][$value]];
}
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function viewValue(FieldItemInterface $item, string $langcode): array {
$field_name = $this->fieldDefinition->getName();
$output = [
'#theme' => 'custom_field',
'#field_name' => $field_name,
'#items' => [],
];
$values = $this->getFormattedValues($item, $langcode);
foreach ($values as $name => $value) {
if ($value !== NULL && $value !== '') {
$output['#items'][$name] = [
'#theme' => 'custom_field_item',
'#field_name' => $field_name,
'#name' => $value['name'],
'#value' => $value['value']['#markup'],
'#label' => $value['label'],
'#label_display' => $value['label_display'],
'#type' => $value['type'],
'#wrappers' => $value['wrappers'],
'#entity_type' => $value['entity_type'],
'#lang_code' => $langcode,
];
}
}
return $output;
}
/**
* Get the custom field items for this field.
*
* @return \Drupal\custom_field\Plugin\CustomFieldTypeInterface[]
* An array of custom field items.
*/
public function getCustomFieldItems(): array {
return $this->customFieldManager->getCustomFieldItems($this->fieldDefinition->getSettings());
}
/**
* Returns an array of formatted custom field item values for a singe field.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* The field item.
* @param string $langcode
* The language code.
*
* @return array<string, mixed>
* An array of formatted values.
*/
protected function getFormattedValues(FieldItemInterface $item, string $langcode): array {
$settings = $this->getSetting('fields') ?? [];
$custom_items = $this->sortFields($settings);
$event = new PreFormatEvent($custom_items, $item, $langcode);
$this->eventDispatcher->dispatch($event);
$custom_items = $event->getCustomItems();
$values = [];
$entity_type = $this->fieldDefinition->getTargetEntityTypeId();
foreach ($custom_items as $name => $custom_item) {
$value = $custom_item->value($item);
$data_type = $custom_item->getDataType();
if ($value === '' || $value === NULL) {
continue;
}
if ($data_type === 'viewfield') {
$value = [
'target_id' => $value,
'display_id' => $item->{$name . '__display'},
'arguments' => $item->{$name . '__arguments'},
'items_to_display' => $item->{$name . '__items'},
];
}
elseif ($data_type === 'uri') {
$value = [
'uri' => $value,
];
}
elseif ($data_type === 'link') {
$value = [
'uri' => $value,
'title' => $item->{$name . '__title'},
'options' => $item->{$name . '__options'},
];
}
elseif ($data_type === 'datetime') {
$value = [
'date' => $item->{$name . '__date'},
'timezone' => $item->{$name . '__timezone'},
];
}
elseif ($data_type === 'daterange') {
$value = [
'start_date' => $item->{$name . '__start_date'},
'end_date' => $item->{$name . '__end_date'},
'timezone' => $item->{$name . '__timezone'},
'duration' => $item->{$name . '__duration'},
];
}
elseif ($data_type === 'time_range') {
$value = [
'start' => $value,
'end' => $item->{$name . '__end'},
'duration' => $item->{$name . '__duration'},
];
}
elseif (in_array($data_type, ['entity_reference', 'file', 'image'])) {
$entity = $item->{$name . '__entity'};
if (!$entity instanceof EntityInterface) {
continue;
}
if ($entity instanceof TranslatableInterface) {
$entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
}
$value = $entity;
}
$default_wrappers = [
'field_wrapper_tag' => '',
'field_wrapper_classes' => '',
'field_tag' => '',
'field_classes' => '',
'label_tag' => '',
'label_classes' => '',
];
$wrappers = $settings[$name]['wrappers'] ?? $default_wrappers;
$formatter_settings = [
'format_type' => $settings[$name]['format_type'] ?? NULL,
'formatter_settings' => $settings[$name]['formatter_settings'] ?? [],
'wrappers' => array_merge($default_wrappers, $wrappers),
];
$format_type = $custom_item->getDefaultFormatter();
// Get the available formatter options for this field type.
$formatter_options = $this->customFieldFormatterManager->getOptions($custom_item);
if (!empty($formatter_settings['format_type']) && isset($formatter_options[$formatter_settings['format_type']])) {
$format_type = $formatter_settings['format_type'];
}
$options = $this->customFieldFormatterManager->createOptionsForInstance($custom_item, $format_type, $formatter_settings['formatter_settings'], $this->viewMode);
/** @var \Drupal\custom_field\Plugin\CustomFieldFormatterInterface $plugin */
$plugin = $this->customFieldFormatterManager->getInstance($options);
$value = $plugin->formatValue($item, $value);
if ($value === '' || $value === NULL) {
continue;
}
$formatter_settings['formatter_settings'] += $plugin::defaultSettings();
$field_label = $formatter_settings['formatter_settings']['field_label'] ?? NULL;
$markup = [
'name' => $name,
'value' => [
'#markup' => $value,
],
'label' => $field_label ?: $custom_item->getLabel(),
'label_display' => $formatter_settings['formatter_settings']['label_display'] ?? 'above',
'type' => $custom_item->getPluginId(),
'wrappers' => $formatter_settings['wrappers'],
'entity_type' => $entity_type,
];
$values[$name] = $markup;
}
return $values;
}
/**
* Returns an array of visibility options for custom field labels.
*
* Copied from Drupal\field_ui\Form\EntityViewDisplayEditForm (can't call
* directly since it's protected)
*
* @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup|string>
* An array of visibility options.
*/
protected function fieldLabelOptions(): array {
return [
'above' => $this->t('Above'),
'inline' => $this->t('Inline'),
'hidden' => '- ' . $this->t('Hidden') . ' -',
'visually_hidden' => '- ' . $this->t('Visually hidden') . ' -',
];
}
/**
* {@inheritdoc}
*
* @return array<string, string[]>
* An array of dependencies grouped by type (config, content, module,
* theme). For example:
* @code
* [
* 'config' => ['user.role.anonymous', 'user.role.authenticated'],
* 'content' => ['node:article:f0a189e6-55fb-47fb-8005-5bef81c44d6d'],
* 'module' => ['node', 'user'],
* 'theme' => ['claro'],
* ];
* @endcode
*/
public function calculateDependencies(): array {
$dependencies = parent::calculateDependencies();
$fields = $this->getSetting('fields');
if (!empty($fields)) {
foreach ($fields as $field) {
$formatter_settings = $field['formatter_settings'] ?? [];
if (empty($formatter_settings)) {
continue;
}
try {
$plugin = $this->customFieldFormatterManager->createInstance($field['format_type']);
assert($plugin instanceof CustomFieldFormatterInterface);
$plugin_dependencies = $plugin->calculateFormatterDependencies($formatter_settings);
$dependencies = \array_merge_recursive($dependencies, $plugin_dependencies);
}
catch (PluginException $e) {
// No dependencies applicable if we somehow have invalid plugin.
}
}
}
return $dependencies;
}
/**
* {@inheritdoc}
*
* @param array<string, string[]> $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
* Dependency types are 'config', 'content', 'module' and 'theme'.
*/
public function onDependencyRemoval(array $dependencies): bool {
$changed = parent::onDependencyRemoval($dependencies);
$settings_changed = FALSE;
$fields = $this->getSetting('fields');
foreach ($fields as $name => $field) {
if (!isset($field['formatter_settings'])) {
continue;
}
try {
$plugin = $this->customFieldFormatterManager->createInstance($field['format_type']);
if ($plugin instanceof CustomFieldFormatterInterface) {
$changed_settings = $plugin->onFormatterDependencyRemoval($dependencies, $field['formatter_settings']);
if (!empty($changed_settings)) {
$fields[$name]['formatter_settings'] = $changed_settings;
$settings_changed = TRUE;
}
}
}
catch (\Exception $exception) {
// Silent fail, for now.
}
}
if ($settings_changed) {
$this->setSetting('fields', $fields);
}
$changed |= $settings_changed;
return (bool) $changed;
}
}
