OptionsWidgetBase.php

Same filename in other branches
  1. 9 core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php
  2. 8.9.x core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php
  3. 10 core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php

Namespace

Drupal\Core\Field\Plugin\Field\FieldWidget

File

core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php

View source
<?php

namespace Drupal\Core\Field\Plugin\Field\FieldWidget;

use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Base class for the 'options_*' widgets.
 *
 * Field types willing to enable one or several of the widgets defined in
 * options.module (select, radios/checkboxes, on/off checkbox) need to
 * implement the OptionsProviderInterface to specify the list of options to
 * display in the widgets.
 *
 * @see \Drupal\Core\TypedData\OptionsProviderInterface
 */
abstract class OptionsWidgetBase extends WidgetBase {
    
    /**
     * Abstract over the actual field columns.
     *
     * Allows different field types to reuse those widgets.
     *
     * @var string
     */
    protected $column;
    
    /**
     * Tracks whether the field is required.
     */
    protected bool $required;
    
    /**
     * Tracks whether the data is multi-valued.
     */
    protected bool $multiple;
    
    /**
     * Tracks whether the field has a value.
     */
    // phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
    protected bool $has_value;
    
    /**
     * The array of options for the widget.
     */
    protected array $options;
    
    /**
     * {@inheritdoc}
     */
    public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
        parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
        $property_names = $this->fieldDefinition
            ->getFieldStorageDefinition()
            ->getPropertyNames();
        $this->column = $property_names[0];
    }
    
    /**
     * {@inheritdoc}
     */
    public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
        // Prepare some properties for the child methods to build the actual form
        // element.
        $this->required = $element['#required'];
        $this->multiple = $this->fieldDefinition
            ->getFieldStorageDefinition()
            ->isMultiple();
        $this->has_value = isset($items[0]->{$this->column});
        // Add our custom validator.
        $element['#element_validate'][] = [
            static::class,
            'validateElement',
        ];
        $element['#key_column'] = $this->column;
        // The rest of the $element is built by child method implementations.
        return $element;
    }
    
    /**
     * Form validation handler for widget elements.
     *
     * @param array $element
     *   The form element.
     * @param \Drupal\Core\Form\FormStateInterface $form_state
     *   The form state.
     */
    public static function validateElement(array $element, FormStateInterface $form_state) {
        if ($element['#required'] && $element['#value'] == '_none') {
            if (isset($element['#required_error'])) {
                $form_state->setError($element, $element['#required_error']);
            }
            else {
                $form_state->setError($element, new TranslatableMarkup('@name field is required.', [
                    '@name' => $element['#title'],
                ]));
            }
        }
        // Massage submitted form values.
        // Drupal\Core\Field\WidgetBase::submit() expects values as
        // an array of values keyed by delta first, then by column, while our
        // widgets return the opposite.
        if (is_array($element['#value'])) {
            $values = array_values($element['#value']);
        }
        else {
            $values = [
                $element['#value'],
            ];
        }
        // Filter out the 'none' option. Use a strict comparison, because
        // 0 == 'any string'.
        $index = array_search('_none', $values, TRUE);
        if ($index !== FALSE) {
            unset($values[$index]);
        }
        // Transpose selections from field => delta to delta => field.
        $items = [];
        foreach ($values as $value) {
            $items[] = [
                $element['#key_column'] => $value,
            ];
        }
        $form_state->setValueForElement($element, $items);
    }
    
    /**
     * Returns the array of options for the widget.
     *
     * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
     *   The entity for which to return options.
     *
     * @return array
     *   The array of options for the widget.
     */
    protected function getOptions(FieldableEntityInterface $entity) {
        if (!isset($this->options)) {
            // Limit the settable options for the current user account.
            $options = $this->fieldDefinition
                ->getFieldStorageDefinition()
                ->getOptionsProvider($this->column, $entity)
                ->getSettableOptions(\Drupal::currentUser());
            // Add an empty option if the widget needs one.
            if ($empty_label = $this->getEmptyLabel()) {
                $options = [
                    '_none' => $empty_label,
                ] + $options;
            }
            $module_handler = \Drupal::moduleHandler();
            $context = [
                'fieldDefinition' => $this->fieldDefinition,
                'entity' => $entity,
                'widget' => $this,
            ];
            $module_handler->alter('options_list', $options, $context);
            array_walk_recursive($options, [
                $this,
                'sanitizeLabel',
            ]);
            // Options might be nested ("optgroups"). If the widget does not support
            // nested options, flatten the list.
            if (!$this->supportsGroups()) {
                $options = OptGroup::flattenOptions($options);
            }
            $this->options = $options;
        }
        return $this->options;
    }
    
    /**
     * Determines selected options from the incoming field values.
     *
     * @param \Drupal\Core\Field\FieldItemListInterface $items
     *   The field values.
     *
     * @return array
     *   The array of corresponding selected options.
     */
    protected function getSelectedOptions(FieldItemListInterface $items) {
        // We need to check against a flat list of options.
        $flat_options = OptGroup::flattenOptions($this->getOptions($items->getEntity()));
        $selected_options = [];
        foreach ($items as $item) {
            $value = $item->{$this->column};
            // Keep the value if it actually is in the list of options (needs to be
            // checked against the flat list).
            if (isset($flat_options[$value])) {
                $selected_options[] = $value;
            }
        }
        return $selected_options;
    }
    
    /**
     * Indicates whether the widgets support optgroups.
     *
     * @return bool
     *   TRUE if the widget supports optgroups, FALSE otherwise.
     */
    protected function supportsGroups() {
        return FALSE;
    }
    
    /**
     * Sanitizes a string label to display as an option.
     *
     * @param string $label
     *   The label to sanitize.
     */
    protected function sanitizeLabel(&$label) {
        // Allow a limited set of HTML tags.
        $label = FieldFilteredMarkup::create($label);
    }
    
    /**
     * Returns the empty option label to add to the list of options, if any.
     *
     * @return string|null
     *   Either a label of the empty option, or NULL.
     */
    protected function getEmptyLabel() {
    }

}

Classes

Title Deprecated Summary
OptionsWidgetBase Base class for the 'options_*' widgets.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.