Обработка форм в PHP. Как это делать правильно в 2020 году

Это достаточно «классическая» задача в 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-файла), поэтому часто её также выносят в отдельный файл.

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)