Drupal 8: Пишем плагин Display Suite поля

Создание собственных плагинов Display Suite для добавления программных полей.

02.04.2016
15 комментариев
11 мин.

В прошлой статье я написал как создавать собственные DisplaySuite поля в Drupal 7. После я решил посмотреть и разобраться как они работают в Drupal 8. И в этой статье я уже расскажу как создать те же самые поля, но в реалиях Drupal 8.

Что поменялось и как работают

Изменился подход. Теперь, как и большинство других хуков, hook_ds_fields_info() заменён системой плагинов. Это реально удобнее и позволяет сохранять модуль понятным и чистым. Это, к слову, решает проблему переизбытка кода в файле о котором я рассказывал в статье для D7.

Display Suite предоставляет два типа плагинов DsField и DsFieldTemplate. Обратите внимание, что для создания полей используется только DsField. Вы можете подумать что DsFieldTemplate это замена DS_FIELD_TYPE_THEME, но это не так. Этот тип плагинов позволяет объявлять обработчики полей. Если вы знакомы с DS, вы видели что есть возможность выбора Field Template после активации соответствующей функции. После этого появлялся выбор: Full reset, Expert, Minimal - вот это они и есть, а этот тип плагина позволяет добавлять их туда. Все поля теперь объявляются через один плагин.

Поля для DisplaySuite хранятся по адресу src/Plugin/DsField, у каждого поля свой файлик который содержит класс, который, в свою очередь, наследуется от DsFieldBase. Также, новой особенностью является то, что в D7 поле возвращало строку со значением, в D8 поле должно возвращать render array, и если вы хотите вернуть строку, то возвращать придется render array с markup. В основном концепция осталась прежней, но вот подход соответственно изменился.

Так как типов полей теперь нет вообще, все зависит от того что вы хотите вернуть, то я приведу просто разные примеры с демонстрацией различных возможностей.

P.s. далее по коду подразумевается что модуль, в котором пишется код, называется dummy.

Пример №1 - набор минимум

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

Давайте создадим поле для материала article, которое будет выводить количество слов в материале.

Информация о поле указывается как и у всех в аннотации к классу. Плагин принимает следующие данные:

  • id: машинное имя поля. В нижнем регистре, латинские буквы и знак подчеркивания.
  • title: Человекопонятное имя поля, просто лейбл для админки.
  • entity_type: Название сущности для которой применимо поле. Сущность одна!
  • provider: Указываем кто предоставляет поле, в данном случае название нашего модуля.
  • ui_limit: bundle|view_mode - указываем на каких подтипах сущности и его форматах вывода будет доступно поле. Используйте знак * для значения 'all'. Несколько значений указываются в следующем формате: {"article|teaser", "news|*", "*|*"}
Листинг файла src/Plugin/DsField/WordCount.php
<?php

namespace Drupal\dummy\Plugin\DsField;

use Drupal\ds\Plugin\DsField\DsFieldBase;
use Drupal\Component\Render\FormattableMarkup;

/**
 * Поле которое выводит количество слов в содержимом.
 *
 * @DsField(
 *   id = "word_count",
 *   title = @Translation("DS: Word count"),
 *   provider = "dummy",
 *   entity_type = "node",
 *   ui_limit = {"article|full"}
 * )
 */
class WordCount extends DsFieldBase {
  /**
   * {@inheritdoc}
   *
   * Метод который должен вернуть результат для поля.
   */
  public function build() {
    // Записываем для удобства объект текущей сущности в переменную.
    $entity = $this->entity();
    // Проверяем есть ли значение в поле body. Если поле ничего не вернет
    // это воспринимается как пустое поле и оно не выводится.
    if ($body_value = $entity->body->value) {
      return [
        '#type' => 'markup',
        '#markup' => new FormattableMarkup(
          '<strong>Количество слов в тексте:</strong> @word_count',
          [
            '@word_count' => str_word_count(strip_tags($body_value))
          ]
        )
      ];
    }
  }
}

Теперь заходим в управление отображением типа материала article, формат вывода "Full content", и там должно появиться поле.

Созданное поле должно появиться в списке.

На странице материала теперь будет выводиться количество слов в тексте:

Результат обработки нашего поля.

Разумеется это не весть текст на скрине ;)

Если оно у вас не появилось, сбросьте кэш drush cr или перейдите по url /core/rebuild.php. Если вы не отключали кэширование, советую сделать отключение кэширование бэкенда.

Пример №2 - выбор формата

Для добавления своих форматов мы должны добавить метод formatters().

В D7 у поля типа DS_FIELD_TYPE_THEME была возможность указания форматов вывода, данные форматы становятся на выбор в админке, по принцппу как, например, у поля body, где вы вбираете выводить полный текст или обрезанный. В отличии от D7, в D8 не требуется объявлять одноименную theme функцию, которая будет вызвана при выборе соответствующего формата. Основываясь на том что вывод будет через render array, то вы можете использовать уже существующие theme функции, или же объявить свои для конкретных случаев. Говоря проще, theme функция объявляется при необходимости.

Теперь мы создадим поле, в котором будем выводить список слов из текста, которые употребляются в нём более одного раза. Также предоставим выбор списка, маркированный или нумерованный - это и будет формат вывода.

Листинг src/Plugin/DsField/WordFrequency.php
<?php

namespace Drupal\dummy\Plugin\DsField;

use Drupal\ds\Plugin\DsField\DsFieldBase;

/**
 * Поле выводит список повторяющихся слов из поля body.
 *
 * @DsField(
 *   id = "word_frequency",
 *   title = @Translation("DS: Word frequency"),
 *   provider = "dummy",
 *   entity_type = "node",
 *   ui_limit = {"article|full"}
 * )
 */
class WordFrequency extends DsFieldBase {

  /**
   * {@inheritdoc}
   * Это наш собственный метод в котором мы подготавливаем массив с
   * повторящимися полями и их количеством повторений. Сделано это лишь для
   * поддержания чистоты кода и читабельности, это не обязательный метод.
   */
  public function prepareWordFrequencyArray() {
    $entity = $this->entity();
    if ($body_value = $entity->body->value) {
      # Получаем весь список повторяющихся слов.
      $words_with_count = array_count_values(str_word_count(strip_tags($body_value), 1));
      # Удаляем значения меньше меньше 2.
      foreach ($words_with_count as $k => $v) {
        if ($v < 2) {
          unset($words_with_count[$k]);
        }
      }
      # Сортируем по убыванию.
      arsort($words_with_count);
      # Теперь нам надо создать массив со значениями, чтобы было проще отдавать
      # на рендер. Нам нужен чтобы ключ был любым а значение: "Слово х N".
      $results = [];
      foreach ($words_with_count as $word => $count) {
        $results[] = $word . ' x ' . $count;
      }
      # Возвращаем результат.
      return $results;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function formatters() {
    # Возвращаем массив с возможными форматами: ключ => метка.
    return ['ol' => 'Нумерованный список', 'ul' => 'Маркированный список'];
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $config = $this->getConfiguration();
    # Записываем выбранный формат в переменную.
    $list_type = $config['field']['formatter'];
    return [
      '#theme' => 'item_list',
      '#title' => 'Повторяющиеся слова:',
      '#items' => $this->prepareWordFrequencyArray(),
      '#list_type' => $list_type,
    ];
  }
}

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

Выбор формата вывода у поля.

Ну а при заходе на страницу, должны увидеть список:

Вывод списка в выбранном формате

Пример №3 - форма с настройками

Также у нас есть возможность объявлять форму с настройками для нашего поля. Чтобы какие-то данные пользователи могли менять прямо в админке. С этим нам поможет справиться метод settingsForm(), в котором мы и объявим форму настроек. Также тут есть ещё один метод settingsSummary(), он позволяет выводить краткую справку о настройках на странице полей, без необходимости раскрывать форму. Это очень удобно и полезно, только сразу оговорюсь, в методе settingsSummary() отсутствуют форматы поля, поэтому они пригодны исключительно для вывода значений настроек или собственной информации.

Пример поля, которое объявляет собственные настройки.

На картинке выше, поле Tags раскрыто, и перед нами форма с настройками для поля, а поля выше, Image, в столбце Widget выводится краткая информация о выбранных настройках для данного поля - это как раз и есть settingsSummary().

Теперь перейдем непосредственно к практической части. Пример может тупой, но лучше ничего не придумал. Допустим у нас на сайте есть какой-то раздел, например услуги, и клиент хочет чтобы под каждым материалом была справка типа: "Остались вопросы? Позвоните нам +7 (999) 123-45-67". В данном случае DS отличное для этого решение, в добавок, мы можем вынести номер телефона в настройки данного поля, чтобы в дальнейшем, кто будет управлять сайтом, без проблем мог его поменять. При этом не нужны никакие страницы с настройками, а также не придется хардкодить в темплейтах. Также в любой момент это позволит поменять позицию или вовсе отключить данную информацию.

Листинг файла src/Plugin/DsField/CallusForMore.php
<?php

namespace Drupal\dummy\Plugin\DsField;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\ds\Plugin\DsField\DsFieldBase;
# Необходимо для формы.
use Drupal\Core\Form\FormStateInterface;

/**
 * Поле которое выводит количество слов в содержимом.
 *
 * @DsField(
 *   id = "call_us_for_more",
 *   title = @Translation("DS: Call us for more"),
 *   provider = "dummy",
 *   entity_type = "node",
 *   ui_limit = {"article|full"}
 * )
 */
class CallUsForMore extends DsFieldBase {

  /**
   * {@inheritdoc}
   *
   * Задаем настройки по умолчанию.
   */
  public function defaultConfiguration() {
    $config = [
      'telephone' => '+7 (999) 123-45-67',
    ];
    return $config;
  }

  /**
   * {@inheritdoc}
   * Данный метод должен возвращать форму в соответствии с Form API.
   */
  public function settingsForm($form, FormStateInterface $form_state) {
    # Получаем конфигурацию
    $config = $this->getConfiguration();
    # Название элемента формы должно равняться ключу в конфигах.
    $form['telephone'] = [
      '#type' => 'tel',
      '#title' => 'Номер телефона',
      '#default_value' => $config['telephone'],
      '#required' => TRUE,
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   *
   * В общем списке полей выводим информацию о номере телефона, чтобы не
   * приходилось загружать форму для проверки. Каждое значение массива будет
   * выводиться с новой строки.
   */
  public function settingsSummary($settings) {
    $config = $this->getConfiguration();
    return ['Номер телефона: ' . $config['telephone']];
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    # Получаем настройки с нашим телефоном.
    $config = $this->getConfiguration();
    return [
      '#type' => 'markup',
      '#markup' => new FormattableMarkup(
        '<strong>Остались вопросы? Позвоните нам:</strong> <a href="tel:@phone">@phone</a>',
        [
          '@phone' => $config['telephone'],
        ]
      )
    ];
  }
}

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

Вывод текущего телефона (по умолчанию взятый из кода)

А если мы нажмём на шестерёнку у данного поля, то у нас откроется наша форма с настройками:

Форма настроек для нашего поля.

А вот так это выводится в самом материале:

Результат поля с динамическим телефоном из настроек.

Вот и всё! Рассмотрел все основные методы для формирования поля. Как по мне, так подход в D8 на голову превосходит создание полей для D7. Он быстрее, проще и понятнее.

Прикрепленные файлы
Модуль со всеми примерами — DsField_examples.tar.gz, 2.56 КБ
Drupal
Drupal 8
DisplaySuite

Комментарии

Алексей   вт, 12/04/2016 - 16:09

Очень понравилась статья, в основном читаю только ваши руководства.
Не могли б вы мне подсказать куда копать, мне нужно создать модуль который будет конвертировать веденое в числовое поле 60

а модуль конвертировал значение в :
Cook time: 1 hour

нашел модуль recipe где даная функция реализована но так и не смог вытянуть ее.

также начал изучать api drupal 8 но что б написать с нуля и правильно мне еще учить пару лет.

Niklan   чт, 05/05/2016 - 12:09

Ну вот DS для этого и можно использовать. Делать псевдо-поле, которое на основе поля с числом будет выводить текстовый вариант.

petrovnn   сб, 30/04/2016 - 13:52

Спасибо за потрясающий туториал! Теперь сомнений по поводу использования восьмерки на новых проектах не возникает. Впервые заюзал DS, хотя делаю сайты на друпале не первый год, и возникает вопрос: а почему-же раньше мне про DS никто не сказал? Хорошо что без панелей, мне они никогда не нравились. И насколько я понимаю, DS будут включены в следующую версию ядра?

Niklan   чт, 05/05/2016 - 12:08

Там лишь возможность создавать свои форматы вывода из админки внесли в ядро. И то, по факту только UI, ибо сам функционал то был и в 7-ке но только на хуках, а DS был UI оберткой для этого. А такие вот поля только из DS. Опять же, в ядре есть такой же функционал, вокруг которого и работает DS, но он очень неудобный (в 7ке, в 8-ке не видел), непонятный и поэтому DS выруливает тут всегда.

Артем   пн, 05/12/2016 - 13:58

Классный блог. Никогда не понимал от куда вы это узнаете? Метод тыка и тотальный просмотр руководства?

Кирилл   пн, 02/04/2018 - 13:14

Спасибо за столько полезной информации! не подскажите как бы еще сделать new FormattableMarkup из последнего примера переводимым на разные языки? а в идеале вывести поле textarea и заполнять на странице настройки поля, и что б его можно было перевести ка тайтл или справочный текст.

Niklan   пн, 02/04/2018 - 17:00

Достаточно вызывать t(), эта функция сама вызывает FormattableMarkup, либо использовать TranslatableMarkup

Кирилл   ср, 04/04/2018 - 15:36

Спасибо за наводку но не подскажете подробнее как использовать TranslatableMarkup гугл не помогает, я немного изменил себе задачку и сделал модулем собственное поле, по мануалу тоже с Вашего сайта :) все прекрасно получилось, но вот целый день тормошу файлы плагинов в поисках решения, как выводятся на страницу admin/structure/types/manage/Types_node/fields/node.Name_Feld/translate/ru/add требуемые поля, например у поля image есть возможность задать там перевод для Alt и Title мне надо свое поле тоже туда засунуть, но вообще затык :( TranslatableMarkup в image не используется

Niklan   пт, 06/04/2018 - 08:47

Меняете

'#markup' => new FormattableMarkup(
  '<strong>Остались вопросы? Позвоните нам:</strong> <a href="tel:@phone">@phone</a>',
  [
    '@phone' => $config['telephone'],
  ]
)

на

'#markup' => t('<strong>Have questions? Call us:</strong> <a href="tel:@phone">@phone</a>',
  [
    '@phone' => $config['telephone'],
  ]
)

А t() уже сама вызовет TranslatableMarkup. В данном случае лучше юзать t() напрямую, банально короче и она специально для этого. Чтобы понять что там происходит, достаточно посмотреть в исходник t():

function t($string, array $args = [], array $options = []) {
  return new TranslatableMarkup($string, $args, $options);
}

Как я писал ранее, это лишь обертка.

Но помните что в t(), как и в TranslatableMarkup обязательно нужно передавать текст на английском. После того как вы это сделаете, оно сразу не будет работать, надо зайти на страницу где эта строка вызовется хотя бы раз, после чего она появится в интерфейсе для переводов. А вот там уже вводите русскую версию строки, и на русском сайте будет русская версия, на английском - английская.

У поля Image немного другое поведение. Там конфигурации, и работает несколько иначе. Image Filed это полноценное поле Field API, DS же поля, это по-факту extra field (псевдо-поля), только на стероидах со своими настройками, что накладывает свои ограничения относительно реальных полей. Но все решаемо :)

Кирилл   пн, 09/04/2018 - 06:36

С интерфейсом классно, транслит есть, спасибо! а вот все же с полноценными полями не подскажете что значит там конфигурации? надо какой то файл или хук добавить в свой модуль что бы заработали переводы как у Label, Help text или у prefix с suffix у числового поля?

Niklan   пн, 09/04/2018 - 09:02

У настоящих полей есть конфигурации, там хранятся все настройки поля, в том числе лейблы. Эти настройки становятся переводимыми в соответствии со schema для этих настроек поставляемых с модулем. Всё остальное уже делает сам друпал, там никаких доп действий не требуется.

Я просто не очень понял в чем вопрос. У DS даже свои форматтеры можно делать. Можно самому также объявить конфиги, сделать их переводимыми и переводить через интерфейс.

Про то как работают поля лучше почитать.

Кирилл   пн, 09/04/2018 - 17:05

я именно по этому мануалу и делал, отдельное спасибо за него) только я взял за основу поле из ядра integer, новое поле работает, я создал в нем доп. textarea, и туда пихаю текст на странице создания полей, в файле NumericFormatterBase обернул его в html и на выводе получаю рядом с числом иконку и tooltip с моим тектом, на english все работает круто но как этот текст перевести? у оригинала можно перевести суфикс, префикс, справку и лейбл а у моего почему то только справку и лейбл :( меня бы даже устроил бы суфикс переделанный из textfield в textarea только бы он работал с транслит как оригинал.

Niklan   пн, 09/04/2018 - 18:05

Надо смотреть как именно все сделано. Конфиги вашего поля должны быть описаны конфиг схемой. И не очень понятно тогда почему вопросы тут задаете, DS поля отличаются от тех что создаются Field API.

Сеня   пн, 20/01/2020 - 19:24

Кажется, половина изображений уже не соответствует действительности.

Niklan   пн, 20/01/2020 - 21:16

Модуль webp что-то тупит. Выключил, сейчас все скрины корректные.