Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Самостоятельный проект: форма обращения от GET до базы
  2. От одной проверки к работающей форме: что запускать и какие файлы читать
  3. Запуск и устройство проекта
  4. Три разные проверки: запрос, CSRF и ключ операции
  5. Проверка полей и безопасный повторный показ
  6. Сохранение и повтор операции
  7. Ошибки, PRG и доступность формы
  8. Упражнение: добавить номер заказа
  9. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 64 · Проект

Самостоятельный проект: форма обращения от GET до базы

Рабочая форма с валидацией, CSRF, SQLite, защитой от дублирования и понятными сообщениями.

От одной проверки к работающей форме: что запускать и какие файлы читать

Задача проекта — принять обращение посетителя и сохранить его один раз. Раньше небольшая страница формы только показывала результат проверки; здесь к ней добавлены постоянное хранение и защита от повторной отправки. Начните с одной части: запустите настоящую проверку полей без браузера и базы. Так результат проверки можно увидеть до чтения большого HTTP-обработчика.

Нужная база: массив с именованными полями, вход и результат функции, подключение соседнего PHP-файла. Запись PracticalForm\Validation — полное имя готового класса; пространство имён отличает его от чужих Validation. Писать новый класс в этом опыте не требуется.

Сначала проверка, затем выбор одной ветки
  1. Получить массив name, email, message
  2. Validation::contact возвращает values и errors
  3. Выбрать одну ветку: есть ошибки → форма; ошибок нет → хранилище
ЗаписьЧто выполняется
require __DIR__ . '/Validation.php';Подключает приложенный файл. Определение класса становится доступно, но проверка ещё не вызвана.
PracticalForm\Validation::contact($submitted)Вызывает готовый статический метод contact с одним массивом. :: обращается к классу без new; сам метод читает переданные данные, а не $_POST.
$checked['values']Массив нормализованных строк: например, внешние пробелы удалены через trim.
$checked['errors']Массив ошибок по именам полей. Пустой массив означает, что проверки этой функции прошли.
Увидеть, какие значения пойдут дальше после проверкиСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/Validation.php';

$submitted = [
    'name' => '  Анна  ',
    'email' => '  reader@example.test  ',
    'message' => '  Нужна помощь с заказом.  ',
];
$checked = PracticalForm\Validation::contact($submitted);

echo 'Ошибок: ', count($checked['errors']), "\n";
echo 'Имя: ', $checked['values']['name'], "\n";
echo 'Email: ', $checked['values']['email'], "\n";
echo 'Сообщение: ', $checked['values']['message'], "\n";
Ожидаемый результат
Ошибок: 0
Имя: Анна
Email: reader@example.test
Сообщение: Нужна помощь с заказом.

Распакуйте example.php и Validation.php рядом, проверьте наличие mbstring командой php -m и выполните php example.php. Это тот же Validation.php, который лежит в src полного проекта. Здесь явно задан учебный снимок полей; сессия, HTTP и база не запускаются. Две пары квадратных скобок выбирают сначала карту values, затем поле внутри неё.

Измените одно поле. Поставьте message равным «Коротко». Число ошибок станет 1: сообщение короче десяти символов. Чтобы увидеть текст, выведите $checked['errors']['message']. Остальные проверенные значения останутся доступны для повторного показа. В настоящем обработчике наличие ошибок не должно вести к сохранению.

Теперь скачайте полный проект формы, распакуйте весь архив и запустите команду ниже из его корня: php -S 127.0.0.1:8770 -t public. Для проекта нужны mbstring и pdo_sqlite; для первого опыта выше достаточно mbstring. Откройте http://127.0.0.1:8770/. Остановка сервера — Ctrl+C. Папка public является доступной браузеру частью; src, templates и var остаются за её пределами.

Действие читателяЧто происходит в реальном проектеГде смотреть
Открыть адрес в браузереGET подготавливает сессию, пустые поля и токены; затем показывает HTML.public/index.php → templates/form.php
Отправить неверный emailPOST проверяет запрос и токены, затем поля. Ошибка даёт 422 и ту же форму с сообщением.public/index.php → src/Validation.php → templates/form.php
Отправить Анна / reader@example.test / Нужна помощь с заказом.После успешных проверок обращение сохраняется в SQLite. Ответ 303 направляет браузер на /?sent=1.src/ContactStore.php → public/index.php
Посмотреть результат и обновить страницуНовый GET показывает одноразовое сообщение из сессии. Обновление этого GET не повторяет POST; сообщение после прочтения исчезает.public/index.php → templates/form.php

Форма отправляет application/x-www-form-urlencoded и получает HTML либо перенаправление. Этот проект не принимает JSON-запрос вместо формы: такой формат получает 415. SQLite хранит запись в var/contacts.sqlite; JSON здесь не является ни HTTP-ответом, ни форматом файла базы. В ContactStore JSON используется внутри вычисления отпечатка нормализованных полей — это отдельная внутренняя задача.

Читайте проект по пути запроса, а не пытаясь запомнить все файлы сразу. Сначала найдите в шаблоне name="email", затем чтение $_POST и вызов Validation::contact в index.php, затем развилку errors и вызов save. Разделы ниже объяснят CSRF, ключ операции, транзакцию и повтор. Для одного только разбора проверки полей эти механизмы пока можно изучать по одному.

Запуск и устройство проекта

Наглядная модель

Как обращение проходит от формы до записи в SQLite

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

  1. GET: показать форму

    Сервер выдаёт поля, CSRF-токен и ключ операции. У этих двух значений разные задачи.

  2. POST: принять и проверить

    Проверяем запрос, CSRF, типы и правила полей. При ошибках возвращаем форму без сохранения.

  3. PDO: сохранить или узнать повтор

    SQLite хранит заявку с ключом операции. Тот же ключ не должен создавать вторую запись.

  4. 303: перейти к результату

    После успешного сохранения браузер получает адрес страницы результата и делает GET.

  5. GET: показать подтверждение

    Обновление этой страницы повторяет чтение, а не исходный POST с заявкой.

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

Соберём форму обращения для небольшого сайта. Посетитель вводит имя, email и сообщение, сервер проверяет данные и сохраняет обращение в SQLite. Это законченный учебный проект: форма работает после распаковки, а база создаётся вне публичного каталога. Он показывает несколько независимых механизмов в одном запросе и позволяет увидеть, в какой момент каждый нужен.

Используйте вымышленные данные. Пример не отправляет письма, не подключается к платёжным системам и не публикует список обращений. Его можно исследовать без внешних учётных записей. Политика полей намеренно конкретна: имя 2–80 символов, ASCII-email до 254 байт и сообщение 10–2000 символов. В своём продукте сначала согласуйте этот договор: технически корректный email не подтверждает владение ящиком, а международный адрес может требовать другой политики.

Один полный цикл формы
  1. GET: сессия и форма
  2. POST: граница HTTP
  3. CSRF и ключ операции
  4. Типы и правила полей
  5. Транзакция SQLite
  6. 303 → GET результата
Запуск распакованного проектаФрагмент или команда
php -m
php -S 127.0.0.1:8770 -t public

Команды выполняются из каталога проекта. Нужны PHP 8.2+, mbstring и pdo_sqlite. Откройте http://127.0.0.1:8770/. Сервер слушает только локальный интерфейс; для публичной эксплуатации встроенный сервер не предназначен.

ФайлОтветственность
public/index.phpHTTP-метод, лимиты, сессия, CSRF, выбор ответа и PRG.
src/Validation.phpПроверка входных значений; не читает $_POST и не отправляет заголовки.
src/ContactStore.phpПодготовленные SQL-запросы, транзакция, уникальный ключ операции.
templates/form.phpHTML5 и Bootstrap 5, подписи, сообщения ошибок и экранирование.
var/contacts.sqliteДанные вне document root; создаются локально и не входят в архив.

Три разные проверки: запрос, CSRF и ключ операции

Сначала сервер проверяет метод: чтение формы разрешено через GET, изменение — через POST, остальные методы получают 405 с Allow. Затем проверяется тип тела и размер. Content-Length недостаточно: он может отсутствовать или не совпадать с фактическим телом. Пример читает не более лимита плюс один байт. В рабочем окружении первый лимит должен стоять ещё на веб-сервере: PHP получает уже принятый запрос, а POST-парсер имеет собственные ограничения post_max_size и max_input_vars.

После запуска сессии создаётся случайный CSRF-токен. При POST он должен быть строкой ожидаемой длины и совпасть с токеном сессии через hash_equals. До сравнения нельзя вслепую приводить массив к строке. SameSite=Lax и HttpOnly полезны, но это не замена токену и не защита от XSS на вашем домене. Флаг Secure зависит от настоящего HTTPS. За обратным прокси нужен доверенный источник информации о TLS; принимать произвольный X-Forwarded-Proto от клиента нельзя.

Ключ операции решает другую задачу. Он связывает повтор запроса с одним сохранением. Сервер выдаёт его заранее и держит до десяти ключей с часовым сроком действия; старые вкладки могут получить понятную ошибку 409. Это выбранный для примера компромисс между несколькими вкладками и ограниченным объёмом сессии. В магазине срок и правила повторов нужно согласовывать с жизнью заказа и платёжной операции.

Проверка полей и безопасный повторный показ

Проверка вынесена в отдельный класс, чтобы её можно было запускать без браузера и сессии. Для каждого поля сначала проверяются тип и UTF-8, затем ограничивается сырой размер, потом выполняется trim и проверяется длина. Значения и ошибки возвращаются отдельно. Неправильное поле не превращается в успешное пустое значение, а корректные поля сохраняются для повторного показа.

Обычная форма, массив вместо имени и неверный emailСамостоятельный пример
<?php
declare(strict_types=1);

require 'Validation.php';
use PracticalForm\Validation;
$valid = ['name'=>'Анна', 'email'=>'reader@example.test', 'message'=>'Нужна помощь с заказом.'];
echo count(Validation::contact($valid)['errors']), "\n";
$wrong = $valid;
$wrong['name'] = ['Анна'];
$wrong['email'] = 'not-an-email';
echo implode(',', array_keys(Validation::contact($wrong)['errors'])), "\n";
Ожидаемый результат
0
name,email

Здесь воспроизводится реальный запрос name[]=Анна: PHP создаёт массив. Проверка типа предотвращает TypeError внутри trim и даёт ошибку именно этого поля. Код использует тот же Validation.php, что и готовый проект.

HTML остаётся текстом при повторном показеСамостоятельный пример
<?php
declare(strict_types=1);

require 'Validation.php';
use PracticalForm\Validation;
$result = Validation::contact([
    'name'=>'<b>Анна</b>',
    'email'=>'bad',
    'message'=>'Хочу узнать о доставке.'
]);
echo htmlspecialchars($result['values']['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), "\n";
echo array_key_exists('email', $result['errors']) ? "email: ошибка\n" : "OK\n";
Ожидаемый результат
&lt;b&gt;Анна&lt;/b&gt;
email: ошибка

Здесь намеренно разрешён текст с угловыми скобками: он экранируется в HTML-контексте. Это не универсальная очистка для JavaScript, URL или SQL. Правила имени в продукте можно сузить, но такой запрет сам по себе не заменяет безопасный вывод.

Сохранение и повтор операции

Хранилище получает уже проверенные значения и PDO. Оно не знает о $_POST: поэтому его можно использовать из HTTP, CLI или теста. Уникальный индекс operation_key защищает от второго сохранения того же ключа на уровне базы. Проверка «сначала SELECT, потом INSERT» без такого ограничения имела бы окно гонки. В примере SQLite используется INSERT … ON CONFLICT(operation_key) DO NOTHING, а затем читается существующая запись.

Ключ хранится вместе с SHA-256 нормализованного набора полей. Если тот же ключ пришёл с другим содержимым, система возвращает конфликт: нельзя молча показать успех для другого сообщения. Это хеш сравнения, а не шифрование и не способ скрыть персональные данные. Секретность ключа обеспечивается случайностью и проверкой принадлежности сессии. Для нескольких побочных эффектов, например письма и записи в базе, одного такого INSERT недостаточно: потребуются отдельные правила доставки, outbox и идемпотентный потребитель.

Повтор и конфликт в настоящей SQLiteСамостоятельный пример
<?php
declare(strict_types=1);

require 'ContactStore.php';
use PracticalForm\ContactStore;
$pdo = new PDO('sqlite::memory:');
$store = new ContactStore($pdo);
$data = ['name'=>'Анна','email'=>'reader@example.test','message'=>'Нужна помощь с заказом.'];
echo $store->save('example-operation-1', $data), "\n";
echo $store->save('example-operation-1', $data), "\n";
$data['message'] = 'Изменённое сообщение';
try { $store->save('example-operation-1', $data); }
catch (DomainException) { echo "Конфликт\n"; }
echo $pdo->query('SELECT COUNT(*) FROM contacts')->fetchColumn(), "\n";
Ожидаемый результат
1
1
Конфликт
1

В тесте используется настоящая база в памяти, поэтому проверяется уникальность, SQL и откат, а не имитация метода save. Два одинаковых вызова возвращают один id. Последний вызов изменяет данные с прежним ключом и отклоняется; число записей остаётся равным одному.

Ошибки, PRG и доступность формы

При ошибках полей сервер отвечает 422 и снова показывает форму: значения экранированы, сообщение связано с полем через aria-describedby, неверное поле получает aria-invalid. Общий список ошибок содержит ссылки на соответствующие поля. Клиентская валидация в учебном проекте отключена атрибутом novalidate, чтобы можно было увидеть серверные ветки; в рабочей форме её можно добавить как удобство, сохранив проверки сервера.

После успешного сохранения сервер кладёт короткое сообщение результата в сессию, закрывает её через session_write_close и отдаёт 303 Location на фиксированный локальный адрес. Следующий запрос уже GET. В URL нет имени и email; обновление страницы результата не повторяет POST. Сообщение читается один раз, поэтому после обновления оно исчезает — это поведение flash, а не потеря записи. Для постоянного просмотра результата нужна защищённая страница с проверкой права доступа.

ОтветЧто произошлоКак проверить
200Получение формы или страницы результатаОткройте локальный адрес.
422Ошибка полейОтправьте короткий текст или неверный email.
403Неверный CSRFПодмените токен тестовым запросом.
409Ключ устарел или повторён с другим содержимымПовторите ключ, изменив сообщение.
303Сохранение успешноПосмотрите переход POST → GET.
405 / 415 / 413Метод / формат / размер не поддерживаетсяОтправьте PUT, JSON или тело больше 32 КиБ.

Упражнение: добавить номер заказа

Расширьте форму полем «номер заказа». Принимайте его как необязательную строку только из цифр заданной длины. Не преобразуйте значение с ведущими нулями в int, если это внешний идентификатор. Добавьте поле в договор, проверку, схему, запрос и HTML. Затем проверьте отсутствие, пустую строку, корректное значение и массив вместо строки.

Предлагаемое решение. Сначала определите нормальную форму: отсутствующий или пустой номер становится null, непустой обязан соответствовать заданному формату. Добавьте тесты валидатора до SQL. В хранилище включите номер в payload_hash, иначе повтор с другим номером ошибочно сочтётся одинаковым. Изменение схемы оформите миграцией; создание таблицы IF NOT EXISTS не добавит новую колонку в уже существующую базу.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.