Небольшому PHP-проекту не всегда нужен полноценный фреймворк. Но отсутствие фреймворка не означает, что маршрутизацию, SQL и HTML нужно смешать в одном файле. Нескольких ясных границ достаточно, чтобы код оставался понятным.
Базовая структура каталогов
project/
├── config/
├── public/
│ └── index.php
├── src/
│ ├── Controller/
│ ├── Repository/
│ └── Service/
├── templates/
├── tests/
├── composer.json
└── vendor/
public — единственный каталог, доступный веб-серверу. В src находится PHP-код приложения, в templates — представления, а в config — конфигурация. Каталог vendor создаёт Composer, вручную его не редактируют.
Автозагрузка через Composer
Стандарт PSR-4 связывает пространство имён класса с каталогом. Благодаря этому не нужно вручную подключать каждый файл через require.
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Класс App\Service\PriceCalculator будет находиться в файле src/Service/PriceCalculator.php. Это предсказуемое соглашение, понятное большинству PHP-инструментов.
Одна точка входа
Все динамические запросы удобно направлять в public/index.php. Этот файл собирает приложение: подключает автозагрузку, создаёт основные зависимости, запускает маршрутизатор и отправляет ответ.
<?php
declare(strict_types=1);
require dirname(__DIR__).'/vendor/autoload.php';
$router = new Router();
$response = $router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
$response->send();
Фронт-контроллер — место сборки приложения, а не место для бизнес-логики. Если index.php начинает рассчитывать цены или выполнять SQL-запросы, ответственность уже смешана.
Разделение ответственности
| Часть | Ответственность |
|---|---|
| Router | Сопоставляет HTTP-запрос с обработчиком |
| Controller | Принимает входные данные и формирует HTTP-ответ |
| Service | Выполняет прикладной сценарий или бизнес-правило |
| Repository | Инкапсулирует получение и сохранение данных |
| Template | Отображает уже подготовленные данные |
Контроллер при этом остаётся тонким: получает запрос, вызывает нужный сервис и возвращает результат. Он не должен одновременно проверять бизнес-правила, строить SQL и рендерить HTML вручную.
Зависимости передаются явно
final class OrderService
{
public function __construct(
private OrderRepository $orders,
private PriceCalculator $calculator,
) {
}
}
По конструктору видно, без чего сервис не работает. Это проще тестировать и безопаснее, чем получать глобальный контейнер и искать зависимости внутри метода.
Сам контейнер зависимостей может быть полезен в точке сборки приложения, но передавать его каждому классу не стоит: такой подход скрывает реальные связи и превращается в Service Locator.
Чего не нужно добавлять заранее
- Интерфейс для каждого класса, если второй реализации не ожидается.
- Репозиторий для кода, который вообще не работает с хранилищем.
- Собственную ORM, шаблонизатор или сложный контейнер зависимостей.
- Слои с названиями, но без самостоятельной ответственности.
Граница полезна, когда она уменьшает связанность, делает код проверяемым или изолирует изменчивую инфраструктуру. Если она только увеличивает количество файлов, проект становится сложнее без практической выгоды.
Когда всё-таки нужен фреймворк
По мере роста понадобятся проверенная маршрутизация, валидация, безопасность, работа с формами, очередями, событиями и базой данных. Собирать всё это самостоятельно обычно дороже, чем взять поддерживаемые компоненты или фреймворк.
Отказ от фреймворка оправдан небольшим объёмом задачи, а не желанием заново написать инфраструктуру современного веб-приложения.