В прошлых статьях мы рассмотрели как устроен тип поля 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 и выводить каждый элемент поля в теге <p>. В массив мы передаем значение ключа #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, где будем создавать кастомные типы сущностей.
Добавить комментарий