Form API в Drupal: рабочие приёмы и типичные ошибки
В Form API можно быстро собрать форму, но проблемы начинаются на валидации, AJAX и сохранении данных. Разбираем практику для Drupal 10.3+ и 11.x: структуру класса, FormState, ошибки и выбор подхода.

В Drupal 10.3+ и 11.x форма из нескольких полей обычно помещается в один класс, но именно здесь чаще всего появляются ошибки: значение читают не из FormState, валидацию смешивают с сохранением, а AJAX-обновление сбрасывает введённые данные. Разберём рабочий подход для кастомной формы, сравним его с формами сущностей и конфигурации и покажем, что проверять перед публикацией.
Сначала выберите правильный тип формы
Form API — это не один способ описать любую страницу. В Drupal выбор базового класса определяет, где будут храниться данные и какие готовые механизмы вы получите.
| Подход | Когда использовать | Что получаете | Цена решения |
|---|---|---|---|
FormBase | Действие, фильтр, обратная связь, служебная форма | Полный контроль над полями, валидацией и submit | Сохранение и права доступа нужно продумать самостоятельно |
EntityForm | Создание или редактирование node, user, media или своей сущности | Интеграция с entity API, полями, переводами и ревизиями | Нельзя бездумно обходить стандартный lifecycle сущности |
ConfigFormBase | Настройки модуля, которые должны попасть в конфигурацию | Работа с конфигурацией и стандартная страница настроек | Нельзя использовать её как произвольное хранилище пользовательских данных |
Если форма создаёт материал или редактирует поле сущности, начинайте с entity form. Если пользователь отправляет запрос, запускает действие или выбирает фильтр, обычно подходит FormBase. Настройки модуля не смешивайте с контентом: для них используйте ConfigFormBase.
Главное правило Form API: форма отвечает за ввод и проверку данных, а не должна превращаться в место, где спрятана вся бизнес-логика приложения.
Form state1 — объект FormStateInterface, в котором Drupal хранит значения полей, ошибки, информацию о rebuild и данные текущего цикла обработки формы.
Базовая форма: значения отдельно, обработка отдельно
Ниже — минимальный класс для Drupal 10.3+ и 11.x. Предполагается, что он находится в модуле site_feedback по пути src/Form/FeedbackForm.php. Имена пространства и модуля должны совпадать с вашей структурой.
<?php
namespace Drupal\site_feedback\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
final class FeedbackForm extends FormBase {
public function getFormId(): string {
return 'site_feedback_form';
}
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Имя'),
'#required' => TRUE,
'#maxlength' => 120,
];
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Email'),
'#required' => TRUE,
];
$form['message'] = [
'#type' => 'textarea',
'#title' => $this->t('Сообщение'),
'#required' => TRUE,
'#rows' => 6,
];
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Отправить'),
];
return $form;
}
public function validateForm(array &$form, FormStateInterface $form_state): void {
$message = trim((string) $form_state->getValue('message'));
if (mb_strlen($message) < 20) {
$form_state->setErrorByName(
'message',
$this->t('Сообщение должно содержать не меньше 20 символов.')
);
}
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
// Здесь вызывайте сервис, который отправляет или сохраняет сообщение.
$this->messenger()->addStatus($this->t('Сообщение принято.'));
}
}
В этом примере #required проверяет обязательность поля на уровне Form API, а собственная проверка длины находится в validateForm(). В submitForm() не нужно повторно извлекать значение из массива формы: источник данных — FormStateInterface.
Как подключить форму к маршруту
Сам класс не создаёт страницу. Для вывода формы нужен маршрут модуля. В файле site_feedback.routing.yml укажите класс формы:
site_feedback.feedback:
path: '/feedback'
defaults:
_form: '\\Drupal\\site_feedback\\Form\\FeedbackForm'
_title: 'Обратная связь'
requirements:
_permission: 'access content'
После очистки кэша маршрутов по адресу /feedback должна открыться форма. В реальном модуле разрешение access content может быть слишком широким: для административного действия создайте отдельное permission и назначьте его нужной роли.
Что делать в buildForm, validateForm и submitForm
Разделение по методам — не формальность. Оно помогает не отправить невалидные данные и не запускать побочные эффекты при повторном показе формы.
buildForm()описывает элементы, подписи, значения по умолчанию, доступность и AJAX-настройки.validateForm()проверяет взаимосвязь полей и ограничения, которые нельзя выразить только через свойства элемента.submitForm()выполняет действие после успешной валидации: вызывает сервис, сохраняет сущность или пишет конфигурацию.- Сервисы содержат отправку почты, запросы к внешним системам и сложную бизнес-логику. Не прячьте всё это в форме.
Ошибку привязывайте к конкретному полю через setErrorByName(). Тогда Drupal покажет сообщение рядом с элементом, а пользователь поймёт, что исправлять. Для общей ошибки формы можно передать пустое имя поля, если проблема относится не к одному значению.
Два подхода к данным: плоские и вложенные значения
Простая форма возвращает значения по именам элементов: name, email, message. Когда поля объединены в повторяемую или логическую группу, используйте #tree. Тогда вложенная структура сохраняется и в FormState.
$form['contact'] = [
'#type' => 'container',
'#tree' => TRUE,
];
$form['contact']['email'] = [
'#type' => 'email',
'#title' => $this->t('Email'),
'#required' => TRUE,
];
$form['contact']['phone'] = [
'#type' => 'tel',
'#title' => $this->t('Телефон'),
];
При такой структуре читайте данные как $form_state->getValue(['contact', 'email']). Не добавляйте #tree автоматически: для обычных полей он усложняет обработчики без пользы.
| Структура | Как читать значение | Когда выбирать |
|---|---|---|
| Плоская | getValue('email') | Небольшая форма без групп и повторителей |
| Вложенная | getValue(['contact', 'email']) | Группа полей должна уйти в сервис одним объектом или массивом |
| Сущность | Через API сущности и её поля | Форма меняет сохранённый объект Drupal |
AJAX: обновляйте только нужную часть формы
Частая ошибка — возвращать из AJAX весь массив формы или перестраивать страницу после каждого символа. Для Drupal это дороже, а для пользователя выглядит как скачущий интерфейс. Оберните меняемый участок в контейнер с идентификатором и возвращайте именно его.
$form['delivery'] = [
'#type' => 'select',
'#title' => $this->t('Способ доставки'),
'#options' => [
'pickup' => $this->t('Самовывоз'),
'courier' => $this->t('Курьер'),
],
'#ajax' => [
'callback' => '::deliveryChanged',
'wrapper' => 'delivery-details',
],
];
$form['delivery_details'] = [
'#type' => 'container',
'#attributes' => ['id' => 'delivery-details'],
];
В callback нужно вернуть элемент, который соответствует wrapper. Если выбор влияет на состав полей, значения перед перестроением должны сохраняться в FormState, а логика создания полей — оставаться детерминированной. Инвалидация кеша и тонкости AJAX-форм — отдельная тема; здесь важен только границы обновляемого участка.
Что ломается чаще всего
Значение читают из неправильного места
$form['email']['#value'] не является надёжным способом получить отправленное пользователем значение. В обработчиках используйте $form_state->getValue('email'), а при вложенной структуре — массив имён.
Проверка стоит после действия
Если запись в базу или вызов внешнего API выполняется в validateForm(), действие может произойти даже при последующей ошибке другого поля. Валидация не должна иметь побочных эффектов. Сохраняйте данные и отправляйте запросы только после успешной проверки.
Форма не появляется после изменения PHP-класса
Drupal кеширует маршруты и определения. Если после добавления класса или маршрута браузер показывает старую страницу, очистите кэш Drupal и проверьте журнал. На рабочем сайте делайте это штатным способом окружения, а не удалением случайных каталогов из файловой системы.
Callback AJAX возвращает не тот элемент
Сообщение вроде «The specified #ajax callback is invalid» обычно означает, что Drupal не нашёл метод или callback указан с ошибкой. Проверьте имя метода, его доступность и соответствие класса форме. Если обновляется не тот участок, сравните возвращаемый элемент с идентификатором в wrapper.
Сложная логика оказалась в форме
Форма начинает плохо тестироваться, если сама отправляет письма, вычисляет тарифы и пишет в несколько таблиц. Вынесите эти операции в сервис и передайте ему уже проверенные значения. Так обработчик остаётся коротким, а сервис можно вызвать не только из формы.
Как выбрать подход на практике
Используйте следующий критерий:
- Если результатом должна стать сущность Drupal, возьмите entity form и не дублируйте сохранение полей вручную.
- Если форма меняет настройки модуля, используйте
ConfigFormBaseи конфигурационный API. - Если форма запускает действие или собирает ввод для сервиса, создайте класс на основе
FormBase. - Если нужны динамические поля, сначала сделайте обычную серверную отправку, затем добавляйте AJAX только вокруг участка, который действительно меняется.
Перед выпуском проверьте форму от имени роли с минимальными правами, отправьте пустые значения, слишком длинный текст и неожиданный формат данных. Отдельно проверьте повторную отправку страницы: действие не должно выполняться повторно только из-за обновления браузера.
Что делать дальше
После базовой формы подключите сервис через контейнер зависимостей, добавьте отдельное разрешение и логирование ошибок внешнего действия. Для формы сущности проверьте права доступа, переводы и ревизии, а для AJAX — работу без JavaScript как запасной сценарий, если это допустимо интерфейсом.
Следующий полезный замер — не время построения формы, а число SQL-запросов и время обработчика после отправки. Если задержка появляется в submitForm(), профилируйте сервис, а не Form API. Официальная документация по render arrays, формам и FormState поможет сверить поведение конкретной версии Drupal.
1 Form state — состояние текущей отправки формы: значения элементов, ошибки и флаги перестроения. Это не постоянное хранилище и не замена базе данных.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Drupal.org: Form API overview, Drupal API: FormStateInterface
Частые вопросы
Как вывести форму Drupal на странице?
Почему значение поля нужно брать из FormState?
Нужно ли писать собственную проверку для поля с #required?
Что делать, если AJAX callback не срабатывает?
Можно ли сохранять данные прямо в submitForm()?
Что будет, если пользователь отправит форму без JavaScript?
Безопасно ли доверять значениям, которые пришли из формы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.