← Назад к блогу

Как организовать небольшой PHP-проект без фреймворка

Небольшому 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, шаблонизатор или сложный контейнер зависимостей.
  • Слои с названиями, но без самостоятельной ответственности.

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

Когда всё-таки нужен фреймворк

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

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

Официальные источники

PHP-FIG: PSR-4 Autoloader
PHP-FIG: PSR-11 Meta Document

← Статья про Nginx и PHP-FPM Все статьи →