Обработка форм в PHP. Как это делать правильно в 2020 году
08-06-2020Время чтения ~ 7 мин.PHP 16379
Это достаточно «классическая» задача в PHP — приём и отправка обычной формы. Давным-давно, ещё во времена PHP 4, в книгах приводился пример того как это делать. Это всегда был один php-файл, где размещался и обработчик формы, и html-код вывода формы, и вывод ошибок. Понятно, что на заре рождения PHP, говорить о каком-то разделении кода или даже о культуре программирования не приходится. Но, недавно я случайно наткнулся на книгу о PHP 7 2018 года выпуска, где рассказывается об основах языка, классах, есть даже глава о PostgreSQL и даже описано несколько ООП-шаблонов проектирования.
Я с удивлением обнаружил, что до сих пор приводится код из PHP 4, как будто бы последних 20 лет развития PHP-программирования и не было. Сами посмотрите: это я сохранил скриншот. То есть вместо того, чтобы учить студентов нормальным практикам, до сих пор предлагается код 20-летней давности.
Чтобы продолжить, давайте определимся что именно неверно в таком подходе.
Код в одном файле — это хорошо или плохо? Хорошо, что это один файл, то есть перенос его будет немного проще. С другой стороны, часто ли вообще требуется куда-то его перемещать? Вряд ли.
Дальше. В файле смесь HTML и PHP. Если вынести код обработчика отдельно, то это уже упростит дальнейшую поддержку. Кстати о поддержке кода.
Поддержка кода — это такая его организация, которая позволит без мата и полной переделки доработать его в любой момент — через месяц или год, любым другим php-программистом. Сюда включается форматирование, а также логическая организация файлов. Форма, конечно, очень простой пример, но он всё равно должен быть сделан по правилам.
Идём дальше. В обработчике формы вроде как есть код отлова ошибок, и если их нет, то выводится какой-то результат. Обратите внимание на exit(), которая принудительно завершает выполнение скрипта. Это значит, что завершающая часть HTML-код не будет выведена — html-код окажется некорректным.
То есть на лицо ещё одна проблема — неверная логика работы скрипта. Даже в моих первых книгах вводились более сложные if/else условия, чтобы корректно завершить вывод html-кода...
Всё, хватит лирики, покажу как это нужно делать правильно в 2020 году. :-)
Базовое правило — логика должна быть разделена на файлы. При отправке формы есть два базовых состояния: вывод самой формы и приём post-данных от формы. HTML-вывод, в свою очередь должен делиться на:
- вывод самой формы
- вывод положительного результата
- и вывод ошибок
Поскольку нам нужен корректный HTML, то нужно вынести ещё и начальную часть html (секция HEAD) и конечную (BODY, HTML).
Основной (запускающийся) php-файл обычно называется фронт-контроллер (front controller). Да, этот термин больше из ООП, но в подавляющем большинстве php-модулей (и приложений), всегда есть точка входа, которая дальше уже подсоединяет все остальные файлы. Это функция контролёра, которая содержит основную логику модуля/приложения.
В нашем случае пусть это будет index.php:
<?php
// Controller
define('BASE_DIR', __DIR__ . DIRECTORY_SEPARATOR);
if ($_POST) {
require 'action/post.php';
} else {
require 'action/show.php';
}
# end of file
Здесь мы определяем константу BASE_DIR, которая нужна для формирования полных путей к подключаемым файлам, которые будут размещаться в разных подкаталогах.
Алгоритм простой: если есть POST-данные, то подсоединяем файл action/post.php, где будем обрабатывать входящие данные и action/show.php, который служит для простого вывода формы.
Рассмотрим action/show.php:
<?php // action «show» require BASE_DIR . 'layout/start.php'; require BASE_DIR . 'layout/form.php'; require BASE_DIR . 'layout/end.php'; # end of file
Форма собирается из нескольких файлов. Каталог layout хранит именно html-вывод. То есть, когда станет задача доработать дизайн формы, достаточно будет поправить какой-то один простой layout-файл.
Файл layout/start.php содержит начальную часть html-страницы:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Form demo</title> <link href="assets/css/berry-normalize.min.css" rel="stylesheet"> </head> <body>
Здесь я подключил css-файл от Berry CSS, который размещается в каталоге assets. Это «типовой» каталог для всех «оформительских» файлов: css/sass, js, fonts и images. Также обратите внимание, что путь задан относительный. Если нужно указывать полный url, то в контролёре следует определить ещё одну константу, например BASE_URL, которую и использовать для формирования начальной части URL.
Файл end.php просто корректно закрывает HTML:
</body> </html>
Файл form.php содержит форму:
<div class="mar30-tb b-center w500px pad30-rl pad10-tb bordered rounded">
<h1>Form demo</h1>
<form method="POST">
<div class="mar10-b">Email</div>
<input class="w100" type="email" name="email" value="">
<div class="mar10-b mar20-t">Password</div>
<input class="w100" type="password" name="password" value="">
<div class="mar10-b mar20-t">Info</div>
<input class="w100" type="text" name="info" value="">
<div><button class="mar20-tb" type="submit">Submit</button></div>
</form>
</div>
Это очень простой вариант с тремя полями: email, password и info. Обратите внимание, что у формы не указан параметр action, это означает, что post-данные формы будут отправлены по текущему URL-адресу.
В Сети полно примеров, когда action указывает на другой php-файл. Такая практика также изредка используется. В отличие от моего примера, такой модуль будет иметь два контролёра, а не один: в нашем случае мы определяем действие по переменой $_POST, а во втором случае — форма сама отправит запрос на «post-файл».Рассмотрим цепочку выполнения данного участка кода.
- Запускается
index.php. - В нём смотрится наличие post-данных.
- Если нет, то подключаем
action/show.php. - В нём подключаются layout-файлы для вывода формы.
Если форма была отправлена, то происходит подключение action/post.php — это и есть обработчик формы.
<?php
// action «post»
$data['email'] = $_POST['email'] ?? '';
$data['password'] = $_POST['password'] ?? '';
$data['info'] = $_POST['info'] ?? '';
if (!$data['info']) $data['info'] = 'No info';
$error = [];
if (!$data['email']) $error[] = 'Invalid email';
if (!$data['password']) $error[] = 'Invalid password';
require BASE_DIR . 'layout/start.php';
if (!$error) {
require BASE_DIR . 'layout/result.php';
} else {
require BASE_DIR . 'layout/error.php';
}
require BASE_DIR . 'layout/end.php';
# end of file
Все данные мы формируем в массив $data (о нём чуть ниже). Первая проверка — просто на наличие необходимых данных от формы. То есть ниже по коду мы будем точно знать, что есть $data['password'], даже если поле не было заполнено пользователем.
После этого идёт валидация данных. Я её cделал примитивной (пример-то учебный), но смысл её в том, что формируется второй массив $error, который содержит список всех ошибок.
Дальше нужно вывести результат. Поскольку начальный и конечный файлы HTML одинаковы, то мы их выносим за пределы if-условия. А дальше просто: если есть ошибки, то подключаем layout/error.php, если нет, то layout/result.php.
Здесь важно то, что есть логика выполнения, но нет непосредственного html-вывода: мы разделяем html-вёрстку от php-кода.
Файл result.php выводит список отправленных данных.
<div class="mar30-tb b-center w500px pad30-rl pad10-tb bordered rounded">
<h1>Form result</h1>
<ul><li><?= implode('<li>', $data) ?></ul>
</div>
А файл error.php выводит список ошибок.
<div class="mar30-tb b-center w500px pad30-rl pad10-tb bordered rounded">
<h1>Form error</h1>
<ul><li><?= implode('<li>', $error) ?></ul>
</div>
Обратите внимание, что здесь мы уже выводим $error.
Вы можете скачать готовый пример.
Такой подход к построению php-модуля (или приложения) наиболее правильный. Если есть возможность, то разделяйте php и html-код по разным файлам. Это особенно актуально для проектов с более сложной логикой.
Пара слов о $data
Часто стоит задача передать в layout-файл какие-то данные. Например название формы, или уже введенные данные, чтобы пользователь их заново не вводил, а только поправил. Лучший способ это сделать — именно через массив. Причём такой, где заранее оговариваются обязательные ключи. Это позволяет избежать дополнительных isset-проверок при их выводе.
// action «post» $data['title'] = 'Заголовок формы'; // layout <h1><?= $data['title'] ?></h1>
Также распространён и другой приём — extract($data), для того, чтобы получить готовые переменные:
// layout <?php extract($data) ?> <h1><?= $title ?></h1>
Это достаточно простой шаблонизатор PHP.
Аля-MVC
Нетрудно заменить, что по своей сути код соответствует концепции MVC.
Контролёр выполняет основную логику модуля. В нашем случае он совпал с фронт-контроллером, но обычно фронт-контролёр — это уровень приложения, а просто контролёр — это уровень модуля.
Дальше контролёр дёргает определенную модель — в нашем случае это action-файлы. В модели происходит работа с данными и дальше модель передаёт управление в представление (view) — у нас это layout-файлы.
То есть у нас управление передаётся по цепочке: controller → model → view. Есть ещё один вариант, когда контролёр передаёт данные в модель, модель возвращает результат обратно, а потом контролёр их передаёт уже в представление. То есть вариант MVC будет определяться уже задачей или архитектурой приложения.
Валидация данных
И в заключение небольшой момент о валидации данных. Очень часто валидация — чуть ли не половина кода модели (action-файла), поэтому часто её также выносят в отдельный файл.
Да уж, хорошо продуманная структура, действительно упрощает жизнь))
А разве верно, что обработчик занимается логикой, что дальше выводить? Не лучше делать редирект назад откуда пришел запрос?
Редирект не решает задачи — нужен именно обработчик входящих данных.