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

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

Разделы этой страницы
  1. Структура веб-приложения: контроллер, сервис, репозиторий
  2. Разделение обязанностей начинается с двух понятных функций
  3. Разделение помогает изменять одну причину за раз
  4. Правило доставки не знает о $_POST
  5. Зависимости передают явно
  6. Практичная структура небольшого сервиса
  7. Событие — факт, обработчик — реакция
  8. Команда, DTO и значение денег
  9. Практика: проверьте решение, а не только успешный путь
  10. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 58 · Практика

Структура веб-приложения: контроллер, сервис, репозиторий

Разделяем обязанности на примере расчёта доставки и заказа, не превращая небольшой сайт в фабрику абстракций.

Разделение обязанностей начинается с двух понятных функций

Нужно посчитать заказ и показать итог. Если вычисление сразу печатает HTML, его трудно использовать для API или письма. Начнём с разделения: одна функция считает, другая решает, как представить результат.

Что уже пригодится. переменная и присваивание, массив, объявление и вызов функции

Данные проходят последовательные шаги
  1. Проверенные входные значения
  2. Функция расчёта возвращает число
  3. Представление превращает число в текст
  4. HTTP-обработчик отправляет ответ

Архитектура — договор о том, какая часть отвечает за действие и с кем общается. Для двух строк не нужен набор классов. Сначала добиваемся понятных границ функций; затем переносим тот же принцип на файлы и объекты.

Расчёт можно использовать в двух представленияхСамостоятельный пример
<?php
declare(strict_types=1);

function total(int $price, int $quantity): int
{
    return $price * $quantity;
}
$sum = total(150, 2);
echo 'Итого: ' . $sum . "\n";
echo json_encode(['total' => $sum], JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
Итого: 300
{"total":300}

Функция total ничего не знает про HTML и JSON. Один результат использован дважды, без копирования расчёта. Цена учебная целочисленная; для денег заранее определите единицу, например копейки, и допустимые границы.

Следующая ступень. Дальше разделяем обработчик запроса, сценарий, данные и внешние API; сравниваем оправданную структуру с лишними слоями.

Разделение помогает изменять одну причину за раз

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

Кто что делает при оформлении заказа

Одни правила заказа нужны форме, API и CLI. Разделение обязанностей позволяет использовать их повторно.

  1. Контроллер: HTTP → входные значения

    Читает запрос, проверяет его форму и передаёт данные сценарию. Не прячет расчёт скидки в echo.

  2. Сервис: выполнить сценарий

    Применяет правила заказа и координирует сохранение. Получает нужные зависимости явно.

  3. Репозиторий: работа с базой

    Выполняет SQL и возвращает данные сервису. Правило доставки не обязано знать о PDO.

  4. Ответ: результат → HTML или JSON

    Контроллер выбирает статус, представление показывает результат, учитывая контекст вывода.

Это обязанности, а не обязательные четыре класса. Результат репозитория возвращается сервису, затем контроллеру и пользователю.

Если index.php одновременно читает POST, считает скидку, делает SQL и печатает HTML, первое изменение кажется быстрым. Затем ту же скидку нужно применить в CLI-импорте и API. Копирование куска контроллера переносит туда зависимость от браузера, сессии и echo. Разделение обязанностей нужно прежде всего для повторного использования правил и понятного тестирования, а не ради количества папок.

Контроллер переводит HTTP во входные значения приложения и результат приложения в HTTP. Сервис выполняет сценарий: оформить заказ, отменить бронь, обновить каталог. Репозиторий читает и сохраняет данные, скрывая SQL. Представление показывает результат. Эти роли могут быть небольшими функциями или классами; ООП полезно, когда нужно хранить зависимости и выразить контракт.

Не каждая таблица требует репозитория, а каждое действие — интерфейса. Начинайте с реальных границ: внешний API, база, часы, очередь. Интерфейс особенно полезен там, где есть замена реализации или тестовый дубль. Пустая обёртка над каждой функцией PHP затрудняет чтение и не добавляет надёжности. Архитектура должна объяснять поток данных, а не прятать его за десятью слоями.

Сценарий заказа
  1. Контроллер: проверить HTTP-вход
  2. Сервис: правила и транзакция
  3. Репозиторий: SQL и данные
  4. Сервис: результат сценария
  5. Представление / JSON-ответ

Правило доставки не знает о $_POST

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

final class ShippingPolicy {
    public function priceFor(int $subtotalMinor, bool $pickup): int {
        if ($subtotalMinor < 0) throw new InvalidArgumentException('Negative subtotal');
        if ($pickup || $subtotalMinor >= 500000) return 0;
        return 39000;
    }
}
$policy = new ShippingPolicy();
echo $policy->priceFor(100000,false)."
";
echo $policy->priceFor(100000,true)."
";
echo $policy->priceFor(500000,false)."
";
Ожидаемый результат
39000
0
0

Политика получает уже проверенные значения. Её можно вызвать из HTTP, CLI и теста. Решение о бесплатной доставке не зависит от скрытого поля free_shipping, которое пользователь способен изменить.

Зависимости передают явно

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

interface Clock { public function now(): int; }
final class FixedClock implements Clock {
    public function __construct(private int $timestamp) {}
    public function now(): int { return $this->timestamp; }
}
final class ReservationPolicy {
    public function __construct(private Clock $clock) {}
    public function expired(int $createdAt): bool {
        return $this->clock->now() - $createdAt >= 900;
    }
}
$policy = new ReservationPolicy(new FixedClock(2000));
echo $policy->expired(1100) ? "expired
" : "active
";
echo $policy->expired(1101) ? "expired
" : "active
";
Ожидаемый результат
expired
active

Интерфейс часов оправдан реальной потребностью проверять границу 15 минут. В рабочей реализации now возвращает time. В тесте не надо ждать 15 минут или менять системные часы.

Точка сборки приложения создаёт PDO, репозитории, клиентов и сервисы. Конструктор сервиса принимает их явно. Так становится видно, почему сценарий обращается к сети и какие зависимости надо подменить в тесте. Глобальный контейнер, из которого любой метод достаёт что угодно, возвращает скрытые зависимости под другим названием.

В доменных правилах предпочитайте значения и понятные результаты. Недостаток товара — ожидаемая ситуация сценария; недоступная база — инфраструктурный сбой. Можно использовать доменное исключение или объект результата, но HTTP-статус не должен случайно определяться текстом PDOException. Граница приложения сопоставляет тип результата с сообщением и кодом ответа.

Практичная структура небольшого сервиса

КаталогОтветственностьЧего там избегать
public/index.php и публичные ресурсыСекретов, дампов базы, исходных архивов
src/Http/Маршруты, контроллеры, преобразование входаРасчёта скидки и SQL в шаблоне
src/Application/Сценарии и координацияПрямого echo и чтения $_POST
src/Domain/Правила и значения предметной областиЗависимости от браузерной сессии
src/Infrastructure/PDO, HTTP-клиенты, файловое хранилищеПоглощения ошибок без диагностики
templates/ и tests/Представления и проверкиЗаписи в production из тестов

Для маленького проекта несколько файлов с этими ролями лучше, чем преждевременно сложная иерархия. Рост структуры должен следовать за ростом поведения. Когда появляется второй вход — консольная команда или webhook — уже готовый сервис предотвращает копирование правил.

Отдельно определите владельца транзакции. Если каждый репозиторий сам вызывает commit, сервис не сможет атомарно объединить заказ и остаток. Если каждый вложенный сервис начинает транзакцию, появится конфликт вложенности. Обычно прикладной сценарий задаёт границу, а репозитории работают на переданном соединении.

Событие — факт, обработчик — реакция

OrderPlaced означает уже произошедший факт, а PlaceOrder — намерение/команда. Синхронный обработчик события выполняется в текущем процессе и может задержать ответ или бросить исключение. Асинхронный обработчик работает позже, требует надёжной доставки и идемпотентности. Само имя EventDispatcher не гарантирует очередь или транзакционность.

Сначала сохраните факт надёжно, затем запускайте внешние реакции через outbox, если потеря сообщения существенна. Не прячьте обязательное правило вроде списания остатка в необязательном слушателе, который можно забыть зарегистрировать. Граф потока событий должен показывать, где синхронный вызов, где граница процесса и где возможен повтор. Это делает отладку понятнее даже опытному разработчику.

Команда, DTO и значение денег

DTO передаёт проверенные данные между слоями, но сам по себе не гарантирует бизнес-правило. OrderInput может содержать quantity как int, а допустимый диапазон и наличие товара проверяются сценарием. Не называйте любую структуру DomainObject только ради солидности: важнее, где гарантируется корректность и кто имеет право менять состояние.

Объект Money полезен, когда нужно совместно хранить сумму и валюту, запретить сложение RUB с USD и централизовать округление. Для маленького расчёта достаточно целых минимальных единиц с явными именами amountMinor. Ошибка возникает, когда один метод ожидает рубли, другой копейки, а оба принимают безымянный int. Тип или ясное имя должны делать единицу измерения видимой.

Результат сценария также лучше выразить явно: created, already_processed, rejected или temporarily_unavailable. Возвращать false на все случаи легко, но контроллеру потом приходится угадывать причину. Хорошая граница позволяет изменить HTML на JSON, не переписывая правила заказа и не теряя различие ожидаемой ошибки пользователя и сбоя инфраструктуры.

Практика: проверьте решение, а не только успешный путь

Нужно запускать оформление заказа из формы и из ночного импорта. Какие части можно разделить, а какие переиспользовать?

Разбор решения

HTTP-валидация формата, CSRF и ответ остаются контроллеру; CLI разбирает свои аргументы. Оба передают проверенные команды одному сервису правил заказа. Репозитории, транзакция, идемпотентность и расчёт цены общие. Контекст полномочий передаётся явно, а импорт не получает неограниченные права случайно.

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

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

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

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

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