Урок 7. Form API

Урок 7. Form API

Drupal 8 Form API похож на API седьмой версии, но в то же время обладает довольно существенными отличиями. А вот какими именно мы постараемся разобраться.

Содержание

Базовые классы

Формы в 8 версии представлены интерфейсом \Drupal\Core\Form\FormInterface[1].
Данный интерфейс содержит методы:

  • getFormId() — возвращает уникальное имя формы.
  • buildForm() — метод, в котором определяются элементы формы.
  • validateForm() — метод проверки формы перед сабмитом. Содержит логику фильтрации/проверки данных, введенных в форму.
  • submitForm() — сабмит формы. Обрабатывает и сохраняет провалидированные данные.

От вышеупомянутого интерфейса реализуются все остальные классы форм, какие-то используют его непосредственного, другие через базовые классы. Базовые абстрактные классы содержатся в ядре и предназначены для создания разных типов форм. Эти классы содержат более проработанную структуру и упрощают создание собственных.

  • FormBase[2] — базовый класс для всех форм. Реализует интерфейсы FormInterface и ContainerInjectionInterface.
  • ConfigFormBase[3] — базовый класс для создания системных конфигурационных форм, таких как, например, на странице Performance по адресу admin/config/development/performance. Наследуется от FormBase.
  • ConfirmFormBase[4] — базовый класс для создания форм подтверждения. Наследуется от FormBase и реализует интерфейс ConfirmFormInterface.

Новые элементы форм

Form API может похвастаться новыми элементами форм, в том числе из HTML5. Например, если необходимо вывести поле для телефона, лучше использовать специально созданный для этого тип #type => ‘tel’ вместо textfield. Почему? Потому, что элементы форм специального назначения поддерживаются мобильными девайсами и, в случае с телефоном, пользователь сможет набрать номер просто кликнув по нему, а не заниматься копированием и вставкой на девайсе.
HTML5 элементы[1]:

  • tel — предназначен для ввода номера телефона.
  • email — для ввода электронных почтовых ящиков. Проверяет корректность ввода email.
  • number — числовое поле. Отображает справа виджет с двумя кнопками для увеличения/уменьшения числа.
    number
  • date — поле для дат. Есть встроенный виджет календаря.
    date
  • datetime — состоит из двух HTML5 элементов: date и time.
    datetime
    У данного элемента также есть виджет даты.
    datetime full widget
  • url — поле для ввода url адресов. Проверяет урл на корректность.
  • search — HTML5 input элемент с типом search.
  • range — отображает ползунок для изменения значений.
    range
    Свойства #min и #max задают диапазон изменения ползунка, а свойство #step позволяет указать шаг сдвига влево/вправо. Если #step не указан или указан параметр #step => 'any', то сдвиг будет происходить плавно.
    Пример:

    1
    2
    3
    4
    5
    6
    7
    
    $form['range'] = [
      '#type' => 'range',
      '#title' => $this->t('Range'),
      '#min' => 0,
      '#max' => 100,
      '#step' => 50,
    ];

Кроме того, появились новые, не менее интересные, элементы:

  • datelist — отображает группу выпадающих списков для указания даты (год, месяц, день) и времени (часы, минуты). Возможности:
    1. выбор отображения единицы даты/времени как выпадающий список или как текст. По умолчанию — выпадающий список.
    2. указание порядка отображения единиц времени. Если какая-либо единица не указана в свойстве #date_part_order, то она отображаться не будет.
    3. указание диапазона минут и секунд с помощью свойства '#date_increment'.
    4. указание диапазона года.

    Пример записи:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    $form['datelist'] = [
      '#type' => 'datelist',
      '#title' => $this->t('Date list'),
      '#default_value' => new DrupalDateTime(),
      '#date_part_order' => [
        'day',
        'month',
        'year',
        'hour',
        'minute',
        'second',
        'ampm',
     ],
      '#date_text_parts' => ['year'],
      '#date_year_range' => '2010:2020',
      '#date_increment'  => 10,
    ];

    Пример отображения:
    datelist

  • color — отображает виджет выбора цвета. Пример:
    1
    2
    3
    4
    5
    
    $form['color'] = [
      '#type' => 'color',
      '#title' => $this->t('Color'),
      '#default_value' => '#f00f00',
    ];

    color

  • details — элемент формы, похожий на филдсет. Отличия в том, что филдсет используется в формах, а details может быть использован вне форм.
  • dropbutton — предоставляет элемент, по клику на который раскрывается список со ссылками.
    Пример:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    $form['dropbutton'] = [
      '#type' => 'dropbutton',
      '#title' => $this->t('Dropbutton'),
      '#links' => array(
        'link1' => [
          'title' => $this->t('Google'),
          'url'   => Url::fromUri('https:google.com'),
        ],
        'link2' => [
          'title' => $this->t('Yandex'),
          'url'   => Url::fromUri('https:yandex.ru'),
        ],
      ),
    ];

    Список ссылок в раскрытом виде приведен на изображении ниже
    dropbutton

  • fieldgroup — группирует дочерние элементы. Различие между филдсетом — это css класс, применяемый к HTML элементу.
  • html_tag — предоставляет рендер элемент для любого HTML тега со свойствами и value.
    1
    2
    3
    4
    5
    
    $form['html_tag'] = [
      '#type' => 'html_tag',
      '#tag' => 'span',
      '#value' => $this->t('HtmlTag'),
    ];

    В данном примере текст HtmlTag будет обернут в тег span.

  • inline_template — предоставляет рендер элемент, в котором пользователь может использовать встроенный шаблон Twig.
    1
    2
    3
    4
    5
    6
    7
    
    $form['inline_template'] = [
      '#type' => 'inline_template',
      '#template' => "{% trans %} This is {% endtrans %} <strong>{{name}}</strong>",
      '#context' => [
        'name' => 'example',
      ],
    ];

    Результат примера:
    This is example

  • language_select — элемент выбора языка. Выбор появится, если на сайте установлено 2 и более языков.
    1
    2
    3
    4
    5
    6
    
    $form['language_select'] = [
      '#type' => 'language_select',
      '#title' => $this->t('Language Select'),
      '#languages' => LanguageInterface::STATE_ALL,
      '#languages' => LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT,
    ];
  • more_link — ссылка More.
    1
    2
    3
    4
    
    $form['more_link'] = [
      '#type' => 'more_link',
      '#url' => Url::fromUri('https:yandex.ru'),
    ];
  • operations — список операций. Элемент аналогичен элементу dropbutton с тем лишь отличием, что элемент operations может темизироваться с помощью theme suggestions.
  • page — представляет собой элемент формы, который отображает main и header тэги html страницы.
  • status_messages — используется для отображения сообщений, которые сгенерированы drupal_set_message().
    1
    2
    3
    
    $form['status_messages'] = [
       '#type' => 'status_messages',
    ];

    Если используется данный элемент формы, то при появлении сообщения на странице оно будет отображено в своем стандартном месте (вверху страницы) и продублировано в этом элементе.

  • status_report — элемент отвечает за построение таблицы сообщений аналогичной статус репорту по адресу admin/reports/status. Пример записи:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
    $requirements['update'] = [
      'title' => t('Database updates'),
      'value' => t('Updates required'),
      'severity' => REQUIREMENT_WARNING,
      'description' => t('Contrib modules are not up to date.'),
    ];
    $requirements['entity_update'] = [
      'title' => t('Entity/field definitions'),
      'value' => t('Security updates required'),
      'severity' => REQUIREMENT_ERROR,
      'description' => t('Entity/field have security updates.'),
    ];
    $form['status_report'] = [
      '#type' => 'status_report',
      '#requirements' => $requirements,
      '#prefix' => $this->t('status_report'),
    ];

    Результат
    status_report

  • system_compact_link — выводит ссылку Hide/show descriptions для скрытия/отображения help текста на административных страницах.
    system_compact_link

Создание кастомного элемента формы

Собственный элемент может быть двух типов: рендер элемент и форм элемент.

  • Рендер элементы — элементы, которые для построения своего отображения используют рендерные массивы. Данные элементы, как правило, не требуют от пользователя каких либо действий (выбор, заполнение, включение/выключение и т.д.). Пример рендер элементов — link, label, html_tag, more_link и т.д.
  • Форм элементы — это те же рендер элементы, которые используются в HTML формах. Особенностью данных элементов является то, что они требуют совершения каких-либо действий от пользователя: выбор из списка, заполнения текстового поля, сабмит формы и т.д. Значения (value) этих элементов попадают в валидацию и сабмит формы. Пример элементов - checkbox, checkboxes, select, textfield, submit и т.д.

И тот и другой тип содержит свойства (ключи которых начинаются со значка #) и могут содержать дочерние элементы. Свойства хранят данные или настройки, которые используются в финальном рендеринге и обработке. Некоторые свойства специфичны для конкретного элемента, другие применимы для всех.
Каждый тип элементы обладает своим базовым классом:

  • RenderElement — базовый класс для создания рендер элементов.
  • FormElement — базовый класс для создания форм элементов. Наследует класс RenderElement и реализует интерфейс FormElementInterface.

Для создания кастомного форм элемента потребуется выполнить несколько простых шагов:

  1. Создать папку Element и разместить ее в директории src/, чтобы получить следующий путь относительно корня модуля MYMODULE/src/Element/
  2. Создать класс MymoduleElement, унаследовав его от класса FormElement и поместить его в папку Element.
  3. Реализовать требуемые методы и добавить собственные, если это необходимо.
  4. Реализовать функцию темизации. Объявить тему в hook_theme() в файле MYMODULE.module, создать twig шаблон mymodule_element.html.twig.
  5. Twig шаблон разместить в MYMODULE/templates/.
  6. Если необходимо, написать препроцесс функции. Сбросить кеш.

Аналогичный план действий для рендер элемента с отличием в наследовании от другого базового класса.
В качестве наглядного примера создадим HTML5 элемент video. Его, кстати, нет среди элементов, доступных из коробки Drupal 8. Так как данный элемент может использоваться не только в формах, но и при рендеринге страниц, то его тип - рендер элемент. У video есть несколько параметров:

  • width — ширина видео.
  • height — высота видео.
  • autoplay — если указан, видео будет воспроизводиться автоматически.
  • controls — если указан, будут отображены элементы управления (страт/стоп, прокрутка, регулировка громкости и т.д.).

Итак, согласно вышеописанному плану создаем папку и размещаем ее, как указано в п.1.
Далее создаем класс MymoduleVideo.php, размещаем его в папке Element. Код класса следующий:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
namespace Drupal\MYMODULE\Element;
 
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElement;
 
/**
 * Class MymoduleVideo.
 *
 * @RenderElement("mymodule_video")
 */
class MymoduleVideo extends RenderElement {
  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $class = get_class($this);
    return [
      '#name'   => 'mymodule-video',
      '#theme'  => 'mymodule_video',
      '#controls' => TRUE,
      '#autoplay' => FALSE,
      '#novideo'  => $this->t('Your browser does not support the video tag.'),
      '#pre_render' => [
        [$class, 'preRenderVideo'],
      ],
    ];
  }
 
  /**
   * Prepares a video render element for mymodule_video.html.twig.
   *
   * @param array $element
   *   An associative array containing the properties of the element.
   *
   * @return array
   *   The $element with prepared variables ready for mymodule_video.html.twig.
   */
  public static function preRenderVideo($element) {
    $element['#attributes']['type'] = 'button';
    Element::setAttributes($element, ['id', 'name']);
 
    // Verify attributes.
    $attr = ['controls', 'autoplay', 'width', 'height'];
    foreach ($attr as $val) {
      if (!empty($element['#' . $val])) {
        $element['#attributes'][$val] = $element['#' . $val];
      }
    }
 
    // Process the sources types.
    foreach ($element['#sources'] as $src => $type) {
      $element['#sources'][$src] = 'video/' . $type;
    }
 
    $element['#attributes']['class'][] = 'mymodule-video-html5';
 
    return $element;
  }
}

В обязательном методе getInfo() возвращаем массив с параметрами элемента. Некоторые из них указаны по умолчанию, если пользователь их пропустит. В свойстве #pre_render указан метод, который будет вызван перед препроцес функциями и рендером элемента. В данном методе производим необходимые манипуляции с параметрами элемента. Обратите внимание, элементы подключаются с помощью аннотации, поэтому запись @RenderElement("mymodule_video") — обязательна. В случае с форм элементом она будет такая — @FormElement("mymodule_video").
Далее в файле MYMODULE.module регистрируем темирующую функцию и препроцесс функцию.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * Implements hook_theme().
 */
function MYMODULE_theme($existing, $type, $theme, $path) {
  $items['mymodule_video'] = [
    'render element' => 'element',
  ];
 
  return $items;
}
 
/**
 * Prepares variables for mymodule_video template.
 *
 * Default template: mymodule_video.html.twig.
 *
 * @param array $variables
 *   An associative array of element properties.
 */
function template_preprocess_mymodule_video(&$variables) {
  $element = $variables['element'];
  $variables['attributes'] = new Attribute($element['#attributes']);
  $variables['sources'] = $element['#sources'];
  $variables['text'] = !empty($element['#novideo']) ? $element['#novideo']->render() : '';
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{#
/**
 * @file
 * Default theme implementation for the MymoduleVideo.
 *
 * @see template_preprocess_mymodule_video()
 *
 * @ingroup themeable
 */
#}
<video {{ attributes }}>
  {% for src, type in sources %}
    <source src="{{ src }}" type="{{ type }}">
  {% endfor %}
  {{ text }}
</video>

Все, новый элемент готов. Пример использования

1
2
3
4
5
6
7
8
9
$form['mymodule_video'] = [
  '#type' => 'mymodule_video',
  '#sources' => [
    'https://www.w3schools.com/html/mov_bbb.mp4' => 'mp4',
    'https://www.w3schools.com/html/mov_bbb.ogg' => 'ogg',
  ],
  '#width'  => '320',
  '#height' => '240',
];

Итоговый результат
html5 video

Создание кастомной формы, валидация и сабмит

Из первого пункта содержания известно, что интерфейс FormInterface имеет в своем наборе два метода, отвечающих за валидацию и сабмит — validateForm() и submitForm() соответственно.
Абстрактный класс FormBase описывает метод validateForm() как заглушку, а метод submitForm() отдает на откуп дочерним классам. ConfigFormBase описывает submitForm(), в котором просто выводит сообщение о том, что настройки формы сохранены. ConfirmFormBase не описывает ни тот, ни другой метод.
Чтобы более детально разобраться как все работает, создадим кастомную форму. Пусть, форма содержит один форм элемент — поле для ввода номера телефона и кнопку сохранить. Форма будет конфигурационной, т.е. созданной на базе класса ConfigFormBase.
Кастомные формы в Drupal 8 располагаются по следующему пути, относительно вашего модуля:

1
MYMODULE/src/Form/MymoduleConfigForm.php

Создаем класс MymoduleConfigForm.php и располагаем его по вышеописанному пути.
ConfigFormBase потребует описание метода getEditableConfigNames() трейта ConfigFormBaseTrait. Данный метод возвращает имя конфига, который будет использоваться для хранения данных. Конфиги хранятся в таблице config базы данных.
Добавим проверку номера телефона в метод validateForm(). Пусть номер телефона соответствует международному формату для Японии, т.е. +81 ХХХ ХХХ ХХХ. Если проверка не проходит, “выбрасываем” сообщение об ошибке. В методе submitForm() сохраняем полученный телефон, а в buildForm() достаем его из конфига и подставляем в поле ввода.
Исходя из требований получаем следующее содержимое класса

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
namespace Drupal\MYMODULE\Form;
 
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
 
/**
* Class MymoduleConfigForm.
*
* @package Drupal\MYMODULE\Form
*/
class MymoduleConfigForm extends ConfigFormBase {
 
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'mymodule_settings';
  }
 
  /**
   * {@inheritdoc}
   */
  public function getEditableConfigNames() {
    return ['mymodule.settings'];
  }
 
  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    if (!preg_match('/\+81[\d]{9}/', $form_state->getValue('tel'))) {
      $form_state->setErrorByName('tel', $this->t('Incorrect phone number. Please verify the format +81xxxxxxxxx.'));
    }
  }
 
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('mymodule.settings')
      ->set('tel', $form_state->getValue('tel'))
      ->save();
 
    parent::submitForm($form, $form_state);
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildForm($form, $form_state);
    $form['tel'] = [
      '#type' => 'tel',
      '#title' => $this->t('Phone'),
      '#description' => $this->t('The phone number should be in Japan format +81xxxxxxxxx.'),
      '#placeholder' => '+81xxxxxxxxx',
      '#required' => TRUE,
      '#default_value' => $this->config('mymodule.settings')->get('tel'),
    ];
 
    return $form;
  }
 
}

Теперь необходимо создать роутинг для отображения формы. Создаем файл роутинга MYMODULE.routing.yml, размещаем его в корне модуля со следующей структурой

1
2
3
4
5
6
7
mymodule.settings:
  path: '/path_to_module_settings'
  defaults:
    _form: 'Drupal\mymodule\Form\MymoduleConfigForm'
    _title: 'MYMODULE Settings'
  requirements:
    _permission: 'administer content'

Не забываем сбрасывать кеш и проверяем форму настроек. Она должна выглядеть как на изображении ниже
форма настроек
При вводе неправильного номера, получаем ошибку
ошибка формы
При успешном вводе — сообщение о том, что все сохранено.
форма сохранена
Также не лишним будет указать ссылку на конфигурацию модуля на странице со списком всех модулей. Для этого добавим в файл MYMODULE.info.yml простую запись

1
configure: mymodule.settings

mymodule.settings — это имя роутинга, который мы описали в MYMODULE.routing.yml. Теперь, помимо версии модуля, будет доступна ссылка на форму настроек.
ссылка configure

Альтер форм

Альтер форм в 8-ой версии осуществляется с помощью тех же самых хуков, что были в 7-ой версии:

  • hook_form_alter()[5]
  • hook_form_FORM_ID_alter()[6]
  • hook_form_BASE_FORM_ID_alter()[7]

Все хуки размещаются в *.module файле.
Например, изменим заголовок "Phone" на "Phone number" для примера из предыдущего пункта.

1
2
3
4
5
6
7
8
use Drupal\Core\Form\FormStateInterface;
 
/**
* Implements hook_form_FORM_ID_alter().
*/
function MYMODULE_form_mymodule_settings_alter(&$form, FormStateInterface $form_state, $form_id) {
 $form['tel']['#title'] = t('Phone number');
}

Получаем
form alter

Получение формы

Функции drupal_get_form(), которая отвечала за получение формы в Drupal 7, больше нет. Данная операция осуществляется через сервис formBuilder. Фактически существует два способа получения формы:

  • первый — возвращает рендерный массив формы. Метод getForm() в качестве аргумента принимает путь класса, который отвечает за создание формы. Пример получения формы на странице “Производительность”:
    1
    
    $form = \Drupal::formBuilder()->getForm('Drupal\system\Form\PerformanceForm');
  • второй — позволяет манипулировать объектом формы до вызова метода buildForm(). Рассмотрим получение той же формы. Инициализируем formBuilder() сервис, создаем объект формы и передаем в ее конструктор все необходимые сервис контейнеры.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
    use Drupal\Core\Cache\CacheBackendInterface;
    use Drupal\Core\Config\ConfigFactoryInterface;
    use Drupal\Core\Datetime\DateFormatterInterface;
    use Drupal\Core\Form\ConfigFormBase;
    use Drupal\Core\Form\FormStateInterface;
    use Drupal\system\Form\PerformanceForm;
     
    $container = \Drupal::getContainer();
    /** @var ConfigFactoryInterface $config_factory */
    $config_factory = $container->get('config.factory');
    /** @var CacheBackendInterface $render_cache */
    $render_cache = $container->get('cache.render');
    /** @var DateFormatterInterface $date_formatter */
    $date_formatter = $container->get('date.formatter');
    /** @var AssetCollectionOptimizerInterface $css_collection_optimizer */
    $css_collection_optimizer = $container->get('asset.css.collection_optimizer');
    /** @var AssetCollectionOptimizerInterface $js_collection_optimizer */
    $js_collection_optimizer = $container->get('asset.js.collection_optimizer');
     
    $form_builder = \Drupal::formBuilder();
    $form_object  = new PerformanceForm($config_factory, $render_cache, $date_formatter, $css_collection_optimizer, $js_collection_optimizer);
    //... манипуляции с объектом формы ...
    $form = $form_builder->getForm($form_object);

Домашнее задание

Ответ по прошлому заданию

Для начала добавим контекстную ссылку в блок. По условию задания необходимо, чтобы ссылка была в конкретном блоке и это можно сделать двумя способами:

  1. с использованием файла news.links.contextual.yml. В данном файле необходимо указать группу. Группу 'block' указывать нельзя, т.к. ссылка появится во всех блоках. Выход простой — использовать свою группу. Название группы можно дать какое-угодно, но для удобства и уникальности даем имя по ID контекстной ссылки.
    1
    2
    3
    4
    
    news.news_settings:
      title: 'Edit news settings'
      route_name: news.news_settings
      group: 'news.news_settings'

    Далее необходимо создать файл news.module и разместить его в корне модуля. В файле определяем хук hook_block_view_BASE_BLOCK_ID_alter(). В хуке важно после ключа #contextual_links указать имя группы, далее идет массив с ключом route_parameters, в нем массив параметров (block и машинное имя блока — newsblock).

    1
    2
    3
    4
    5
    6
    7
    8
    
    use Drupal\Core\Block\BlockPluginInterface;
     
    /**
     * Implements hook_block_view_BASE_BLOCK_ID_alter().
     */
    function news_block_view_newsblock_alter(array &$build, BlockPluginInterface $block) {
      $build['#contextual_links']['news.news_settings']['route_parameters']['block'] = 'newsblock';
    }
  2. без использования файла news.links.contextual.yml. Потребуется создать файл news.module и разместить в нем хук hook_contextual_links_alter(), в котором и добавим требуемую ссылку. Имя ключа новой ссылки (в нашем случае news.news_settings) может быть любое, главное — имя ключа в массиве должно быть уникальным дабы не переопределить уже существующую ссылку.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    /**
     * Implements hook_contextual_links_alter().
     */
    function news_contextual_links_alter(array &$links, $group, array $route_parameters) {
      if ($group == 'block' && $route_parameters['block'] == 'newsblock') {
        $links['news.news_settings'] = [
          'route_name' => 'news.news_settings',
          'route_parameters' => $route_parameters,
          'title' => t('Edit news settings'),
          'weight' => NULL,
          'localized_options' => [],
          'metadata' => [
            'langcode' => 'en',
          ]
        ];
      }
    }

Первый вариант предпочтительнее (на нем и остановимся), т.к. позволяет оставить определение контекстной ссылки там, где оно и должно находится — в news.links.contextual.yml файле.
Сбрасываем кеш и получаем такой результат
контекстная ссылка
Переходим к табам. Для таба "Settings" route_name пока фейковый. Создаем файл news.links.task.yml

1
2
3
4
5
6
7
8
9
10
11
news.blocklist:
  route_name: news.news_settings
  title: 'News block list'
  base_route: news.news_settings
  weight: 1
 
news.settings:
  route_name: dblog.overview
  title: 'Settings'
  base_route: news.news_settings
  weight: 2

На очереди action ссылка (route_name пока фейковый). Создаем для нее файл news.links.action.yml

1
2
3
4
5
news.add_block:
  route_name: news.news_settings
  title: 'Add news block'
  appears_on:
    - news.news_settings

В итоге, по адресу admin/config/services/news должна быть следующая картина
ссылки для модуля news

Задание по текущему уроку

Задание на текущий урок — создать UI для добавления/удаления/редактирования блоков новостей. Блок новости — это тот же друпаловский блок с типом basic block (впоследствии будет создан свой тип).
Итак, что необходимо сделать:

  • Пользователь, по нажатию на таб Settings, должен видеть список новостных источников, который в данный момент возвращает News block list, т.е. контроллер NewsSettingsController будет отрабатывать для второго таба.
  • На дефолтном табе News block list необходимо отображать форму со списком блоков в виде таблицы. Таблица должна быть draggable, т.е. должна быть возможность менять порядок строк. Сохранение порядка строк пока работать не будет (не реализовано хранение весов), поэтому кнопка Save будет только выводить сообщение, что настройки сохранены. Таблица имеет 3 колонки:
    1. name — заголовок блока
    2. description — описание блока
    3. operations — это dropbutton со списком ссылок edit/delete.

    Пример макета таблицы, которая должна быть отображена на дефолтном табе.
    таблица списка новостных блоков

  • По клику по action ссылке Add news block, пользователь должен попадать на форму создания блока. На этой форме будут отображаться поля блока, а также кнопка сохранить и ссылка Cancel. Cancel ссылка будет вести на на страницу со списком блоков.
  • При редактировании блока должна использоваться та же форма, что и при создании. При нажатии по кнопке Save, блок должен быть сохранен, пользователь возвращен на страницу со списком блоков с сообщением New block BLOCK_NAME has been created. По ссылке Cancel пользователь возвращается на страницу со списком блоков.
  • При нажатии по ссылке Delete в колонке Operations — выводить форму подтверждения с текстом Are you sure want to delete this block BLOCK_NAME. Для построения формы использовать базовый класс ConfirmFormBase.

Цель домашнего задания — закрепить знания по работе с формами, поэтому мы намеренно не используем на текущем этапе стандартную форму создания блока, а также Views для вывода списка всех блоков.

Дополнительная информация по статье

  1. https://www.drupal.org/docs/8/api/form-api/introduction-to-form-api - введение в Form API из официальной документации.
  2. https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!FormBase.php/class/FormBase/8.2.x - класс FormBase.
  3. https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!ConfigFormBase.php/class/ConfigFormBase/8 - класс ConfigFormBase.
  4. https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!ConfirmFormBase.php/class/ConfirmFormBase/8 - класс ConfirmFormBase.
  5. https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!form.api.php/function/hook_form_alter/8.2.x - описание hook_form_alter().
  6. https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!form.api.php/function/hook_form_FORM_ID_alter/8.2.x - описание hook_form_FORM_ID_alter().
  7. https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!form.api.php/function/hook_form_BASE_FORM_ID_alter/8.2.x - описание хука hook_form_BASE_FORM_ID_alter().
  8. Версии программных продуктов, используемых в статье: Drupal 8.3.2

1 Комментарий