12.10.3. Работа с полями в Drupal 8. Создаем свой тип поля, widget, formatter для вставки видео с Youtube.

Главные вкладки

levmyshkin 16 августа 2019 в 10:41

В прошлых статьях мы рассмотрели как устроен тип поля Link: Storage, Widget, Formatter. В этой статье мы сделаем свой костомный тип поля для вывода видео с youtube на странице с двумя разными форматами и настройками.

Эта статья направлена на изучение Fields API, если вам нужно добавить на сайт поле Yotube video, то лучше воспользоваться готовым модулем:

https://www.drupal.org/project/video_embed_field

Я добавил весь код на github в модуль drupalbook_youtube, вы можете скачать модуль и добавить его к себе на сайт:

https://github.com/levmyshkin/drupalbook8

Давайте рассмотрим листинг этого модуля и я постараюсь расписать как работает этот тип поля:

modules/custom/drupalbook_youtube/drupalbook_youtube.info.yml

name: DrupalBook Youtube
type: module
description: Youtube embed field
core: 8.x
package: Custom

Определяем мета данные для модуля.

modules/custom/drupalbook_youtube/src/Plugin/Field/FieldType/DrupalbookYoutubeItem.php

< ?php
 
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldType;
 
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
 
/**
 * Plugin implementation of the 'drupalbook_youtube' field type.
 *
 * @FieldType(
 *   id = "drupalbook_youtube",
 *   label = @Translation("Embed Youtube video"),
 *   module = "drupalbook_youtube",
 *   description = @Translation("Output video from Youtube."),
 *   default_widget = "drupalbook_youtube",
 *   default_formatter = "drupalbook_youtube_thumbnail"
 * )
 */

class DrupalbookYoutubeItem extends FieldItemBase {
  /**
   * {@inheritdoc}
   */

  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return array(
      'columns' => array(
        'value' => array(
          'type' => 'text',
          'size' => 'tiny',
          'not null' => FALSE,
        ),
      ),
    );
  }
 
  /**
   * {@inheritdoc}
   */

  public function isEmpty() {
    $value = $this->get('value')->getValue();
    return $value === NULL || $value === '';
  }
 
  /**
   * {@inheritdoc}
   */

  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Youtube video URL'));
 
    return $properties;
  }
 
}

Создаем тип поля, чтобы друпал знал, что мы будем хранить в таблице для этого поля.

< ?php
 
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldType;
 
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;

Определяем namespaces для нашего типа поля.

/**
 * Plugin implementation of the 'drupalbook_youtube' field type.
 *
 * @FieldType(
 *   id = "drupalbook_youtube",
 *   label = @Translation("Embed Youtube video"),
 *   module = "drupalbook_youtube",
 *   description = @Translation("Output video from Youtube."),
 *   default_widget = "drupalbook_youtube",
 *   default_formatter = "drupalbook_youtube_thumbnail"
 * )
 */

Пишем аннатацию для нашего класса, из этой аннатации друпал будет брать название нашего типа поля и его машинное имя.

class DrupalbookYoutubeItem extends FieldItemBase {

Название класса лучше всего писать с Item на конце.

/**
 * {@inheritdoc}
 */

public static function schema(FieldStorageDefinitionInterface $field_definition) {
  return array(
    'columns' => array(
      'value' => array(
        'type' => 'text',
        'size' => 'tiny',
        'not null' => FALSE,
      ),
    ),
  );
}

Определяем, что будем хранить поле value текстового типа.

/**
 * {@inheritdoc}
 */

public function isEmpty() {
  $value = $this->get('value')->getValue();
  return $value === NULL || $value === '';
}

В случае если у нас будет обращение к полю из стороннего кода, то для случая пустого поля мы выводим fallback с пустым результатом.

/**
 * {@inheritdoc}
 */

public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
  $properties['value'] = DataDefinition::create('string')
    ->setLabel(t('Youtube video URL'));
 
  return $properties;
}

Описываем наши колонки для таблицы MySQL и объекта entity. В итоге мы будем хранить ссылку целиком:

Теперь когда мы добавили тип поля, давайте создадим Widget для ввода данных:

modules/custom/drupalbook_youtube/src/Plugin/Field/FieldWidget/DrupalbookYoutubeWidget.php

< ?php
 
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldWidget;
 
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Plugin implementation of the 'drupalbook_youtube' widget.
 *
 * @FieldWidget(
 *   id = "drupalbook_youtube",
 *   module = "drupalbook_youtube",
 *   label = @Translation("Youtube video URL"),
 *   field_types = {
 *     "drupalbook_youtube"
 *   }
 * )
 */

class DrupalbookYoutubeWidget extends WidgetBase {
 
  /**
   * {@inheritdoc}
   */

  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $value = isset($items[$delta]->value) ? $items[$delta]->value : '';
    $element += array(
      '#type' => 'textfield',
      '#default_value' => $value,
      '#size' => 32,
      '#maxlength' => 256,
      '#element_validate' => array(
        array($this, 'validate'),
      ),
    );
    return array('value' => $element);
  }
 
  /**
   * Validate the color text field.
   */

  public function validate($element, FormStateInterface $form_state) {
    $value = $element['#value'];
    if (strlen($value) == 0) {
      $form_state->setValueForElement($element, '');
      return;
    }
    if(!preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $value, $matches)) {
      $form_state->setError($element, t("Youtube video URL is not correct."));
    }
  }
 
}

Виджет позволит нам вводить данные на форме редактирования entity.

/**
 * Plugin implementation of the 'drupalbook_youtube' widget.
 *
 * @FieldWidget(
 *   id = "drupalbook_youtube",
 *   module = "drupalbook_youtube",
 *   label = @Translation("Youtube video URL"),
 *   field_types = {
 *     "drupalbook_youtube"
 *   }
 * )
 */

В аннатации к классу мы должны указать field_type, который создали выше, то есть drupalbook_youtube.

class DrupalbookYoutubeWidget extends WidgetBase {

В конце имени класса мы добавляем Widget, чтобы показать что этот класс нужен для Field Widget.

/**
 * {@inheritdoc}
 */

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
  $value = isset($items[$delta]->value) ? $items[$delta]->value : '';
  $element += array(
    '#type' => 'textfield',
    '#default_value' => $value,
    '#size' => 32,
    '#maxlength' => 256,
    '#element_validate' => array(
      array($this, 'validate'),
    ),
  );
  return array('value' => $element);
}

Создаем через Form API простое текстовое поле, куда мы будем вводить ссылку на Youtube видео.

/**
 * Validate the color text field.
 */

public function validate($element, FormStateInterface $form_state) {
  $value = $element['#value'];
  if (strlen($value) == 0) {
    $form_state->setValueForElement($element, '');
    return;
  }
  if(!preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $value, $matches)) {
    $form_state->setError($element, t("Youtube video URL is not correct."));
  }
}

Validation callback который мы указали выше в #element_validate. Это нужно чтобы убедиться, что пользователь ввел корректную ссылку на youtube видео. Регулярное выражение я взял из stackoverflow, вы можете заменить его если оно у вас не рабоает.

Теперь мы можем вводить данные для нашего поля, осталось добавить Field Formatter, для вывода данных.

modules/custom/drupalbook_youtube/src/Plugin/Field/FieldFormatter/DrupalbookYoutubeThumbnailFormatter.php

У нас будет два форматера, начнем с простого, который будет выводить картинку с ссылкой на страницу видео Youtube.

< ?php
 
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldFormatter;
 
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
 
/**
 * Plugin implementation of the 'drupalbook_youtube_thumbnail' formatter.
 *
 * @FieldFormatter(
 *   id = "drupalbook_youtube_thumbnail",
 *   module = "drupalbook_youtube",
 *   label = @Translation("Displays video thumbnail"),
 *   field_types = {
 *     "drupalbook_youtube"
 *   }
 * )
 */

class DrupalbookYoutubeThumbnailFormatter extends FormatterBase {
 
  /**
   * {@inheritdoc}
   */

  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = array();
 
    foreach ($items as $delta => $item) {
      preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $item->value, $matches);
 
      if (!empty($matches)) {
        $content = '<a href="' . $item->value . '" target="_blank"><img src="http://img.youtube.com/vi/' . $matches[0] . '/0.jpg"></a>';
        $elements[$delta] = array(
          '#type' => 'html_tag',
          '#tag' => 'p',
          '#value' => $content,
        );
      }
 
    }
 
    return $elements;
  }
 
}

Указываем тип поля в аннатации к классу:

/**
 * Plugin implementation of the 'drupalbook_youtube_thumbnail' formatter.
 *
 * @FieldFormatter(
 *   id = "drupalbook_youtube_thumbnail",
 *   module = "drupalbook_youtube",
 *   label = @Translation("Displays video thumbnail"),
 *   field_types = {
 *     "drupalbook_youtube"
 *   }
 * )
 */

Мы будем использовать #type html_tag и выводить каждый элемент поля в теге . В массив мы передаем значение ключа #value уже готовый HTML, который и будет выводиться на страницу:

$content = '<a href="' . $item->value . '" target="_blank"><img src="http://img.youtube.com/vi/' . $matches[0] . '/0.jpg"></a>';
$elements[$delta] = array(
  '#type' => 'html_tag',
  '#tag' => 'p',
  '#value' => $content,
);

Картинки привью уже сгенерированны Youtube и мы можем напрямую к ним обращаться, зная ID видео.

Мы используем $delta, чтобы поддерживать множественные значения для поля, чтобы можно было вывести больше одного видео на страницу через одно поле.

Теперь давайте рассмотрим более сложный форматер с шаблоном и настройками:

modules/custom/drupalbook_youtube/src/Plugin/Field/FieldFormatter/DrupalbookYoutubeVideoFormatter.php

< ?php
 
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldFormatter;
 
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Plugin implementation of the 'drupalbook_youtube_video' formatter.
 *
 * @FieldFormatter(
 *   id = "drupalbook_youtube_video",
 *   module = "drupalbook_youtube",
 *   label = @Translation("Displays Youtube video"),
 *   field_types = {
 *     "drupalbook_youtube"
 *   }
 * )
 */

class DrupalbookYoutubeVideoFormatter extends FormatterBase {
 
  /**
   * {@inheritdoc}
   */

  public static function defaultSettings() {
    return array(
      'width' => '600',
      'height' => '450',
    ) + parent::defaultSettings();
  }
 
  /**
   * {@inheritdoc}
   */

  public function settingsForm(array $form, FormStateInterface $form_state) {
    $elements['width'] = array(
      '#type' => 'textfield',
      '#title' => t('Youtube video width'),
      '#default_value' => $this->getSetting('width'),
    );
    $elements['height'] = array(
      '#type' => 'textfield',
      '#title' => t('Youtube video height'),
      '#default_value' => $this->getSetting('height'),
    );
 
    return $elements;
  }
 
  /**
   * {@inheritdoc}
   */

  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = array();
    $width = $this->getSetting('width');
    $height = $this->getSetting('height');
 
    foreach ($items as $delta => $item) {
      preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $item->value, $matches);
 
      if (!empty($matches)) {
        $elements[$delta] = array(
          '#theme' => 'drupalbook_youtube_video_formatter',
          '#width' => $width,
          '#height' => $height,
          '#video_id' => $matches[0],
        );
      }
 
    }
 
    return $elements;
  }
 
  /**
   * {@inheritdoc}
   */

  public function settingsSummary() {
    $summary = [];
 
    $settings = $this->getSettings();
 
    if (!empty($settings['width']) && !empty($settings['height'])) {
      $summary[] = t('Video size: @width x @height', ['@width' => $settings['width'], '@height' => $settings['height']]);
    }
    else {
      $summary[] = t('Define video size');
    }
 
    return $summary;
  }
 
}

В начале файла идут стандартные подключения namespaces и аннатация:

< ?php
 
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldFormatter;
 
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Plugin implementation of the 'drupalbook_youtube_video' formatter.
 *
 * @FieldFormatter(
 *   id = "drupalbook_youtube_video",
 *   module = "drupalbook_youtube",
 *   label = @Translation("Displays Youtube video"),
 *   field_types = {
 *     "drupalbook_youtube"
 *   }
 * )
 */

Название класса заканчивается на Formatter, чтобы показать что это Field Formatter:

class DrupalbookYoutubeVideoFormatter extends FormatterBase {

Определяем дефолтные настройки для размера изображения:

/**
 * {@inheritdoc}
 */

public static function defaultSettings() {
  return array(
    'width' => '600',
    'height' => '450',
  ) + parent::defaultSettings();
}

Определяем дальше форму настроек для форматера поля:

/**
 * {@inheritdoc}
 */

public function settingsForm(array $form, FormStateInterface $form_state) {
  $elements['width'] = array(
    '#type' => 'textfield',
    '#title' => t('Youtube video width'),
    '#default_value' => $this->getSetting('width'),
  );
  $elements['height'] = array(
    '#type' => 'textfield',
    '#title' => t('Youtube video height'),
    '#default_value' => $this->getSetting('height'),
  );
 
  return $elements;
}

Это будет выглядеть следующим образом на странице Manage display:

В методе settingsSummary() мы возвращаем, что будет показываться в описание поля на странице Manage display:

public function settingsSummary() {
  $summary = [];
 
  $settings = $this->getSettings();
 
  if (!empty($settings['width']) && !empty($settings['height'])) {
    $summary[] = t('Video size: @width x @height', ['@width' => $settings['width'], '@height' => $settings['height']]);
  }
  else {
    $summary[] = t('Define video size');
  }
 
  return $summary;
}

И теперь давайте рассмотрим самый важным метод форматера viewElements().

$width = $this->getSetting('width');
$height = $this->getSetting('height');

Подгружаем настройки, которые определяются через форму настроек или через свойства класса как fallback.

foreach ($items as $delta => $item) {

Мы поддерживаем множественные поля, поэтому перебераем все элементы поля.

$elements[$delta] = array(
  '#theme' => 'drupalbook_youtube_video_formatter',
  '#width' => $width,
  '#height' => $height,
  '#video_id' => $matches[0],
);

В отличии от прошлого форматера в этом мы задаем ключ #theme и в него прописываем каким шаблоном обрабатывать элементы поля. Для нашего форматера мы создадим новый шаблон drupalbook_youtube_video_formatter. Этот шаблон мы определим в файле drupalbook_youtube.module:

modules/custom/drupalbook_youtube/drupalbook_youtube.module

/**
 * Implements hook_theme().
 */

function drupalbook_youtube_theme() {
  return array(
    'drupalbook_youtube_video_formatter' => array(
      'variables' => array('width' => 600, 'height' => 450, 'video_id' => NULL),
    ),
  );
}

Мы также здесь задем дефолтные значения ширины и высоты, а video_id, мы будем получать из '#video_id' => $matches[0] массива $elements.

И теперь нужно добавить сам файл шаблона:

modules/custom/drupalbook_youtube/templates/drupalbook-youtube-video-formatter.html.twig

{#
/**
 * @file
 * Default theme implementation of a simple Youtube video.
 *
 * Available variables:
 * - width: Youtube video width.
 * - height: Youtube video height.
 * - video_id: Youtube video ID.
 *
 * @see template_preprocess()
 * @see template_drupalbook_youtube_video_formatter()
 *
 * @ingroup themeable
 */

#}
{% spaceless %}
  <iframe width="{{ width }}" height="{{ height }}" src="https://www.youtube.com/embed/{{ video_id }}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
{% endspaceless %}

И теперь вы можете создавать поле Youtube video:

На этом мы закончим разбирать создание кастомных полей и перейдем к Entity API, где будем создавать кастомные типы сущностей.

Атрибуция

Абраменко Иван

Автор

Комментарии

Аватар пользователя levmyshkin levmyshkin 8 ноября 2022 в 21:00

Обычно в Drupal хранят сериализованный объект или массив:
https://www.php.net/manual/en/function.serialize.php

Пример поля можно здесь посмотреть, достаточно массив передать при сохранение и Drupal сам его сериализует и десериализует обратно при загрузке сущности:
https://www.drupal.org/project/ebt_core

Json тоже можно, но уже есть готовый модуль для этого:
https://www.drupal.org/project/json_field

Аватар пользователя dimitriy4k dimitriy4k 9 ноября 2022 в 17:18

да я о другом:
в formElement как я понял генерится html разметка для работы с этими данными на форме, а как туда js прикрутить? вроде должно быть что то вроде такого
$form['#attached']['library'][]='lib/libname';
но как его тут присандалить ваще не понятно. Тоже интересно и для FieldFormatter