FileWidget.php

Same filename in other branches
  1. 9 core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
  2. 8.9.x core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
  3. 11.x core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php

Namespace

Drupal\file\Plugin\Field\FieldWidget

File

core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php

View source
<?php

namespace Drupal\file\Plugin\Field\FieldWidget;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\file\Element\ManagedFile;
use Drupal\file\Entity\File;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;

/**
 * Plugin implementation of the 'file_generic' widget.
 */
class FileWidget extends WidgetBase {
    
    /**
     * The element info manager.
     */
    protected ElementInfoManagerInterface $elementInfo;
    
    /**
     * {@inheritdoc}
     */
    public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) {
        parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
        $this->elementInfo = $element_info;
    }
    
    /**
     * {@inheritdoc}
     */
    public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
        return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('element_info'));
    }
    
    /**
     * {@inheritdoc}
     */
    public static function defaultSettings() {
        return [
            'progress_indicator' => 'throbber',
        ] + parent::defaultSettings();
    }
    
    /**
     * {@inheritdoc}
     */
    public function settingsForm(array $form, FormStateInterface $form_state) {
        $element['progress_indicator'] = [
            '#type' => 'radios',
            '#title' => $this->t('Progress indicator'),
            '#options' => [
                'throbber' => $this->t('Throbber'),
                'bar' => $this->t('Bar with progress meter'),
            ],
            '#default_value' => $this->getSetting('progress_indicator'),
            '#description' => $this->t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
            '#weight' => 16,
            '#access' => extension_loaded('uploadprogress'),
        ];
        return $element;
    }
    
    /**
     * {@inheritdoc}
     */
    public function settingsSummary() {
        $summary = [];
        $summary[] = $this->t('Progress indicator: @progress_indicator', [
            '@progress_indicator' => $this->getSetting('progress_indicator'),
        ]);
        return $summary;
    }
    
    /**
     * Overrides \Drupal\Core\Field\WidgetBase::formMultipleElements().
     *
     * Special handling for draggable multiple widgets and 'add more' button.
     */
    protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
        $field_name = $this->fieldDefinition
            ->getName();
        $parents = $form['#parents'];
        // Load the items for form rebuilds from the field state as they might not
        // be in $form_state->getValues() because of validation limitations. Also,
        // they are only passed in as $items when editing existing entities.
        $field_state = static::getWidgetState($parents, $field_name, $form_state);
        if (isset($field_state['items'])) {
            $items->setValue($field_state['items']);
        }
        // Determine the number of widgets to display.
        $cardinality = $this->fieldDefinition
            ->getFieldStorageDefinition()
            ->getCardinality();
        switch ($cardinality) {
            case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
                $max = count($items);
                $is_multiple = TRUE;
                break;
            default:
                $max = $cardinality - 1;
                $is_multiple = $cardinality > 1;
                break;
        }
        $title = $this->fieldDefinition
            ->getLabel();
        $description = $this->getFilteredDescription();
        $elements = [];
        $delta = 0;
        // Add an element for every existing item.
        foreach ($items as $item) {
            $element = [
                '#title' => $title,
                '#description' => $description,
            ];
            $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
            if ($element) {
                // Input field for the delta (drag-n-drop reordering).
                if ($is_multiple) {
                    // We name the element '_weight' to avoid clashing with elements
                    // defined by widget.
                    $element['_weight'] = [
                        '#type' => 'weight',
                        '#title' => $this->t('Weight for row @number', [
                            '@number' => $delta + 1,
                        ]),
                        '#title_display' => 'invisible',
                        // Note: this 'delta' is the FAPI #type 'weight' element's property.
'#delta' => $max,
                        '#default_value' => $item->_weight ?: $delta,
                        '#weight' => 100,
                    ];
                }
                $elements[$delta] = $element;
                $delta++;
            }
        }
        $empty_single_allowed = $cardinality == 1 && $delta == 0;
        $empty_multiple_allowed = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta < $cardinality) && !$form_state->isProgrammed();
        // Add one more empty row for new uploads except when this is a programmed
        // multiple form as it is not necessary.
        if ($empty_single_allowed || $empty_multiple_allowed) {
            // Create a new empty item.
            $items->appendItem();
            $element = [
                '#title' => $title,
                '#description' => $description,
            ];
            $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
            if ($element) {
                $element['#required'] = $element['#required'] && $delta == 0;
                $elements[$delta] = $element;
            }
        }
        if ($is_multiple) {
            // The group of elements all-together need some extra functionality after
            // building up the full list (like draggable table rows).
            $elements['#file_upload_delta'] = $delta;
            $elements['#type'] = 'details';
            $elements['#open'] = TRUE;
            $elements['#theme'] = 'file_widget_multiple';
            $elements['#theme_wrappers'] = [
                'details',
            ];
            $elements['#process'] = [
                [
                    static::class,
                    'processMultiple',
                ],
            ];
            $elements['#title'] = $title;
            $elements['#description'] = $description;
            $elements['#field_name'] = $field_name;
            $elements['#language'] = $items->getLangcode();
            // The field settings include defaults for the field type. However, this
            // widget is a base class for other widgets (e.g., ImageWidget) that may
            // act on field types without these expected settings.
            $field_settings = $this->getFieldSettings() + [
                'display_field' => NULL,
            ];
            $elements['#display_field'] = (bool) $field_settings['display_field'];
            // Add some properties that will eventually be added to the file upload
            // field. These are added here so that they may be referenced easily
            // through a hook_form_alter().
            $elements['#file_upload_title'] = $this->t('Add a new file');
            $elements['#file_upload_description'] = [
                '#theme' => 'file_upload_help',
                '#description' => '',
                '#upload_validators' => $elements[0]['#upload_validators'],
                '#cardinality' => $cardinality,
            ];
        }
        return $elements;
    }
    
    /**
     * {@inheritdoc}
     */
    public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
        $field_settings = $this->getFieldSettings();
        // The field settings include defaults for the field type. However, this
        // widget is a base class for other widgets (e.g., ImageWidget) that may act
        // on field types without these expected settings.
        $field_settings += [
            'display_default' => NULL,
            'display_field' => NULL,
            'description_field' => NULL,
        ];
        $cardinality = $this->fieldDefinition
            ->getFieldStorageDefinition()
            ->getCardinality();
        $defaults = [
            'fids' => [],
            'display' => (bool) $field_settings['display_default'],
            'description' => '',
        ];
        // Essentially we use the managed_file type, extended with some
        // enhancements.
        $element_info = $this->elementInfo
            ->getInfo('managed_file');
        $element += [
            '#type' => 'managed_file',
            '#upload_location' => $items[$delta]->getUploadLocation(),
            '#upload_validators' => $items[$delta]->getUploadValidators(),
            '#value_callback' => [
                static::class,
                'value',
            ],
            '#process' => array_merge($element_info['#process'], [
                [
                    static::class,
                    'process',
                ],
            ]),
            '#progress_indicator' => $this->getSetting('progress_indicator'),
            // Allows this field to return an array instead of a single value.
'#extended' => TRUE,
            // Add properties needed by value() and process() methods.
'#field_name' => $this->fieldDefinition
                ->getName(),
            '#entity_type' => $items->getEntity()
                ->getEntityTypeId(),
            '#display_field' => (bool) $field_settings['display_field'],
            '#display_default' => $field_settings['display_default'],
            '#description_field' => $field_settings['description_field'],
            '#cardinality' => $cardinality,
        ];
        $element['#weight'] = $delta;
        // Field stores FID value in a single mode, so we need to transform it for
        // form element to recognize it correctly.
        if (!isset($items[$delta]->fids) && isset($items[$delta]->target_id)) {
            $items[$delta]->fids = [
                $items[$delta]->target_id,
            ];
        }
        $element['#default_value'] = $items[$delta]->getValue() + $defaults;
        $default_fids = $element['#extended'] ? $element['#default_value']['fids'] : $element['#default_value'];
        if (empty($default_fids)) {
            $file_upload_help = [
                '#theme' => 'file_upload_help',
                '#description' => $element['#description'],
                '#upload_validators' => $element['#upload_validators'],
                '#cardinality' => $cardinality,
            ];
            $element['#description'] = \Drupal::service('renderer')->renderInIsolation($file_upload_help);
            $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE;
            if ($cardinality != 1 && $cardinality != -1) {
                $element['#element_validate'] = [
                    [
                        static::class,
                        'validateMultipleCount',
                    ],
                ];
            }
        }
        return $element;
    }
    
    /**
     * {@inheritdoc}
     */
    public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
        // Since file upload widget now supports uploads of more than one file at a
        // time it always returns an array of fids. We have to translate this to a
        // single fid, as field expects single value.
        $new_values = [];
        foreach ($values as &$value) {
            foreach ($value['fids'] as $fid) {
                $new_value = $value;
                $new_value['target_id'] = $fid;
                unset($new_value['fids']);
                $new_values[] = $new_value;
            }
        }
        return $new_values;
    }
    
    /**
     * {@inheritdoc}
     */
    public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
        parent::extractFormValues($items, $form, $form_state);
        // Update reference to 'items' stored during upload to take into account
        // changes to values like 'alt' etc.
        // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit()
        $field_name = $this->fieldDefinition
            ->getName();
        $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
        $field_state['items'] = $items->getValue();
        static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
    }
    
    /**
     * Form API callback. Retrieves the value for the file_generic field element.
     *
     * This method is assigned as a #value_callback in formElement() method.
     */
    public static function value($element, $input, FormStateInterface $form_state) {
        if ($input) {
            if (empty($input['display'])) {
                // Updates the display field with the default value because
                // #display_field is invisible.
                if (empty($input['fids'])) {
                    $input['display'] = $element['#display_default'];
                }
                else {
                    $input['display'] = $element['#display_field'] ? 0 : 1;
                }
            }
        }
        // We depend on the managed file element to handle uploads.
        $return = ManagedFile::valueCallback($element, $input, $form_state);
        // Ensure that all the required properties are returned even if empty.
        $return += [
            'fids' => [],
            'display' => 1,
            'description' => '',
        ];
        return $return;
    }
    
    /**
     * Validates the number of uploaded files.
     *
     * This validator is used only when cardinality not set to 1 or unlimited.
     */
    public static function validateMultipleCount($element, FormStateInterface $form_state, $form) {
        $values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
        $array_parents = $element['#array_parents'];
        array_pop($array_parents);
        $previously_uploaded_count = count(Element::children(NestedArray::getValue($form, $array_parents))) - 1;
        $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($element['#entity_type']);
        $field_storage = $field_storage_definitions[$element['#field_name']];
        $newly_uploaded_count = count($values['fids']);
        $total_uploaded_count = $newly_uploaded_count + $previously_uploaded_count;
        if ($total_uploaded_count > $field_storage->getCardinality()) {
            $keep = $newly_uploaded_count - $total_uploaded_count + $field_storage->getCardinality();
            $removed_files = array_slice($values['fids'], $keep);
            $removed_names = [];
            foreach ($removed_files as $fid) {
                $file = File::load($fid);
                $removed_names[] = $file->getFilename();
            }
            $args = [
                '%field' => $field_storage->getName(),
                '@max' => $field_storage->getCardinality(),
                '@count' => $total_uploaded_count,
                '%list' => implode(', ', $removed_names),
            ];
            $message = new TranslatableMarkup('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args);
            \Drupal::messenger()->addWarning($message);
            $values['fids'] = array_slice($values['fids'], 0, $keep);
            NestedArray::setValue($form_state->getValues(), $element['#parents'], $values);
        }
    }
    
    /**
     * Form API callback: Processes a file_generic field element.
     *
     * Expands the file_generic type to include the description and display
     * fields.
     *
     * This method is assigned as a #process callback in formElement() method.
     */
    public static function process($element, FormStateInterface $form_state, $form) {
        $item = $element['#value'];
        $item['fids'] = $element['fids']['#value'];
        // Add the display field if enabled.
        if ($element['#display_field']) {
            $element['display'] = [
                '#type' => empty($item['fids']) ? 'hidden' : 'checkbox',
                '#title' => new TranslatableMarkup('Include file in display'),
                '#attributes' => [
                    'class' => [
                        'file-display',
                    ],
                ],
            ];
            if (isset($item['display'])) {
                $element['display']['#value'] = $item['display'] ? '1' : '';
            }
            else {
                $element['display']['#value'] = $element['#display_default'];
            }
        }
        else {
            $element['display'] = [
                '#type' => 'hidden',
                '#value' => '1',
            ];
        }
        // Add the description field if enabled.
        if ($element['#description_field'] && $item['fids']) {
            $config = \Drupal::config('file.settings');
            $element['description'] = [
                '#type' => $config->get('description.type'),
                '#title' => new TranslatableMarkup('Description'),
                '#value' => $item['description'] ?? '',
                '#maxlength' => $config->get('description.length'),
                '#description' => new TranslatableMarkup('The description may be used as the label of the link to the file.'),
            ];
        }
        // Adjust the Ajax settings so that on upload and remove of any individual
        // file, the entire group of file fields is updated together.
        if ($element['#cardinality'] != 1) {
            $parents = array_slice($element['#array_parents'], 0, -1);
            $new_options = [
                'query' => [
                    'element_parents' => implode('/', $parents),
                ],
            ];
            $field_element = NestedArray::getValue($form, $parents);
            $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
            foreach (Element::children($element) as $key) {
                if (isset($element[$key]['#ajax'])) {
                    $element[$key]['#ajax']['options'] = $new_options;
                    $element[$key]['#ajax']['wrapper'] = $new_wrapper;
                }
            }
            unset($element['#prefix'], $element['#suffix']);
        }
        // Add another submit handler to the upload and remove buttons, to implement
        // functionality needed by the field widget. This submit handler, along with
        // the rebuild logic in file_field_widget_form() requires the entire field,
        // not just the individual item, to be valid.
        foreach ([
            'upload_button',
            'remove_button',
        ] as $key) {
            $element[$key]['#submit'][] = [
                static::class,
                'submit',
            ];
            $element[$key]['#limit_validation_errors'] = [
                array_slice($element['#parents'], 0, -1),
            ];
        }
        return $element;
    }
    
    /**
     * Form API callback: Processes a group of file_generic field elements.
     *
     * Adds the weight field to each row so it can be ordered and adds a new Ajax
     * wrapper around the entire group so it can be replaced all at once.
     *
     * This method on is assigned as a #process callback in formMultipleElements()
     * method.
     */
    public static function processMultiple($element, FormStateInterface $form_state, $form) {
        $element_children = Element::children($element, TRUE);
        $count = count($element_children);
        // Count the number of already uploaded files, in order to display new
        // items in \Drupal\file\Element\ManagedFile::uploadAjaxCallback().
        if (!$form_state->isRebuilding()) {
            $count_items_before = 0;
            foreach ($element_children as $children) {
                if (!empty($element[$children]['#default_value']['fids'])) {
                    $count_items_before++;
                }
            }
            $form_state->set('file_upload_delta_initial', $count_items_before);
        }
        foreach ($element_children as $delta => $key) {
            if ($key != $element['#file_upload_delta']) {
                $description = static::getDescriptionFromElement($element[$key]);
                $element[$key]['_weight'] = [
                    '#type' => 'weight',
                    '#title' => $description ? new TranslatableMarkup('Weight for @title', [
                        '@title' => $description,
                    ]) : new TranslatableMarkup('Weight for new file'),
                    '#title_display' => 'invisible',
                    '#delta' => $count,
                    '#default_value' => $delta,
                ];
            }
            else {
                // The title needs to be assigned to the upload field so that validation
                // errors include the correct widget label.
                $element[$key]['#title'] = $element['#title'];
                $element[$key]['_weight'] = [
                    '#type' => 'hidden',
                    '#default_value' => $delta,
                ];
            }
        }
        // Add a new wrapper around all the elements for Ajax replacement.
        $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
        $element['#suffix'] = '</div>';
        return $element;
    }
    
    /**
     * Retrieves the file description from a field element.
     *
     * This helper static method is used by processMultiple() method.
     *
     * @param array $element
     *   An associative array with the element being processed.
     *
     * @return array|false
     *   A description of the file suitable for use in the administrative
     *   interface.
     */
    protected static function getDescriptionFromElement($element) {
        // Use the actual file description, if it's available.
        if (!empty($element['#default_value']['description'])) {
            return $element['#default_value']['description'];
        }
        // Otherwise, fall back to the filename.
        if (!empty($element['#default_value']['filename'])) {
            return $element['#default_value']['filename'];
        }
        // This is probably a newly uploaded file; no description is available.
        return FALSE;
    }
    
    /**
     * Form submission handler for upload/remove button of formElement().
     *
     * This runs in addition to and after file_managed_file_submit().
     *
     * @see file_managed_file_submit()
     */
    public static function submit($form, FormStateInterface $form_state) {
        // During the form rebuild, formElement() will create field item widget
        // elements using re-indexed deltas, so clear out FormState::$input to
        // avoid a mismatch between old and new deltas. The rebuilt elements will
        // have #default_value set appropriately for the current state of the field,
        // so nothing is lost in doing this.
        $button = $form_state->getTriggeringElement();
        $parents = array_slice($button['#parents'], 0, -2);
        NestedArray::setValue($form_state->getUserInput(), $parents, NULL);
        // Go one level up in the form, to the widgets container.
        $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
        $field_name = $element['#field_name'];
        $parents = $element['#field_parents'];
        $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2));
        foreach ($submitted_values as $delta => $submitted_value) {
            if (empty($submitted_value['fids'])) {
                unset($submitted_values[$delta]);
            }
        }
        // If there are more files uploaded via the same widget, we have to separate
        // them, as we display each file in its own widget.
        $new_values = [];
        foreach ($submitted_values as $delta => $submitted_value) {
            if (is_array($submitted_value['fids'])) {
                foreach ($submitted_value['fids'] as $fid) {
                    $new_value = $submitted_value;
                    $new_value['fids'] = [
                        $fid,
                    ];
                    $new_values[] = $new_value;
                }
            }
            else {
                $new_value = $submitted_value;
            }
        }
        // Re-index deltas after removing empty items.
        $submitted_values = array_values($new_values);
        // Update form_state values.
        NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values);
        // Update items.
        $field_state = static::getWidgetState($parents, $field_name, $form_state);
        $field_state['items'] = $submitted_values;
        static::setWidgetState($parents, $field_name, $form_state, $field_state);
    }
    
    /**
     * {@inheritdoc}
     */
    public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
        // Never flag validation errors for the remove button.
        $clicked_button = end($form_state->getTriggeringElement()['#parents']);
        if ($clicked_button !== 'remove_button') {
            parent::flagErrors($items, $violations, $form, $form_state);
        }
    }

}

Classes

Title Deprecated Summary
FileWidget Plugin implementation of the 'file_generic' widget.

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