Обработка форм в PHP. Как это делать правильно в 2020 году
08-06-2020Время чтения ~ 7 мин.PHP 15256
Это достаточно «классическая» задача в 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-файла), поэтому часто её также выносят в отдельный файл.
Да уж, хорошо продуманная структура, действительно упрощает жизнь))
А разве верно, что обработчик занимается логикой, что дальше выводить? Не лучше делать редирект назад откуда пришел запрос?
Редирект не решает задачи — нужен именно обработчик входящих данных.