Перейти к основному содержанию
menu

Основное внимание в этой главе уделяется созданию модулей. В первой и второй частях серии мы познакомились с концепциями ООП в Drupal и узнали о некоторых компонентах Symfony, которыми пользуется Drupal 8. Теперь мы собираемся начать кодирование.

Вот некоторые из важных тем, которые мы рассмотрим в этой главе:

  • Запуск нового модуля
  • Создание файлов .info.yml для предоставления Drupal информации о модуле.
  • Создание файлов .module для хранения кода Drupal
  • Добавление новых блоков с использованием подсистемы блоков
  • Форматирование кода в соответствии со стандартами кодирования Drupal
  • Написание автоматизированного теста для Drupal

К концу этой главы у вас должны быть базовые знания, необходимые для создания собственного модуля с нуля.

 

Наша цель: модуль с блоком


В этой главе мы собираемся построить простой модуль. Модуль будет использовать подсистему блоков для добавления нового пользовательского блока. Блок, который мы добавим, просто отобразит список всех включенных в данный момент модулей в нашей установке Drupal.

Мы собираемся разделить эту задачу построения нового модуля на три части:

  • Создайте новую папку модуля и файлы модуля
  • Работа с блочной подсистемой
  • Написание автоматических тестов с использованием среды тестирования SimpleTest, включенной в Drupal 8

Давайте теперь перейдем к первому шагу создания нового модуля.

 

Создание нового модуля


Создание модулей Drupal - очень простая задача. Как легко? Настолько легко, что у Drupal есть тысячи представленных модулей, которые расширяют его базовую функциональность различными способами, и многие разработчики Drupal не обязательно являются экспертами PHP. Цель этой главы - показать, как легко написать свой собственный модуль, создав несколько каталогов и три небольших файла.

 

Имена модулей


Для именования нашего модуля нам необходимо указать два имени: 

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

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

Это было значительно улучшено в Drupal 8, и размещение наших модулей намного проще и понятнее.

drupal api

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

Есть две основные причины для размещения модулей в нужной папке:

Держать их отдельно от основных модулей
Чтобы разрешить безопасные, автономные обновления кода
Поскольку ядро ​​теперь находится в отдельной отдельной папке (которую я настоятельно не рекомендую изменять), гораздо проще выяснить, где размещены наши модули: / modules . Эта папка является рекомендуемым местом для размещения дополнительных и пользовательских модулей.

Drupal также позволяет нам размещать модули в разных папках. В случае, если мы разрабатываем мультисайт (это означает, что мы обслуживаем несколько сайтов Drupal из одной единственной установки), мы также можем поместить наши модули в / sites / all / modules (эти папки по умолчанию не существуют, поэтому нам нужно создать их). Модули, размещенные здесь, будут доступны на всех сайтах, а модули, специфичные для сайта, могут быть размещены в папке / sites / site_one / modules, чтобы они были доступны только на первом сайте . 

Drupal также позволяет нам создавать подпапки в папке / modules для классификации наших модулей. Это очень полезно с точки зрения разделения добавленных и пользовательских модулей. 

На протяжении всей этой серии мы будем создавать наши собственные модули в папке / modules / custom . Это соответствует рекомендациям Drupal, а также упрощает поиск наших модулей в отличие от всех других дополнительных модулей.

 

Создание каталога модулей


Теперь, когда мы знаем, что наши модули должны находиться в / modules / custom , мы можем создать новый модуль здесь.

Модули могут быть организованы различными способами, но лучше всего создать каталог модулей в / modules / custom , а затем поместить в него как минимум два файла: .info.yml (он же «файл info yaml») ) и файл .module ("dot-module").

Каталог должен быть назван с машиночитаемым именем модуля. Аналогично, файлы .info.yml и .module должны использовать машиночитаемое имя модуля.

Мы будем называть наш первый модуль first , так как это наш первый Drupal 8 модуль. Таким образом, мы создадим новый каталог / модули / пользовательский / первый , а затем создаем first.info.yml файл и first.module файл:

folder modules

Убедитесь, что ваш веб-сервер может читать файлы .info.yml и .module . Однако он не должен иметь возможности записи в любой файл.


Далее мы напишем содержимое файла .info.yml .

 

Написание файла .info.yml


Цель файла .info.yml - предоставить Drupal информацию о модуле (или теме, или профиле установки) - такую ​​информацию, как удобочитаемое имя, от каких других модулей зависит этот модуль и от какие файлы он содержит.

.Info.yml файл представляет собой обычный текстовый файл в формате YAML. Директива в файле .info.yml состоит из имени, двоеточия и значения:

name: value

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

name:
  - value1
  
  - value2

Для начала давайте взглянем на полный файл first.info.yml для нашего первого модуля:

name: First
type: module
description : 'Our first Drupal 8 module'
package: Drupal 8 Module Development
core: 8.x
dependencies:
  - block

Этот файл из восьми строк так же сложен, как и файл модуля .info.yml . Давайте рассмотрим эти директивы, чтобы выяснить, что они делают:

name ( обязательно ) - это читаемое человеком имя модуля в том виде, в каком оно отображается в интерфейсе пользователя.
type ( обязательный ) - тип проекта (возможны варианты модуля , темы и профиля - для профилей установки).
описание ( обязательно ) - это описание модуля в том виде, в каком оно отображается в интерфейсе пользователя.
Пакет (необязательно) позволяет классифицировать модули в интерфейсе пользователя. Модули с одинаковым пакетом  появятся в той же категории на странице «Расширtybq» (где модули могут быть включены или отключены).
core ( обязательно ) определяет основную версию Drupal, с которой совместим наш модуль.
Зависимости (необязательно) определяют любой модуль (и), от которого должен работать наш модуль, другими словами, зависит. Например, при вызове кода из другого модуля нам нужно убедиться, что модуль включен в Drupal, в противном случае мы получим ошибку.
Теперь мы создали наш первый файл .info.yml . Как только Drupal прочитает этот файл, модуль появится на странице Расширения.

extend

На снимке экрана обратите внимание, что модуль появляется в пакете DRUPAL 8 DEVELOPMENT и имеет ИМЯ и ОПИСАНИЕ, как определено в файле .info.yml .

Закончив наш файл .info.yml , мы можем двигаться дальше и закодировать наш файл .module .

 

Создание файла модуля


.Module файл является PHP - файлом , который обычно содержит все основные реализации хуков для модуля.

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

Для начала мы создадим простой файл first.module, который содержит единственную реализацию хука - ту, которая предоставляет справочную информацию.

<?php

/**
 * @file
 * A module demonstrating Drupal coding practices and APIs.
 *
 * This module provides a block that lists all of the
 * installed modules in our Drupal system. It illustrates coding standards,
 * practices, and API use for Drupal 8.
*/

/**
* Implements hook_help().
*/
function first_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) {
  if ($route_name == 'help.page.first') {
    return t('A demonstration module.');
  }
}

Drupal использует стиль документации, называемый doc blocks . Это комментарии, которые можно извлечь с помощью различных инструментов (например, Doxygen), которые могут форматировать информацию для использования другими разработчиками. Весь Drupal документирован с использованием блоков Doc, и вам также рекомендуется использовать их в своем собственном коде. Чтобы увидеть извлеченные блоки документов Drupal в действии, зайдите на  http://api.drupal.org

Прежде чем перейти к функции, давайте посмотрим на блок документации здесь. Это одно предложение: реализует hook_help () . Это описание соответствует стандарту кодирования Drupal. Когда наша функция является подключаемой реализацией, она должна указывать это в формате, указанном выше: Реализует NAME OF HOOK . какой в ​​этом смысл? Чтобы разработчики могли быстро определить назначение функции, а также чтобы автоматизированные инструменты (такие как Doxygen) могли найти реализации хуков в нашем коде.

Обратите внимание, что мы больше не добавляем описание и не документируем параметры. Это нормально, когда два утверждения верны:

Функция реализует хук
Функция проста
В таких случаях подойдет однострочное описание, поскольку кодеры могут просто обратиться к документации API для получения дополнительной информации.

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

 

Хук помощи


Drupal определяет хук под названием hook_help () . Хук помощи вызывается, когда пользователь просматривает внутреннюю справочную систему. Каждый модуль может иметь одну реализацию hook_help () . Наш модуль предоставляет краткий справочный текст путем реализации хука помощи.

<?php

function first_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) {
  if ($route_name == 'help.page.first') {
    return t('A demonstration module.');
  }
}

 

Как эта функция становится реализацией хука? Просто объявив имя функции: first_help () . Название расположено перед шаблоном хука. Если хук называется hook_help () , то для ее реализации мы заменим слово hook на имя модуля . Таким образом, чтобы реализовать hook_help () , мы просто объявляем функцию в нашем первом модуле с именем first_help () .

Каждый хук имеет свои собственные параметры, и все основные хуки Drupal описаны на http://api.drupal.org .
Hook_help () реализация принимает два аргумента:

$ route_name: справочная система route(маршрутизации)
$ route_match: может использоваться для создания разных выходных данных справки для разных страниц с одинаковым маршрутом
В нашем случае мы имеем дело только с первым из этих двух. По сути, справочная система работает путем сопоставления маршрутов с текстом справки. Наш модуль должен объявить, какой текст справки должен быть возвращен для данного маршрута.

В частности, текст справки по всему модулю должен быть доступен в URI admin / help / MODULE_NAME , где MODULE_NAME - машиночитаемое имя модуля.

Наша функция работает путем проверки параметра $ route . Если $ route  -  admin.help.first  (экран справки по умолчанию для модуля), он вернет простой текст справки.

Если бы мы включили наш новый модуль и затем посмотрели на страницу справки Drupal, мы бы увидели это:

drupal api

Функция t () и переводы


Каждая языковая строка, отображаемая пользователю, должна быть заключена в функцию t () . Зачем? Потому что функция t () отвечает за перевод строк с одного языка на другой.

Эта функция должна быть понятна каждому разработчику:

Что происходит, когда вызывается t ()


Дополнительные функции, которые вы получаете, используя функцию t ()


Сначала давайте посмотрим, что делает функция t () при ее вызове. Если в нашей установке поддержка языка не включена и второй аргумент не передается в t () , он просто возвращает строку без изменений. Если у нас включено несколько языков и язык пользователя не является английским (который является языком по умолчанию), Drupal попытается заменить строку строкой на выбранном языке.

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

Во многих приложениях PHP вы можете увидеть такой код:

<?php

print "Welcome, $username.";

Приведенный выше код заменит $ username значением переменной $ username . Этот код оставляет открытой возможность того, что значение $ username содержит данные, которые могут нарушить HTML в выводе - или, что еще хуже, позволить злоумышленнику внедрить JavaScript или другой код в наше приложение.

Функция t () предоставляет альтернативный и более безопасный метод замены заполнителей в тексте значением. Функция принимает необязательный второй аргумент, который представляет собой ассоциативный массив элементов, которые можно заменить. Вот пример, который заменяет предыдущий код:

<?php

$values = array('@user' => $username);
print t('Welcome, @user', $values);

В предыдущем случае мы объявляем заполнитель с именем @user со значением переменной $ username . Когда функция t () выполняется,  значения  массива $ values используются для замены заполнителей правильными данными. Но есть и дополнительное преимущество: эти замены выполняются безопасным способом .

Если местозаполнитель начинается с @ , то перед тем, как вставить значение, Drupal очищает значение, используя свой внутренний метод Html :: escape () (с которым мы столкнемся много раз в последующих главах).

Если вы уверены, что строка не содержит никакой опасной информации, вы можете использовать другой символ, чтобы назначить заполнитель,: восклицательный знак (!). Когда это используется, Drupal просто вставит значение как есть. Это может быть очень полезно, когда вам нужно вставить данные, которые не нужно переводить:

<?php

$values = array('!url' => 'http://example.com');
print t('The website can be found at !url', $values);

В этом случае URL будет введен без экранирования. Мы можем сделать это безопасно только потому, что мы уже знаем значение URL. Это не исходит от доверенного пользователя.

Наконец, существует третий декоратор-заполнитель: знак процента (%) говорит Drupal покинуть код и пометить его как выделенный.

<?php

$values = array('%color' => 'blue');
print t('My favorite color is %color.', $values);

Это не только удалит все опасные символы из значения, но также вставит разметку, чтобы рассматривать этот текст как выделенный текст. По умолчанию предыдущий код приведет к печати строки. Мой любимый цвет - <em> синий </ em> . <EM> теги были добавлены с помощью внутреннего метода ( строка :: заполнителя () ) вызывается т () функция.

Есть варианты использования, которые можно сделать с помощью t () , format_plural () , контекстов перевода и других функций системы перевода. Чтобы узнать больше, вы можете обратиться к документации API для t () по адресу http://api.drupal.org/api/function/t/8 .
Хорошо, это было довольно длинное объяснение функции t () и ее функций, но для этого есть веская причина. Теперь вы понимаете, почему в своем собственном коде при написании модулей Drupal рекомендуется использовать функцию t () всякий раз, когда ваш модуль выводит строки в пользовательский интерфейс.

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

 

Работа с блочным API


Блочный API существенно изменился по сравнению с предыдущими версиями Drupal. Ранее для создания новых блоков нужно было реализовать несколько хуков и написать процедурный код. Блоки в Drupal 8 теперь являются плагинами , которые предоставляют унифицированную замену в стиле ООП для создания подключаемых функциональных возможностей в Drupal. Если вы не знакомы с новым API плагинов, вам не о чем беспокоиться. В следующей главе мы подробно рассмотрим это.


Мы собираемся создать блок, который отображает маркированный список всех модулей, которые в настоящее время включены на нашем сайте. Поскольку блоки определены как плагины, им нужен менеджер плагинов, который позаботится об обнаружении и создании экземпляров блочных плагинов. Менеджер плагинов - это, по сути, класс PHP, который сообщает Drupal, где он находит плагины определенного типа и как он должен их создавать. Мы подробно рассмотрим менеджеры плагинов в следующей главе, но сейчас давайте быстро взглянем на конструктор класса менеджера плагинов модуля Block в core / lib / Drupal / Core / Block / BlockManager.php :

<?php

public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');

    $this->alterInfo('block');
    $this->setCacheBackend($cache_backend, 'block_plugins');
  }

Как мы узнаем, что этот файл нам нужен? 

Поскольку менеджеры плагинов должны быть определены как сервисы, мы можем взглянуть на  файл core.services.yaml ядра Drupal, который содержит ссылку на этот класс в настройке plugin.manager.block .

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

<?php

parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');

Этот вызов конструктора класса  DefaultPluginManager  (класс BlockManager расширяется) в основном описывает систему плагинов, как модуль Block предпочитает определять свои плагины, и, по сути, говорит нам разработчикам, как нам нужно создавать новые блоки. Мы еще глубже погрузимся в систему плагинов в следующей главе, но сейчас давайте рассмотрим два наиболее важных параметра этого вызова: первый и последний.

Первый параметр, « Plugin / Block », сообщает DefaultPluginManager о  том, что блочные плагины могут быть найдены системой плагинов в папке Plugin / Block любого реализуемого модуля . По сути это означает, что мы можем поместить наши пользовательские файлы блоков в папку с таким именем, и система плагинов автоматически найдет наши пользовательские блоки. Следуя стандарты PSR-4, фактический путь этой папки будет имя_модуль / SRC / Plugin / блок (замена MODULENAME с машиночитаемым именем нашего модуля) .

Мы можем разместить столько файлов блоков в этой папке, сколько захотим. Drupal автоматически найдет и создаст их экземпляр. Эти файлы должны быть простыми PHP-файлами, которые содержат класс, расширяющий  BlockBase (абстрактный класс, предоставляемый модулем Block, который обеспечивает поддержку ряда функций API, таких как управление конфигурацией и контроль доступа). 

Последний параметр вызова,  Drupal \ Core \ Block \ Annotation \ Block , определяет пространство имен класса Annotation, которое система плагинов должна использовать при попытке обнаружить блоки, определенные в нашей системе. Мы можем найти этот класс в  core / lib / Drupal / Core / Block / Annotation / Block.php :

<?php

namespace Drupal\Core\Block\Annotation;

use Drupal\Component\Annotation\Plugin;

/**
 * Defines a Block annotation object.
 *
 * @ingroup block_api
 *
 * @Annotation
 */
class Block extends Plugin {

  /**
   * The plugin ID.
   *
   * @var string
   */
  public $id;

  /**
   * The administrative label of the block.
   *
   * @var \Drupal\Core\Annotation\Translation
   *
   * @ingroup plugin_translatable
   */
  public $admin_label = '';

  /**
   * The category in the admin UI where the block will be listed.
   *
   * @var \Drupal\Core\Annotation\Translation
   *
   * @ingroup plugin_translatable
   */
  public $category = '';

}

 

Хотя этот PHP-класс действительно прост, здесь следует отметить две важные вещи.


Во-первых, он определяет объект аннотации . Это достигается путем объявления @Annotation прямо перед оператором класса Block. В свою очередь, при определении наших пользовательских блоков мы можем использовать аннотацию @Block прямо перед нашими утверждениями класса. 

Вторым важным моментом, который следует отметить, является то, что класс Block определяет некоторые свойства, которые будут иметь объекты модуля Plugin, когда они будут созданы. 

Первое свойство, $ id , станет уникальным идентификатором данного блока в системе. Важно, чтобы этот идентификатор был уникальным; в противном случае это приведет к ошибке. 

Второе свойство, $ admin_label , представляет собой переводимую метку данного блока, как она появляется в пользовательском интерфейсе. 

Третий аргумент, $ category , позволяет нам классифицировать наш Блок в пользовательском интерфейсе, чтобы мы могли легко найти их при размещении блоков на нашем сайте Drupal.

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

<?php

/**
 * Provides a cool block
 *
 * @Block(
 *   id = "cool_block",
 *   admin_label = @Translation("Our Cool Block"),
 *   category = @Translation("Development")
 * )
 */
class CoolBlock extends BlockBase {
  //...
}

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

Наш первый пользовательский блок
Как мы узнали из предыдущего раздела, Drupal ожидает, что наши блоки будут находиться в папке  modulename / src / Plugin / Block . В нашем случае фактическая структура папок будет такой / src / Plugin / Block . Давайте создадим эту структуру папок:

stuctch

Теперь мы создали папку, которая будет содержать наши пользовательские блоки. Как мы узнали из предыдущего раздела, файлы PHP, помещенные в эту папку, будут автоматически подхвачены Drupal и распознаются как блочные плагины (учитывая, что они наследуются из класса BlockBase и определяют действительную аннотацию плагина). Итак, давайте создадим наш первый настоящий класс Block.

Мы собираемся поместить наш первый пользовательский класс Block в файл с именем FirstBlock.php . На момент написания, нет никаких ограничений на то, какие имена файлов использовать при создании пользовательских блоков (или любого другого типа плагина). Рекомендуется использовать стиль CamelCase в именах файлов. Итак, давайте сначала создадим файл / src / Plugin / Block / FirstBlock.php со следующим содержимым:

<?php

/**
 * @file
 * Contains \Drupal\first\Plugin\Block\FirstBlock.
 * 
 * Our first custom Block that displays a list of enabled
 * modules in our Drupal site. It also demonstrates a few
 * API methods provided by the Block sub-system, such as 
 * configuration form, form validation and access control.
 */

namespace Drupal\first\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\Annotation\Block;
use Drupal\Core\Session\AccountInterface;

/**
 * Provides a simple block with a list of enabled modules
 *
 * @Block(
 *   id = "first_block",
 *   admin_label = @Translation("Our first Block"),
 *   category = @Translation("Drupal 8 Development")
 * )
 */
class FirstBlock extends BlockBase {
    
}

Как мы видим выше, мы начинаем со стандартного блока документов, который содержит несколько важных примечаний. Во-первых, мы определяем, какое пространство имен содержит этот файл, объявляя Contains \ Drupal \ first \ Plugin \ Block \ FirstBlock . Хотя это и не требуется, рекомендуется указывать пространство имен, с которым мы сейчас работаем, в самом начале файла, в главном блоке документации. Это значительно упрощает поиск заданного пространства имен или класса.

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

Сразу после блока документов мы определяем пространство имен нашего класса, объявляя пространство имен Drupal \ first \ Plugin \ Block . Это необходимо для работы нашего блока. Он также должен соответствовать стандартам PSR-4, и мы должны тщательно следить за тем, чтобы наше пространство имен указывало на соответствующие папки и файлы, иначе наш Блок не будет инициализирован системой плагинов.

После объявления пространства имен мы импортируем несколько пространств имен, которые нам понадобятся для нашего блока, используя инструкцию use :

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\Annotation\Block;

Первое импортируемое нами пространство имен - это Drupal \ block \ BlockBase . Это основной класс Plugin, определенный модулем Block, который предоставляет большинство методов API для блоков. Мы будем расширять этот класс при создании нашего первого пользовательского блока.

Затем мы импортируем Drupal \ block \ Annotation \ Block , который позволяет нам создавать аннотации @Block, чтобы мы могли дать системе плагинов команду инициализировать наш класс как блочный плагин.

После импорта всех пространств имен, в которых нуждается наш Блок, мы переходим к объявлению определения плагина. Как мы узнали ранее в этой главе, определение плагина должно находиться в блоке документа непосредственно перед нашим определением класса и должно использовать синтаксис аннотаций для предоставления информации системе плагинов о нашем блоке:

/**
 * Provides a simple block with a list of enabled modules
 *
 * @Block(
 *   id = "first_block",
 *   admin_label = @Translation("Our first Block"),
 *   category = @Translation("Drupal 8 Module Development")
 * )
 */
class FirstBlock extends BlockBase {

Лучше всего начинать этот блок документации с описания того, что делает наш Блок. Простой однострочный комментарий подойдет в большинстве случаев; главное, что мы документируем наш класс Block для дальнейшего использования.

Затем, все еще в этом блоке документа, мы определяем наш плагин блока, используя синтаксис аннотации:

* @Block(
*   id = "first_block",
*   admin_label = @Translation("Our first Block"),
*   category = @Translation("Drupal 8 Module Development")
* )

Здесь мы инструктируем систему плагинов для создания блочного плагина, объявив @Block () . Как мы узнали ранее, эта аннотация предоставляется Drupal \ Core \ Block \ Annotation \ Block . Мы также определяем некоторые значения в определении нашего плагина, которые будет использовать наш блочный плагин, такие как уникальный идентификатор ( id = «first_block» ), удобочитаемая, переводимая метка ( admin_label = @Translation («Наш первый блок») ) и категория, под которой наш блок будет отображаться в пользовательском интерфейсе ( category = @Translation («Разработка модулей Drupal 8») ).

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

Метод build ()


При работе с блоками единственный метод, который нам требуется реализовать, это build () . Этот метод отвечает за предоставление фактического вывода нашего блока. Другими словами, это то, куда идет содержание нашего Блока. Итак, давайте реализуем этот метод в нашем классе:

<?php

/**
 * @file
 * Contains \Drupal\first\Plugin\Block\FirstBlock.
 * 
 * Our first custom Block that displays a list of enabled
 * modules in our Drupal site. It also demonstrates a few
 * API methods provided by the Block sub-system, such as 
 * configuration form, form validation and access control.
 */

namespace Drupal\first\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\Annotation\Block;

/**
 * Provides a simple block with a list of enabled modules
 *
 * @Block(
 *   id = "first_block",
 *   admin_label = @Translation("Our first Block"),
 *   category = @Translation("Drupal 8 Module Development")
 * )
 */
class FirstBlock extends BlockBase {
  
  /**
   * Implements \Drupal\Core\Block\BlockPluginInterface::build().
   *  
   * Displays the currently enabled modules in our Drupal site. 
   */
  public function build() {    

    $build = array();

    // Use the global Drupal Service container to get
    // the list of enabled modules
    $modules = \Drupal::moduleHandler()->getModuleList();

    // Loop through the modules and save them in a variable
    $list = array();
    foreach ($modules as $module => $path) {      
      $list[$module] = $module;     
    }    

    // Return the output in a Render array
    $build = array(
      '#theme'	=>  'item_list',
      '#items'	=>  $list,
      '#list_type'  =>  'ol'
    );   

    return $build;    
  }
}

Давайте посмотрим, что здесь происходит. Как мы видим, метод начинается с блока документов. Это, опять же, рекомендуемая практика для документирования нашего кода. Мы добавляем блок doc к нашему методу, чтобы документировать, какой метод родительского класса мы реализуем (или переопределяем) и что делает эта реализация.

/**
   * Implements \Drupal\Core\Block\BlockPluginInterface::build().
   *  
   * Displays the currently enabled modules in our Drupal site. 
   */
  public function build() { 

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

$build = array();

Эта переменная должна быть массивом, так как мы должны передавать массив Render в слой темы при построении вывода.

Массивы рендеринга вместе со слоем темы подробно описаны в следующей главе.

 Следующее, что мы делаем, - это извлекаем список модулей, которые в данный момент включены на нашем сайте Drupal. Для этого мы используем метод moduleHandler () глобального контейнера Drupal Service, который предоставляет нам доступ к ряду удобных методов, таких как получение списка включенных модулей.

// Use the global Drupal Service container to get
// the list of enabled modules
$modules = \Drupal::moduleHandler()->getModuleList();

Этот метод возвращает список включенных модулей в ассоциативном массиве с ключами, являющимися именами модулей, и значениями, являющимися значением этих имен. На следующем шаге мы пройдемся по этому массиву и создадим список имен модулей, которые мы фактически отобразим в нашем блоке.

// Loop through the modules and save them in a variable
$list = array();
foreach ($modules as $module => $path) {      
  $list[$module] = $module;     
}  

Теперь у нас есть необработанный массив данных. Следующее, что нам нужно сделать, это отформатировать его для отображения. В Drupal форматирование почти всегда выполняется тематическим, иначе слоем темы. Здесь мы хотим передать данные слою темы и заставить его превратить наш список модулей в упорядоченный список HTML. Мы делаем это, заставляя нашу функцию возвращать массив Render, который Drupal распознает и превращает в список HTML:

// Return the output in a Render array
$build = array(
  '#theme'  =>  'item_list',
  '#items'  =>  $list,
  '#list_type'  =>  'ol'
);

Здесь мы использовали внутреннюю функцию Drupal theme_item_list (), которая выводит список HTML в слой темы. 

В следующей главе мы подробно рассмотрим систему тем. Однако сейчас мы просто представим, что когда мы используем функцию theme, как мы делали выше, она возвращает отформатированный HTML.
Основная внутренняя функция для работы с системой тем - _theme () . В Drupal 8 _theme () принимает один или два аргумента:

  • Название темы операции
  • Ассоциативный массив переменных


Чтобы отформатировать массив строк в список HTML, мы используем  item_list и передаем ассоциативный массив, содержащий две переменные:

  • Элементы которые мы хотим перечислить
  • Тип списка, который мы хотим отобразить


Помните, что вы не должны вызывать внутреннюю функцию _theme () непосредственно в своем коде. Для этого есть несколько веских причин, включая обход кэша, пропуск фаз pre_render и post_render и т. д. Мы обсудим эти аспекты в следующей главе. Всякий раз, когда вы хотите использовать функцию темы, используйте метод, который мы использовали в приведенном выше примере кода.

 Мы написали целый модуль (хотя у нас еще есть код для автоматического тестирования). Посмотрим, как выглядит наш новый Блок в браузере.

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

Давайте включим наш модуль на странице « Расшрений» (начните вводить имя модуля в поле поиска над списком модулей и включите его), а затем перейдите к Структуре |список блоков, где мы можем видеть все блоки, определенные на нашем сайте Drupal. Давайте попробуем разместить наш новый блок в области контента нашего сайта:

block

Расширение нашего блока


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

Доступ к блокам


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

Однако Block API также позволяет нам определять наши собственные правила контроля доступа прямо в нашем классе Block. Это можно сделать, переопределив метод BlockBase :: blockAccess () . Этот метод должен возвращать логическое значение, указывающее, разрешено ли пользователю ( TRUE ) или нет ( FALSE ) просматривать наш блок.

Метод blockAccess ()


Наша цель - переопределить этот метод и разрешить доступ пользователям только с разрешением « администрировать блоки ». Для этого мы вернемся к нашему классу FirstBlock и создадим метод с именем blockAccess () :

/**
 * {@inheritdoc}
 */
protected function blockAccess(AccountInterface $account) {
  // Only grant access to users with the 'administer blocks' permission.
  return AccessResult::allowedIfHasPermission($account, 'administer blocks');
}

Реализуя этот метод, мы можем поручить Блочному API показывать наш Блок только тем пользователям, которые принадлежат к роли пользователя, у которой есть доступ к разрешению, называемому « администрировать блоки ». Кроме того, в этой реализации метода мы используем пару низкоуровневых блоков Drupal : AccountInterface и AccessResult . 

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

AccessResult построен на основе этого интерфейса, предоставляя удобные методы для определения того, следует ли разрешить или запретить действующему пользователю доступ для выполнения определенной операции на нашем сайте Drupal. Используя его методы, мы можем вернуть  объект AccessResult, который содержит эту информацию.

Эти классы, однако, не известны нашему классу блочных плагинов; это отдельные классы, которые нужно импортировать в наш класс. Новая архитектура Drupal 8 предлагает отличный способ разделить определенные части функциональности на их собственные, автономные классы. Используя этот децентрализованный шаблон, мы можем вручную выбрать конкретные строительные блоки, на которых будет основана наша новая функциональность. Таким образом, мы можем создать приложение, которое использует слой абстракции, чтобы его можно было легко поддерживать в долгосрочной перспективе.

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

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\Annotation\Block;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;

После того, как мы импортировали необходимые пространства имен, Drupal с радостью выполнит реализацию нашего метода blockAccess () и отобразит или скроет наш новый блок, в зависимости от результата проверки разрешения.

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

Предоставление дополнительной конфигурации блока


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

block access

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

К счастью, Block API позволяет нам сделать очень просто. 

Метод blockForm ()


В случае, если мы хотим расширить форму конфигурации блока и предоставить свои собственные параметры конфигурации, все, что нам нужно сделать, это реализовать метод blockForm (), предоставляемый BlockBase . Этот метод ожидает от нас предоставления элементов формы конфигурации с использованием синтаксиса Form API .

API форм Drupal - это мощный инструмент, позволяющий разработчикам быстро и легко создавать формы и обрабатывать их. Это делается путем определения массивов элементов формы и создания проверки и отправки обратных вызовов для формы. Если вы не знакомы с этим API, не беспокойтесь. Я объясню все шаги по мере продвижения, а также, этот API будет подробно объяснен в следующей главе.
Давайте продолжим и реализуем этот метод в нашем классе FirstBlock . Мы собираемся добавить простое текстовое поле в форму конфигурации, которое будет использоваться для контроля количества модулей, возвращаемых в списке.

/**
   * Overrides \Drupal\block\BlockBase::blockForm().
   */
  public function blockForm($form, FormStateInterface $form_state) {  
    // Add a simple text field on the Block configuration form
    // to limit the number of enabled modules to display
    $form['number_of_items'] = array(
      '#type' => 'textfield',
      '#title' => t('Number of items in list'),
      '#default_value' => $this->configuration['number_of_items'],      
      '#description' => t('Define the number of items returned in the list.')
    );
    return $form;
  }

Как обычно, мы начинаем с блока комментариев прямо перед реализацией, чтобы документировать наш код. Затем мы начинаем фактическую реализацию. Как мы видим, метод blockForm () требует передачи двух параметров:

  • $ form представляет всю форму конфигурации блока. Он содержит базовые элементы, определенные модулем основного блока, такие как заголовок блока, административная метка, отображаемая в пользовательском интерфейсе, и флажок, используемый для отображения или скрытия заголовка блока в пользовательском интерфейсе.
  • $ form_state представляет текущее состояние формы. Это переменная, которая (среди других данных, таких как информация о плагине) будет содержать данные отправки формы.

Как вы можете видеть выше, $ form_state является типизированным объектом, представляющим FormStateInterface (интерфейс, который позволяет нам работать с данными формы), который, опять же, неизвестен нашему классу блочных плагинов. Как и прежде, нам нужно импортировать этот интерфейс в наш текущий класс:

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\Annotation\Block;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;

Итак, по сути, все, что нам нужно сделать, - это расширить базовый массив $ form для предоставления наших собственных настроек конфигурации. Мы определяем новый элемент в массиве $ form с ключом  number_of_items . Мы должны быть осторожны с тем, какой ключ мы используем здесь, поэтому всегда убедитесь, что он уникален и настолько конкретен, насколько это возможно.

Следующие несколько строк определяют фактический элемент формы, используя синтаксис Form API. Я не буду здесь вдаваться в подробности, так как API формы будет подробно объяснен в следующей главе, но вот простая разбивка того, что здесь происходит:

'#type' => 'textfield' определяет, что мы хотим создать элемент формы текстового поля.
'#title' => t ('Количество элементов в списке') превратится в переводимый ярлык нашего поля.
'#default_value' => $ this-> configuration ['number_of_items'] предоставляет значение по умолчанию, которое предварительно заполняется при создании нового блока. Здесь также хранятся существующие данные для этого поля.
'#description' => t ('Определить количество элементов, возвращаемых в списке.') создает текст справки прямо под нашим полем. Рекомендуется предоставить описание нашего поля, чтобы помочь пользователям нашего сайта понять, для чего они могут использовать наше поле.
Теперь мы добавили наше текстовое поле в форму. Вернемся к структуре | настройки блока» рядом с нашим блоком. вы должны увидеть результат, похожий на этот:

block access

Как мы видим на скриншоте, наше вновь созданное поле теперь отображается в форме конфигурации блока вместе с основными полями, предоставленными по умолчанию. Хотя наше новое поле готово к использованию (это означает, что мы можем вводить в него данные), данные конфигурации необходимо где-то хранить, чтобы мы могли получить к ним доступ позже. Для этого мы реализуем метод blockSubmit () .

Метод blockSubmit ()


Метод blockSubmit () вызывается каждый раз, когда мы создаем или обновляем конфигурацию блока. Он обеспечивает обработку отправки в форме конфигурации блока. Другими словами, именно здесь мы можем проверить представленные данные и добавить дополнительную логику. 

Реализуя этот метод, мы можем сохранить наш пользовательский параметр конфигурации в свойстве конфигурации блока . Давайте реализуем этот метод в нашем классе FirstBlock .

/**
   * Overrides \Drupal\block\BlockBase::blockSubmit().
   * 
   * Stores the configuration setting based on the form
   * submission value.
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['number_of_items'] = intval($form_state->getValue('number_of_items'));    
  }  

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

При работе с формами $ form_state-> getValue () является методом, который используется, когда мы хотим проверить значения, представленные в форме. Этим методом можно получить значения полей представления формы, передав им соответствующие машиночитаемые имена.
До сих пор мы создали поле, которое мы можем использовать для определения нашей пользовательской настройки, и сохранили его значение в нашей конфигурации блока. Осталось только использовать это значение конфигурации в нашем блоке. Чтобы сделать это, нам нужно вернуться к методу build () нашего Блока и внести небольшие изменения, чтобы наш параметр конфигурации учитывался при построении списка модулей.

/**
   * Implements \Drupal\Core\Block\BlockPluginInterface::build().
   *  
   * Displays the currently enabled modules in our Drupal site. 
   */
  public function build() {    

    $build = array();

    // Use the global Drupal Service container to get
    // the list of enabled modules
    $modules = \Drupal::moduleHandler()->getModuleList();

    // Set an index for the loop to count the 
    // number of iterations
    $index = 1;

    // Loop through the modules and save them in a variable
    $list = array();
    foreach ($modules as $module => $path) {      
      $list[$module] = $module;
	  
      // Stop the loop here if $index equals the value 
      // set in the configuration form
      if ($index == $this->configuration['number_of_items']) {
        break;
      }

      $index++;
    }    

    // Return the output in a Render array
    $build = array(
      '#theme'  =>  'item_list',
      '#items'  =>  $list,
      '#list_type'  =>  'ol'
    );

    return $build;    
  }

Посмотрим, что мы здесь изменили. 

Прежде всего, мы добавили новую переменную с именем $ index , которую мы будем использовать для хранения количества итераций в цикле foreach () . Мы устанавливаем его начальное значение равным 1, а затем с каждой итерацией мы увеличиваем это число на единицу. 

Внутри самого цикла мы проверяем значение $ index  с каждой итерацией и, если оно равно значению, которое мы установили в форме конфигурации блока, мы останавливаем цикл. Мы делаем это, потому что к настоящему времени мы достигли количества элементов, которые мы хотим отобразить в нашем блоке, и все они хранятся в массиве $ list .

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

block access

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

Однако здесь есть только одна проблема: теоретически, мы можем ввести все виды символов в это текстовое поле, и это может нарушить функциональность. Как мы решаем это?

Метод blockValidate ()


Как и любая форма в Drupal, форма конфигурации Block имеет собственный метод проверки. Мы можем использовать этот метод для очистки данных, представленных в форме. Как и в методе blockSubmit () , мы можем использовать метод $ form_state-> getValue () для проверки отправленных значений в реализации blockValidate () .

/**
   * Overrides \Drupal\block\BlockBase::blockValidate().
   * 
   * Validates the configuration form.
   */
  public function blockValidate($form, FormStateInterface $form_state) {    
    if (!is_numeric($form_state->getValue('number_of_items'))) {      
      $form_state->setErrorByName('number_of_items', t('Please enter a numeric value.'));      
    }    
  }

Как видите, метод blockValidate () принимает те же аргументы, что и blockSubmit () . Все формы Drupal используют один и тот же шаблон; у них есть обратный вызов проверки и обратный вызов submit, и они оба принимают $ form и $ form_state в качестве аргументов. 

Обратный вызов проверки запускается перед обратным вызовом submit, и это место, где нам нужно отлавливать возможные ошибки пользователя. Характер возможных ошибок зависит от вашего фактического варианта использования; могут быть пользователи, которые вводят текст, когда ожидаются цифры (как в нашем случае здесь), или вводят символы, когда разрешен только простой текст. В случае, если мы обнаружим ошибку, нам нужно остановить процесс отправки формы и показать пользователю сообщение с указанием ошибки.

Итак, если мы оглянемся назад на наш метод blockValidate () , то мы проверяем, являются ли введенные данные числовыми, используя PHP- функцию is_numeric () . В случае,если он возвращает FALSE , мы используем FormStateInterface «s setErrorByName () метод , чтобы указать , что есть ошибка в данном элементе. 

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

block access

Здесь вы можете видеть, что наш метод проверки корректно определил, что поле содержит неверные данные, и поле подсвечено, чтобы указать на ошибку. Процесс отправки формы остановлен, поскольку введенное нами значение не является числовым. На этом этапе эти данные не сохраняются, потому что метод BlockSubmit () блока вызывается только в том случае, если проверка формы завершена без ошибок.

До сих пор мы довольно много рассмотрели в этой главе. Мы изучили некоторые основные понятия системы плагинов, чтобы мы могли создать наш первый Блок. Мы также разработали пользовательские функциональные возможности, используя Block API, и предоставили нашу пользовательскую опцию конфигурации, для которой мы предоставили пользовательский метод проверки. 

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

Написание автоматических тестов


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

Встроенная среда тестирования Drupal основана на инструменте автоматического тестирования, который называется PHPUnit (называемый «  Тестирование на странице расширения»). В предыдущих версиях Drupal классическое модульное тестирование не было обычным делом просто потому, что у разработчиков не было подходящих инструментов для этого. Это изменилось в Drupal 8 после интеграции PHPUnit, который является широко используемой (если не стандартизированной) структурой модульного тестирования для приложений PHP. 

Существуют различные типы тестов, которые могут быть построены в коде. Двумя популярными являются юнит-тесты и функциональные тесты .

Модульный тест ориентирован на тестирование отдельных частей кода. В объектно-ориентированном коде целью модульного тестирования часто является выполнение каждого метода объекта (или класса). В процедурном коде модульные тесты фокусируются на функциях и даже иногда на глобальных переменных. Цель состоит в том, чтобы просто убедиться, что каждый элемент (каждый блок) выполняет свою работу, как ожидалось.

Напротив, функциональные тесты предназначены для проверки такой ситуации, когда данный фрагмент кода вставляется в Drupal, он функционирует, как ожидается, в контексте приложения. Это более широкая категория тестирования, чем модульные тесты. Ожидается, что более крупные фрагменты кода (например, Drupal в целом) будут работать правильно еще до того, как функциональный тест сможет точно измерить правильность тестируемого кода. И вместо непосредственного вызова проверяемых функций функциональный тест часто выполняет все приложение в условиях, облегчающих проверку работоспособности тестируемого кода. Например, функциональные тесты Drupal часто запускают Drupal, добавляют пользователя, включают некоторые модули, затем извлекают URL-адреса через HTTP-соединение и, наконец, проверяют вывод.

Drupal 8 также представляет концепцию, называемую Kernel Testing . Этот тип тестирования находится между модульным тестированием и функциональным тестированием. При выполнении теста Kernel, фреймворк устанавливает облегченную версию нашего приложения, что позволяет нам тестировать новые компоненты и проверять, действительно ли они работают с существующими в Drupal. Для сравнения различных типов тестирования перейдите по ссылке https://www.drupal.org/docs/8/testing/types-of-tests-in-drupal-8 .

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

Мы собираемся создать несколько тестов для следующих сценариев:

  • Убедитесь, что наш блок отображается
  • Убедитесь, что наша проверка формы работает
  • Убедитесь, что наш контроль доступа работает


Поскольку нам понадобится полностью функциональная среда Drupal для тестирования нашего модуля, мы будем основывать наш тест на  классе BrowserTestBase, чтобы проверить его функциональность.

Хотя модуль тестирования включен в Drupal 8, он не включен по умолчанию. Перейдите на страницу « Расширений» и включите его. Как только он включен, в верхнем меню перейдите в  раздел « Конфигурация» , в разделе «Разработка» найдите страницу конфигурации тестирования. Это точка входа в пользовательский интерфейс тестирования.

Если вы не можете включить модуль тестирования, возможно, ваши пакеты разработки не установлены. В этом случае Drupal подскажет вам, что нужно запустить composer install --dev  в вашем терминале, который установит некоторые пакеты PHP, необходимые для работы PHPUnit.


Создание теста


Аналогично тому, как мы должны размещать наши блочные файлы в их определенных папках, среда тестирования Drupal также ожидает, что наши тестовые файлы будут следовать знакомому шаблону при создании наших тестов. Он ожидает, что мы поместим наши тесты в структуру папок, такую ​​как module_name / tests / src / test_type . В нашем случае это будет первый / tests / src / Functional .

Давайте продолжим и создадим эту структуру папок:

test folder

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

firstblocktest

Теперь мы готовы добавить код в FirstBlockTest.php.

 

Основная модель


Большинство функциональных тестов следуют простой схеме:

Создайте новый класс, который расширяет BrowserTestBase
Выполните любую необходимую настройку в методе setUp ()
Напишите один или несколько тестовых методов, начиная каждый метод со слова test
В каждом методе тестирования используйте одно или несколько утверждений для проверки фактических значений.
Когда мы пройдем наши собственные тесты, мы пройдем через каждый из этих шагов.


Во-первых, мы начнем с добавления тестового класса в наш файл FirstBlockTest.php . Это должно выглядеть примерно так:

<?php

/**
 * @file
 * Contains Drupal\Tests\first\Functional\FirstBlockTest
 */

namespace Drupal\Tests\first\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Provides test cases for our first custom Block
 *
 * @group first
 */
class FirstBlockTest extends BrowserTestBase {
  
  // ...
  
}

Как обычно, мы начинаем тестовый файл с блока комментариев. После этого мы объявляем наше пространство имен и импортируем класс BrowserTestBase, предоставленный Drupal. В большинстве случаев вы будете использовать этот класс при функциональном тестировании.

Затем мы документируем наш класс в следующем блоке документации и объявляем платформе тестирования, к какой группе тестирования принадлежит наш тестовый класс. Мы делаем это с помощью метода, с которым вы теперь должны быть знакомы: Синтаксис аннотации . @Group аннотации используются , что говорит системе тестирования Drupal, что этот тест класс является частью тестов , разработанных для модуля который называется "первый". Мы можем создать столько тестов для нашего модуля, сколько нам нужно; тестовые классы, принадлежащие к одной и той же группе, будут разделены на один и тот же раздел пользовательского интерфейса - мы увидим это чуть позже.

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

Настройка теста
Прежде чем реализовывать какие-либо методы в нашем тестовом классе, давайте настроим некоторые основные свойства, которые мы будем использовать в нашем коде:

/**
   * A user with permission to create and edit books and to administer blocks
   *
   * @var object
   */
  protected $adminUser;
  
  /**
   * An authenticated user with no administrative permissions
   *
   * @var object
   */
  protected $normalUser;
  
  /**
   * The ID of our Block
   *
   * @var object
   */
  protected $block_id;
  
  /**
   * The title of our Block
   *
   * @var object
   */
  protected $title;
  
  /**
   * The default, installed theme on our site
   *
   * @var object
   */
  protected $default_theme;
  
  /**
   * Modules to enable.
   *
   * @var array
   */
  public static $modules = array('first');

Мы будем работать с этими свойствами в оставшейся части этой главы, и я объясню, что они делают и как мы их используем по ходу. Есть одно важное свойство, которое крайне важно для правильной работы нашего теста: $ modules . Поскольку мы тестируем наш модуль с именем f irst , это единственный модуль, который нам нужен для выполнения тестов (на самом деле нам также нужно включить модуль Block, но это по умолчанию при установке Drupal). Для этого мы определяем зависимость в открытом свойстве с именем $ modules , используя машиночитаемое имя нашего модуля. 

Зачастую тестовый пример требует некоторой настройки и конфигурации, где общие значения инициализируются и становятся доступными подсистемы. К счастью, Drupal обрабатывает большинство. Уровень базы данных, модульная система и начальная конфигурация выполняются до того, как будут выполнены наши тесты. Тем не менее, тесты часто должны обрабатывать некоторую инициализацию самостоятельно. В случаях, когда вам необходимо это сделать, существует метод, который будет вызываться перед выполнением тестов. Это метод setUp () .

В методе setUp () мы предоставляем некоторую конфигурацию, чтобы наши тесты работали правильно. Помните, что мы пишем наши тесты для следующих сценариев:

  • Убедитесь, что наш блок отображается
  • Убедитесь, что наша проверка формы работает
  • Убедитесь, что наш контроль доступа работает


Чтобы проверить эти сценарии, нам нужно будет подготовиться. Во-первых, нам нужно создать пользователя с правами администратора, чтобы мы могли протестировать создание блоков. Затем нам нужно будет создать другого пользователя без разрешения «администрировать блоки», чтобы протестировать нашу функцию контроля доступа (то есть, чтобы проверить, что пользователю без этого разрешения не разрешено просматривать блок). Нам также понадобятся некоторые свойства, которые будут использоваться во всех наших тестах. Метод setUp () - это место для всего этого.

 public function setUp() {
    parent::setUp();

    // Create an administrative user with access to
    // 'administer blocks' permission
    $this->adminUser = $this->drupalCreateUser(array(
      'administer blocks'      
    ));

    // Create a normal user with no such permissions
    $this->normalUser = $this->drupalCreateUser();

    // Specify the Block ID
    $this->block_id = 'first_block';

    // Create a random title for the block
    $this->title = $this->randomName(8);

    // Retrieve the default system theme
    $this->default_theme = \Drupal::config('system.theme')->get('default');    
  }

BrowserTestBase :: setUp () выполняет некоторые необходимые операции установки - то, что необходимо сделать, прежде чем наши тесты будут успешно выполнены. По этой причине нам нужно убедиться, что в любой реализации метода setUp () мы явно вызываем parent :: setUp () .
Как мы можем видеть выше, мы переопределили setUp () метод BrowserTestBase и добавили несколько свойств нашего тестового класса. 

Прежде всего, мы объявили $ this-> adminUser и использовали метод drupalCreateUser () , передавая имя разрешений, которые должны быть у нашего администратора ( «администрирование блоков» и  «администрирование разрешений» ). Мы будем использовать этого пользователя для создания наших блоков в тестах.

Затем, поскольку мы также хотим протестировать функцию блокировки доступа к блоку, мы также создали $ this-> normalUser без каких-либо назначенных разрешений.

Остальные свойства будут использованы в методах тестирования позже в этом разделе. Я объясню все из них.

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

Написание тестового метода


Большинство методов в тестовом примере являются тестовыми методами; то есть они запускают операции с целью проверки их работы. Но, как вы заметите, нигде в нашем коде мы явно не вызываем эти тестовые методы.

Итак, как же система тестирования Drupal знает, как вызывать наши методы? Как и в случае с хуками Drupal, ответ заключается в соглашении об именах. Предполагается, что любой метод, начинающийся со слова test, является контрольным примером и автоматически запускается средой тестирования.

Давайте напишем наш первый тестовый метод, показанный в контексте всего класса.

<?php

/**
 * @file
 * Contains Drupal\Tests\first\Functional\FirstBlockTest
 */

namespace Drupal\Tests\first\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Provides test cases for our first custom Block
 *
 * @group first
 */
class FirstBlockTest extends BrowserTestBase {
  
  /**
   * A user with permission to create and edit books and to administer blocks.
   *
   * @var object
   */
  protected $adminUser;
  
  /**
   * An authenticated user to test block caching.
   *
   * @var object
   */
  protected $normalUser;
  
  /**
   * The ID of our Block
   *
   * @var object
   */
  protected $block_id;
  
  /**
   * The title of our Block
   *
   * @var object
   */
  protected $title;
  
  /**
   * The default, installed theme on our site
   *
   * @var object
   */
  protected $default_theme;
  
  /**
   * Modules to enable.
   *
   * @var array
   */
  public static $modules = array('first');
     
  public function setUp() {
    parent::setUp();

    // Create an administrative user with access to
    // 'administer blocks' permission
    $this->adminUser = $this->drupalCreateUser(array(
      'administer blocks',
	  'administer permissions'
    ));

    // Create a normal user with no such permissions
    $this->normalUser = $this->drupalCreateUser();

    // Specify the Block ID
    $this->block_id = 'first_block';

    // Create a random title for the block
    $this->title = t('Our first block');

    // Retrieve the default system theme
    $this->default_theme = \Drupal::config('system.theme')->get('default');    
  }
    
  /**
   * Tests the Block creation
   */
  public function testFirstBlockCreation() {
    
    // Log in as admin user
    $this->drupalLogin($this->adminUser);
            
    // Create an array that contains our Block configuration
    $configuration = array(     
      'region' => 'sidebar_first',
      'settings[label]' => $this->title,
      'settings[number_of_items]' => 10
    );
    
    // Submit the Block creation form programmatically   
    $this->drupalPostForm('admin/structure/block/add/' . $this->block_id . '/' . $this->default_theme, $configuration, t('Save block'));

    // Test if block creation was successful
    $this->assertText(t('The block configuration has been saved.'));    
  }  
}

Как вы можете видеть выше, мы добавили тестовый метод с именем testFirstBlockCreation () . В общем, цель этого метода - убедиться, что мы можем создать созданный нами блок типа Plugin без ошибок. Давайте внимательнее посмотрим на этот метод, чтобы увидеть, как мы этого добиваемся.

Прежде всего, нам нужен пользователь, у которого достаточно прав для создания блока в Drupal. По этой причине нам пришлось создать $ adminUser в методе setUp () . Мы используем встроенный метод drupalLogin () и передаем учетную запись администратора, которую мы создали ранее. Теперь наш пользователь-администратор вошел в тестовую среду.

Затем мы создаем массив, содержащий параметры конфигурации, которые нам понадобятся при создании блока. Мы определяем его регион (где мы хотим, чтобы он появлялся на сайте), его заголовок («Наш первый блок») и настройку number_of_items (настройку, которую мы добавили в форму конфигурации блока ранее).

 // Create an array that contains our Block configuration
    $configuration = array(      
      'region' => 'sidebar_first',
      'settings[label]' => $this->title,
      'settings[number_of_items]' => 10
    );

Далее мы просим среду тестирования отправить нам форму создания блока, вызвав метод  drupalPostForm () .

$this->drupalPostForm('admin/structure/block/add/' . $this->block_id . '/' . $this->default_theme, $configuration, t('Save block'));

Метод drupalPostForm () используется для проверки отправки форм во время теста и может принимать несколько аргументов. Первый аргумент определяет путь на нашем сайте, где PHPUnit может найти форму, которую мы хотим отправить. Как мы выясним, какой путь использовать здесь?

Когда мы добавляем новый блок на сайт в интерфейсе пользователя, мы видим, что URL ссылки, по которой мы щелкаем, выглядит примерно так:

admin/structure/block/add/{block_id}/{name_of_theme}

Это шаблон URL, который модуль Block определяет и использует для добавления новых блоков, поэтому мы передаем его в качестве аргумента $ path в drupalPostForm () .

Второй аргумент, $ configuration , содержит наш массив конфигурации Block. Он соответствует структуре массива формы создания блока, упомянутой выше, и это способ имитировать ввод данных в формы, представленные drupalPostForm () .

Третий аргумент представляет значение (или метку) кнопки отправки, нажатие которой необходимо эмулировать. Причина, по которой нам нужно это указать, заключается в том, что могут быть случаи, когда форма имеет несколько кнопок отправки (например, «Отправить», «Удалить» и т. д.). Другими словами, мы даем указание структуре тестирования нажать эту кнопку для нас.

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

Мы делаем это, делая заявление - утверждение о том, что мы ожидаем. Затем среда тестирования проверяет это. Если код работает должным образом, тест проходит. Если нет, тест не пройден.

Вот наше утверждение:

$this->assertText(t('The block configuration has been saved.'));

Каждое утверждение обычно имеет форму $ this-> assertSOMETHING ($ условие, $ сообщение) , где SOMETHING - это тип утверждения, $ условия - это условия, которые должны быть выполнены для прохождения теста, а $ message - это сообщение, описывающее результат испытаний.

Полный список доступных методов утверждений в Drupal 8 приведен по  адресу https://www.drupal.org/docs/7/testing/assertions .

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

На данный момент мы определили один тестовый пример FirstBlockTest и готовы запустить тест. Давайте перейдем к конфигурации | Разработка | Тестирование . Если ваш контрольный пример реализован правильно и ваши кеши очищены, группа с именем first должна теперь отображаться на этой странице в списке групп.

test group

Откройте этот свернутый набор полей, и вы должны увидеть наш тестовый класс:

our test

 Выберите класс теста, а затем нажмите кнопку « Запустить тесты» , чтобы выполнить наш тестовый пример. Тесты часто занимают много времени для запуска. За кулисами фреймворк фактически создает специальную установку Drupal, которая будет использоваться только для этого раунда тестов. Но через некоторое время тестовая среда должна напечатать отчет, который выглядит примерно так:

test outpute

Как видите, наш тест не прошел успешно. Мы ожидали вышеупомянутый результат? Точно нет. На самом деле, мы настроили наш тестовый пример именно так, как должно быть. Так что пошло не так? Давайте рассмотрим это сообщение об ошибке.

Мы видим, что тестовая среда не смогла найти строку  . Конфигурация блока была сохранена. при выполнении теста. Строка «Конфигурация блока сохранена». не найден нигде в ответе HTML текущей страницы.  После этого он отображает, какие методы и в каких классах приводят к этой ошибке. Это действительно полезная функция инфраструктуры тестирования, поскольку она значительно облегчает отслеживание ошибок.

Но что еще более полезно, мы получили список снимков, которые фреймворк создал во время выполнения теста. Внизу внизу вы увидите несколько ссылок - это ссылки на статические HTML-страницы, которые фреймворк создал во время тестирования, и они содержат точные шаги, которые он предпринял во время работы. Если вы нажмете на последнюю ссылку, вы увидите фактическую ошибку, с которой столкнулся Drupal во время теста:

Drupal \ Core \ Config \ Schema \ SchemaIncompleteException: ошибки схемы для block.block.ourfirstblock со следующими ошибками: block.block.ourfirstblock: settings.number_of_items отсутствует схема в Drupal \ Core \ Config \ Development \ ConfigSchemaChecker-> onConfigSave () строка 95 файла core / lib / Drupal / Core / Config / Development / ConfigSchemaChecker.php).

Boom! Drupal выдал исключение при попытке создать наш блок в тестовой среде. Основываясь на сообщении об ошибке, он не может найти схему конфигурации для нашего блока. В частности, он не распознает number_of_items как допустимый параметр конфигурации, основываясь на схеме блока по умолчанию, предоставленной Drupal.

А Drupal на самом деле прав, мы никогда не указывали Системе управления конфигурациями, какие данные мы хотим сохранить в нашем свойстве пользовательских настроек. Это означает, что нам нужно сделать это сейчас.

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


Мы собираемся определить схему для нашего пользовательского параметра конфигурации, чтобы система управления конфигурацией знала, как обрабатывать (и проверять) наше свойство конфигурации. Для этого мы добавим две новые папки и простой файл YAML в наш модуль:

first shema

Как вы можете видеть выше, мы добавили config / schema / first.schema.yml  в папку нашего модуля. Этот файл будет содержать схему для нашего пользовательского свойства конфигурации, number_of_items . Давайте добавим следующий код YAML в этот файл:

# Block schema
block.settings.first_block:
  type: block_settings
  mapping:
    number_of_items:
      type: integer
      label: Number of items     

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

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

success

Теперь тест успешно завершен, и мы подтвердили, что у Drupal не возникнет проблем при создании экземпляра нашего пользовательского типа Block.

Теперь мы собираемся добавить еще несколько методов тестирования.

Проверка формы


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

Давайте добавим метод с именем testFirstBlockValidation () в наш класс.

/**
   * Tests the Block form validation
   */
  public function testFirstBlockValidation() {
    
    // Log in as admin user
    $this->drupalLogin($this->adminUser);
    
    // Create an array that contains our Block configuration
    $configuration = array(      
      'region' => 'sidebar_first',
      'settings[label]' => $this->title,
      'settings[number_of_items]' => t('Hey, this is not a number!')
    );
	
    // Place the Block
    $this->drupalPostForm('admin/structure/block/add/' . $this->block_id . '/' . $this->default_theme, $configuration, t('Save block'));
    // Test if our custom validation callback works as expected
    $this->assertText(t('Please enter a numeric value.'));    
  }

Этот метод выглядит довольно похоже на наш первый метод испытаний. На самом деле, это совершенно то же самое, за исключением двух вещей: 

Вместо числового значения мы установили number_of_items в текстовую строку в массиве $ configuration . 
Мы изменили параметры $ this-> assertText (), чтобы найти сообщение об ошибке, отображаемое нашим методом blockValidate () .
Этот код должен вызвать сбой проверки и сообщение об ошибке. Введите числовое значение. должен появиться на сайте во время тестового прогона. Давайте снова запустим тест и посмотрим, будет ли он успешным:

success

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

Тестирование контроля доступа


Последний тест, который мы собираемся сделать, состоит в том, чтобы убедиться, что к нашему Блоку могут обращаться только пользователи с соответствующими правами. Ранее в этой главе мы реализовали метод с именем blockAccess () в нашем классе FirstBlock, чтобы контролировать видимость нашего блока на основе разрешения («администрировать блоки»). Теперь мы собираемся написать тест, чтобы убедиться, что этот метод делает то, что должен.

Этот тест будет немного отличаться от других тестов, которые мы написали до сих пор. Как обычно, мы войдем в систему с созданным нами администратором и создадим новый блок. Когда блок будет создан, мы выйдем из системы и войдем в систему с учетной записью обычного пользователя, которую мы также создали в методе setUp () нашего тестового класса. 

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

Для достижения этого мы будем использовать метод assertNoText () , который можно рассматривать как отрицательный аналог assertText () ; он используется для того, чтобы убедиться, что искомая строка не найдена на сайте во время теста.

Давайте добавим новый метод с именем testFirstBlockAccess () в наш тестовый класс.

/**
   * Tests the Block access
   */
  public function testFirstBlockAccess() {

    // Log in as admin user
    $this->drupalLogin($this->adminUser);
            
    // Create an array that contains our Block configuration
    $configuration = array(      
      'region' => 'sidebar_first',
      'settings[label]' => $this->title,
      'settings[number_of_items]' => 10
    );

    // Place the Block
    $this->drupalPostForm('admin/structure/block/add/' . $this->block_id . '/' . $this->default_theme, $configuration, t('Save block'));
  	
    // Log out admin user
    $this->drupalLogout();
	
    // Log our normal user in
    $this->drupalLogin($this->normalUser);

    // Our Block should not be visible 
    $this->assertNoText('Our first block');
  }  

Вот что здесь происходит:

Войти с правами администратора
Создайте блок как обычно
Выйти
Войти под обычным пользователем
Убедитесь, что блок не виден (мы искали заголовок блока)
Запуск тестов должен привести к следующему выводу:

success

Резюме


Мы завершили легкую прогулку по созданию модуля. Мы начали с создания каталога модулей, за которым следовал файл .info.yml . Затем мы добавили файл .module и реализовали хук. Мы также создали собственный блочный плагин. Наконец, мы написали наш первый тест для этого модуля, изучая среду тестирования PHPUnit в Drupal.

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

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

assistant Теги

keyboard_arrow_up