Разработка

Создание кастомного фильтра для Drupal 8

Drupal
Добавление в избранное
Сохранить
Создание кастомного фильтра для Drupal 8

Хотите ли вы упросить вставку HTML блока через включение короткого токена? А может вы хотите добавить кастомный Javascript или CSS, но только для контента, который содержит определённый паттерн, или отфильтровать некоторые слова, которые посетители сочтут оскорбительными?

В этом материале мы создадим кастомный фильтр для Drupal 8, который заменяет паттерн и добавляет CSS на страницу. Для фильтра мы также добавим настройку, которую пользователь сможет переключать.

Что такое фильтры Drupal и форматы текста?

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

Одним из дефолтных текстовых форматов является Basic HTML. Когда вы настраиваете этот формат в admin/config/content/formats/manage/basic_html, то чуть ниже вы можете видеть все фильтры, которые для него включены.

Включенные фильтры для Basic HTML

У каждого фильтра могут быть необязательные настройки. Например, Вы можете увидеть форму настроек для фильтра Limit allowed HTML tags, если немного проскролите ниже.

Включенные фильтры для Limit allowed HTML tags

Как же создать свой фильтр, который будет доступен в форматах текста, и как его сделать настраиваемым?

Каркас модуля

Сначала мы создаём папку celebrate для модуля, и потом файл celebrate.info.yml:


name: Celebrate
description: Custom filter to replace a celebrate token.
type: module
package: custom
core: 8.x

Кастомный фильтр, это своего рода плагин, так что нам необходимо создать правильную структуру папок для того, чтобы следовать PSR-4 стандартам. Наша структура папок будет следующая: celebrate/src/Plugin/Filter.

В папке Filter создайте файл с названием FilterCelebrate.php. Добавьте правильный namespace и класс FilterBase, чтобы бы могли его расширить.

Сейчас наш файл выглядит вот так:


namespace Drupal\celebrate\Plugin\Filter;

use Drupal\filter\Plugin\FilterBase;

class FilterCelebrate extends FilterBase {

}

FilterBase - это абстрактный класс, который реализует FilterInterface, и заботиться о рутинной установке и настройке. В нашем собственном классе фильтра мы должны реализовать всего одну функцию process(). Если посмотреть документацию FilterInterface::process, функция должна возвращать отфильтрованный текст, который обёрнут в объект FilterProcessResult. Это значит, что мы должны добавить ещё один use оператор в наш файл.

Но почему мы не можем просто вернуть сам текст? Зачем нужен ещё один объект? Это станет понятно позже, когда нам понадобиться добавить больше функционала. А для того чтобы наш IDE или текстовый редактор не ругались, в качестве заглушки мы добавим транзитную функцию, которая пока что не делает никаких преобразований над текстом.

Вот наш обновлённый код:


namespace Drupal\celebrate\Plugin\Filter;

use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;

class FilterCelebrate extends FilterBase {

  public function process($text, $langcode) {
    return new FilterProcessResult($text);
  }
}

Позволяем Drupal обнаружить наш фильтр

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

Файл с аннотациями будет выглядеть так:


namespace Drupal\celebrate\Plugin\Filter;

use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;

/**
 * @Filter(
 *   id = "filter_celebrate",
 *   title = @Translation("Celebrate Filter"),
 *   description = @Translation("Help this text format celebrate good times!"),
 *   type = Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE,
 * )
 */
class FilterCelebrate extends FilterBase {
  public function process($text, $langcode) {
    return new FilterProcessResult($text);
  }
}

Любому объявлению плагина необходимо id, так что присвойте подходящее. Заголовок (title) и описание (description) будут показываться в админке. После того, как мы включили модуль, вы должны увидеть что-то подобное на экране:

Включенный кастомный фильтр

Тип (type) в аннотации требует больше объяснений. Это классификация назначения фильтра, и тут у нас есть несколько констант, которые помогут нам для указания свойства. Согласно документации у нас есть следующие варианты:

  • FilterInterface::TYPE_HTML_RESTRICTOR: Фильтры, запрещающие HTML теги и артрибуты.
  • FilterInterface::TYPE_MARKUP_LANGUAGE: Фильтры не-HTML разметки, которые генерируют HTML.
  • FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE: Нереверсивные (irreversible) преобразующие фильтры.
  • FilterInterface::TYPE_TRANSFORM_REVERSIBLE: Реверсивные (reversible) преобразующие фильтры.

Мы планируем взять немного не-HTML разметки и превратить её в HTML, так что нам подходит вторая классификация.

Существует ещё несколько необязательных свойств для аннотаций фильтра, их можно найти в документации FilterInterface.

Добавляем базовую текстовую обработку

В рамках этого фильтра мы хотим заменять каждое вхождение токена [celebrate] на HTML сниппет:


<span class="celebrate-filter">Good Times!</span>

Для этого мы добавляем следующий код в нашу функцию FilterCelebrate::process:


public function process($text, $langcode) {
  $replace = '<span class="celebrate-filter">' . $this->t('Good Times!') . '<span>';
  $new_text = str_replace('[celebrate]', $replace, $text);
  return new FilterProcessResult($new_text);
}

Включите фильтр Celebrate для контент-фильтра Basic HTML, и создайте тестовый контент, который содержит токен [celebrate]. Вы должны увидеть, что он был заменён на HTML сниппет, указанный выше. Если нет, то проверьте, что к полю применяется фильтр Basic HTML.

Добавляем форму настроек для фильтра

Но мы хотим, чтобы пользователь мог переключать настройку, которая касается нашего фильтра. Для этого мы должны определить форму настроек путём переопределения метода settingsForm() в нашем классе.

Мы добавляем следующий код в наш класс для определения массива формы для нашего фильтра:


public function settingsForm(array $form, FormStateInterface $form_state) {
  $form['celebrate_invitation'] = array(
    '#type' => 'checkbox',
    '#title' => $this->t('Show Invitation?'),
    '#default_value' => $this->settings['celebrate_invitation'],
    '#description' => $this->t('Display a short invitation after the default text.'),
  );
  return $form;
}

Больше деталей по использованию Form API при определении массива формы вы можете найти в документации. Если вы ранее создавали формы в Drupal 7, то вам должно быть это знакомо.

Если вы рефрешните страницу администрирования нашего формата текста после добавления этой функции, то получите ошибку:

Fatal error: Declaration of Drupal\celebrate\Plugin\Filter\FilterCelebrate::settingsForm() must be compatible with Drupal\filter\Plugin\FilterInterface::settingsForm(array $form, Drupal\Core\Form\FormStateInterface $form_state)

Фактически ошибка возникает из-за того, что неизвестен FormStateInterface, который определён в нашем методе settingsForm(). Мы либо должны добавить полный PSR-4 namespace в определение метода, либо добавить ещё один оператор use. Давайте добавим его в файл FilterCelebrate.php:


use Drupal\Core\Form\FormStateInterface;

Теперь мы видим нашу форму в действии:

Настройки кастомного фильтра

Для получения доступа к этим настройкам в нашем классе, мы можем вызвать $this->settings['celebrate_invitation'].

Наш метод process() теперь выглядит так:


public function process($text, $langcode) {
  $invitation = $this->settings['celebrate_invitation'] ? ' Come on!' : '';
  $replace = '<span class="celebrate-filter">' . $this->t('Good Times!' . $invitation) . ' </span>';
  $new_text = str_replace('[celebrate]', $replace, $text);
  return new FilterProcessResult($new_text);
}

Если настройка "Show Invitation?" отмечена, то в конец заменяющего текста добавляется текст "Come on!".

Добавляем CSS, когда применяется фильтр

А теперь давайте добавим CSS анимацию к заменяющему тексту при наведении. Этот CSS должен загружаться только тогда, когда используется фильтр. Вот где в игру вступают дополнительные свойства объекта FilterProcessResult.

Сначала создадим CSS файл в корне папки модуля и назовём его celebrate.theme.css. Вот что нам понадобится, чтобы включить эффект при наведении:


.celebrate-filter {
    background-color: #000066;
    padding: 10px 5px;
    color: #fff;
}

.celebrate-filter:hover {
    animation: shake .3s ease-in-out infinite;
    background-color: #ff0000;
}

@keyframes shake {
    0% {
        transform: translateX(0);
    }
    20% {
        transform: translateX(-6px);
    }
    40% {
        transform: translateX(6px);
    }
    60% {
        transform: translateX(-6px);
    }
    80% {
        transform: translateX(6px);
    }
    100% {
        transform: translateX(0);
    }
}

Чтобы добавить наш CSS к FilterProcessResult, он должен быть объявлен как библиотека. Создайте ещё одни файл в корне модуля с названием celebrate.libraries.yml со следующим текстом:


celebrate-shake:
  version: 1.x
  css:
    theme:
      celebrate.theme.css: {}

Это определяет библиотеку celebrate-shake, которая подключает CSS файл. В одну библиотеку могут быть включены несколько CSS и/или Javascript файлов. Подробнее смотрите в документации по определению библиотеки.

Теперь, когда мы определили библиотеку, мы можем добавить её тогда, когда применяется наш фильтр. Для этого мы используем метод setAttachments() объекта FilterProcessResult. Вот так теперь выглядит метод process():


public function process($text, $langcode) {
  $invitation = $this->settings['celebrate_invitation'] ? ' Come on!' : '';
  $replace = '<span class="celebrate-filter">' . $this->t('Good Times!' . $invitation) . ' </span>';
  $new_text = str_replace('[celebrate]', $replace, $text);

  $result = new FilterProcessResult($new_text);
  $result->setAttachments(array(
    'library' => array('celebrate/celebrate-shake'),
  ));

  return $result;
}

Обратите внимание, что мы используем идентификатор celebrate/celebrate-shake для ссылки на нашу новую библиотеку. Первая часть идентификатора - это название модуля, а вторая часть - это название библиотеки. Это помогает избежать конфликта имён.

И как бонус, другие модули также смогут использовать нашу библиотеку celebrate.

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

Исходный код фильтра доступен на GitHub.

Оригинальная статья:
Dmitry Rekun
Работаю в банковской сфере, а с веб-разработкой (непосредственно с Joomla) столкнулся в 2007 году. Теперь это моё хобби, а в редких случаях и вторая работа. Какое-то время вёл свой блог, но решил попробовать работать в команде. И вот c 2012 года я здесь :)

Подпишитесь на рассылку новостей CMScafe