Учебно-демонстрационный микрофреймворк. Написан для открытого урока, чтобы показать изнутри, как устроены DI-контейнер и роутинг современных PHP-фреймворков (Symfony, Laravel и т.п.) — не как ими пользоваться, а как они работают под капотом.
Это обучающий код. Ряд механизмов намеренно упрощён ради наглядности - чтобы алгоритм было видно глазами, а не прятать его в магии. Места, где production-решение выглядело бы иначе, отмечены ниже в разделе «Сознательные упрощения». Это сделано сознательно: цель кода - быть прочитанным и понятым на уроке.
- PHP 8.2+
- nginx + php-fpm (пример конфигурации - ниже)
- DI-контейнер с autowiring - автоматическое разрешение зависимостей по типам параметров конструктора, через Reflection.
- Сканирование сервисов - рекурсивный обход каталогов приложения и построение карты сервисов из найденных классов.
- Атрибутный роутинг -
#[AsController]/#[AsRoute], как в актуальном Symfony. - PSR-совместимость - PSR-7 (HTTP-сообщения) и PSR-17 (фабрики) на базе
nyholm/psr7. - Middleware pipeline - запрос проходит через цепочку middleware до dispatch.
При старте Kernel:
- Сканирует каталоги, указанные в конфиге (
services), и для каждого найденного класса строит объект\ReflectionClass. - Инстанцирует листья графа - сначала создаёт все сервисы без зависимостей (конструктор отсутствует или без параметров). Только их и можно создать на первом шаге, не имея ещё ничего собранного.
- Достраивает граф итеративно. Пока созданы не все найденные классы, контейнер делает повторные проходы: на каждом проходе пытается собрать сервисы, все зависимости которых уже инстанцированы (зависимости берутся по типу параметра конструктора). Так граф зависимостей разворачивается слой за слоем - это наглядная топологическая сортировка через многопроходный fixed-point. Реализация выбрана ради того, чтобы алгоритм было видно, а не ради максимальной эффективности.
- Строит карту роутов из атрибутов
#[AsRoute]на методах контроллеров, помеченных#[AsController]. - На каждый HTTP-запрос: запрос (PSR-7) проходит через middleware pipeline и
попадает в
dispatch(), который по карте роутов находит контроллер и возвращает PSR-7ResponseInterface.
Сердце DI - трейт ServiceLoaderTrait (см. \CodersLairDev\ClFw\DI\Trait). Именно там живёт
описанный выше цикл разрешения зависимостей.
Чтобы не вводить читателя в заблуждение - вот где код намеренно проще, чем боевой, и как это решалось бы в production:
- Нет детекции циклических зависимостей. Цикл разрешения предполагает, что
граф ацикличен и разрешим. При циклической (
A -> B -> A) или неразрешимой зависимости production-контейнер обнаружил бы, что за полный проход не добавилось ни одного сервиса, и бросил бы исключение с описанием цикла. Здесь это опущено ради простоты примера. - Эффективность принесена в жертву наглядности. Многопроходный алгоритм в худшем случае близок к O(n^2). На реальных объёмах разумнее однопроходный топологический resolve по построенному графу либо ленивая инстанциация по требованию (как делает Symfony) с компиляцией контейнера.
- Autowiring - только по типу. Разрешаются зависимости-объекты по типу параметра конструктора. Скалярные параметры, union-типы, значения по умолчанию и именованные аргументы конфигурации — вне зоны демонстрации.
- PSR-17 фабрика в контроллере создаётся через
new(в примереRootController) - ради краткости. По-хорошему фабрика тоже приходит из контейнера; так демонстрация DI замкнулась бы и на сам контроллер.
Классы, живущие во фреймворке (vendor/), сканированием не охватываются, поэтому
регистрируются явно через factories - это показывает разницу между autowiring и
ручной регистрацией фабрикой:
'factories' => [
MiddlewarePipeline::class => fn($c) => new MiddlewarePipeline(),
],Точка входа - public/index.php. Конфигурация (каталоги сервисов, фабрики,
bootstrap-хуки middleware) задаётся массивом и передаётся в Kernel. Пример -
в public/index.php.
server {
listen 80;
server_name localhost;
error_log /dev/stderr;
access_log /dev/stdout;
root /app/public;
location = /favicon.ico {
log_not_found off;
access_log off;
}
rewrite ^/index\.php/?(.*)$ /$1 permanent;
try_files $uri @rewriteapp;
location @rewriteapp {
rewrite ^(.*)$ /index.php/$1 last;
}
location ~ /\. {
deny all;
}
location ~ ^/index\.php(/|$) {
internal;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_index index.php;
send_timeout 1800;
fastcgi_read_timeout 1800;
fastcgi_pass php-fpm:9000;
}
}<?php
namespace App\Root\Infrastructure\Http\Web;
use CodersLairDev\ClFw\Http\Response\Trait\ResponseTrait;
use CodersLairDev\ClFw\Routing\Attribute\AsController;
use CodersLairDev\ClFw\Routing\Attribute\AsRoute;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
#[AsController]
class RootController
{
use ResponseTrait;
#[AsRoute(path: '/')]
public function rootIndex(): ResponseInterface
{
$data = [
'success' => true,
'data' => __CLASS__ . '::' . __FUNCTION__ . '()',
'messages' => [uniqid()],
];
return $this->createResponse(
psr17Factory: new Psr17Factory(),
content: json_encode($data, JSON_THROW_ON_ERROR),
status: 200
);
}
}Написано как материал к открытому уроку по внутреннему устройству DI-контейнеров.