Skip to content

coders-lair-dev/clfw

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Минимальный PHP-фреймворк (ClFw)

Учебно-демонстрационный микрофреймворк. Написан для открытого урока, чтобы показать изнутри, как устроены 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:

  1. Сканирует каталоги, указанные в конфиге (services), и для каждого найденного класса строит объект \ReflectionClass.
  2. Инстанцирует листья графа - сначала создаёт все сервисы без зависимостей (конструктор отсутствует или без параметров). Только их и можно создать на первом шаге, не имея ещё ничего собранного.
  3. Достраивает граф итеративно. Пока созданы не все найденные классы, контейнер делает повторные проходы: на каждом проходе пытается собрать сервисы, все зависимости которых уже инстанцированы (зависимости берутся по типу параметра конструктора). Так граф зависимостей разворачивается слой за слоем - это наглядная топологическая сортировка через многопроходный fixed-point. Реализация выбрана ради того, чтобы алгоритм было видно, а не ради максимальной эффективности.
  4. Строит карту роутов из атрибутов #[AsRoute] на методах контроллеров, помеченных #[AsController].
  5. На каждый HTTP-запрос: запрос (PSR-7) проходит через middleware pipeline и попадает в dispatch(), который по карте роутов находит контроллер и возвращает PSR-7 ResponseInterface.

Сердце 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.

nginx.conf (для docker-окружения)

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-контейнеров.

About

Simple framework implementation for pure PHP

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages