Документация
Предыстория
Next2 является переработкой более ранней библиотеки psr15-next. Первая версия была написана для простой, гибкой и быстрой работы с PSR-15 middleware в стиле приложений на фреймворке KoaJs, который в свою очередь очень похож на ExpressJs.
Hello World
Минимальное приложение с поддержкой PSR-7 на Next2 выглядит так:
use PTS\Next2\Context\ContextInterface;
use PTS\Next2\MicroApp;
use PTS\Psr7\Response\JsonResponse;
use PTS\Psr7\ServerRequest;
use PTS\Psr7\Uri;
require_once '../../../vendor/autoload.php';
$psr7Request = new ServerRequest('GET', new Uri('/'));
$app = new MicroApp;
$app->store->get('/', function(ContextInterface $ctx) {
$ctx->setResponse(new JsonResponse(['message' => 'Hello World!']));
});
$psr7Response = $app->handle($psr7Request); // psr-15 runner
Глоссарий
Middleware (обработчик промежуточного уровня)
Middleware - это часть приложения, которая отвечает за конкретную единицу логики в обработке request/response. (PSR-15 middleware)
Отсылка к PSR-15
Первой версия фреймворка использовала PSR-15 middleware. Next2 используеют похожую на PSR-15
по смыслу и сигнатуре модель выстраивания обработчиков.
Отличия от PSR-15 обработчиков:
- Обработчиком промежуточного уровня выступает любой обработчик с типом
callable
вместоPsr\Http\Server\MiddlewareInterface
. - Layer может запускать не 1 обработчик, а группу обработчиков
callable[]
, позволяя делить код на более мелкие части сохраняя логическую целостность.
Layer / Слой
Это абстракция над обработчиком, которая содержит метаинформцию (для простоты это можно считатать алиасом middleware). Layer может иметь как 1 обработчик, так и несколько, чтобы декомпозировать на более мелкие части код. Приложение это набор Layers, которые определяются для каждого http запроса индивидуально.
Приложение
Приложение является самым высоким уровнем и реалзиует полноую обработку запроса. Задача приложения получить на входе http request и создать http response.
Приложение осознано сводит число сущностей к минимуму, чтобы оставаться действительно простым, понятным и надежным. Основное понятие с которым придется постоянно работать это Layer (Слой).
Приложеине получает на вход стандартный объект запроса PSR-7
и прогоняет через все обработчики слоев. Какие именно слои примут частие в обработке запроса определяется метаифнормацией слоя. Семантически можно выделить следующие типы слоев:
- Активация на любом запросе
- Активация по http методу
- Активация по соответствию uri в регулярном выражении
Собственные стратегии
Из коробки идет минимальный набор стратений (по http методу и поиск по uri). Можно дополнительно реализовать любые кастомные правила, по которым будет принято решеине активируетя слой для обработки request или нет. Для э того нужно добавить свою реализацию PTS\Next2\Layer\Resolver\LayerResolverInterface
Строительные блоки
Handler / Обработчик
Обработчиком может выступать любая форма callable
PHP типа. Функция обработчик в общем случае принмает 1 аргумент. Первый аргумент это контекст, который должнен реализовывать PTS\Next2\Context\ContextInterface
либо его наследников в случае расширения базового контекста под себя.
Обработчик это самая минимальная единица.
Context
Контекст - это связующая часть между всеми слоями. Базовый контекст хранит request, response и позволяет получить доступ к текущему слою (Layer) в runtime. В контексте можно хранить все что угодно, расширяя его дочерним классом и не ограничивая себя. Данный подход хорошо себя зарекомендовал в таких фреймворках как koaJs и goFiber.
Слои
Приложение MicroApp
хранит в себе в свойстве $app->store
хранилище слоев LayersStore
. Слой инкапсулирует 1 обработчик или группу обработчиков. Хранилище слоев позволяет добавлять обрабочтики к приложению посредством метода use
:
/**
* @param callable[] | callable $handler
*/
public function use(callable|array $handler, array $options = []): static
{
$layer = $this->layerFactory->create($handler, $options);
return $this->addLayer($layer);
}
Чтобы дерелировать управление следующему обработчику, нужно вызвать метод next
на объекте контекста:
public function someHandler(\PTS\Next2\Context\ContextInterface $ctx)
{
// request phase
$ctx->next();
// response phase
}
Слоенный подход
Каждый слой имеет 1 определенную зону отвественности, это позволяет оставаться вашему коду простым и чистым. Слой может самостоятельно создать http response и не передавать управление следующему слою. Например слой, который проверяет аунтификацию, может сам создать http response с статус кодом 401
. С таким подходом, мы можем очень быстро обрабатывать некоторые запросы, даже не поднимая тяжелый фреймворк, если он используется на последующих слоях.
Другой пример, это обработчик, который может кешировать запрос и повторно очень быстро отвечать на запрос из кеша.
Options
Опции объекта Layer
Опция | Описание | По умолчанию | Пример значения |
---|---|---|---|
path | описывает uri путь, на который должен активироваться слой, можро использловать регулярки, без указария этого параметра слой активируется на любой uri | - | /users/{id} |
name | определяем уникальное имя слоя, через это имя можно будет найти слой (по умолчанию все слои получают имена вида l-0 , l-1 и т.д.) | - | usersAction |
methods | принимает массив из http методов, слой активируется только на http запросы с указанными http методами. Если методы не переданы, то слой активируется на любой http метод | [] | ['GET'] |
priority | принимает int число с приоритетом, чем ни меньше число, тем раньше выполнится слой. Это позволяет конфигурировать слои декларативно, не в порядке добавления | 50 | 100 |
restrictions | принимает массив вида ['id' => \id+], позволяя накладывать дополнительные ограничения на переменны в uri запроса | [] | ['id'=>'\d+'] |
context | позволяет прикрепить к слою любые произволные данные | [] | ['foo'=>'bar'] |
Пример использования options в коде:
use PTS\Next2\Context\ContextInterface;
use PTS\Next2\MicroApp;
use PTS\Psr7\Response\JsonResponse;
use PTS\Psr7\ServerRequest;
use PTS\Psr7\Uri;
require_once '../../../vendor/autoload.php';
$psr7Request = new ServerRequest('GET', new Uri('/hello/alex/'));
$app = new MicroApp;
$app->store->use(function(ContextInterface $ctx) {
$name = $ctx->getUriParams()['name'] ?? null;
$ctx->response = new JsonResponse(['message' => "Hello {$name}!"]);
}, [
'path' => '/hello/{name}/',
'name' => 'helloWorldAction',
'priority' => 50,
'methods' => 'GET|HEAD',
'restrictions' => [
'name' => '[a-zA-Z0-9]+'
]
]);
$psr7Response = $app->handle($psr7Request);
Приложение
Приложение это самый высокий слой. Приложение инкапсулирует группу слоев. Приложения могут объединяться через CompositionMicroApp
в общее приложение.
Прочее
Быстрые http методы (сахар)
Для упрощения также доступны методы, которые зеркалируют основные http методы, например:
$app->store
->get('/users/{id}/', fn($ctx) => $ctx->response = new JsonResponse)
->post('/users/{id}/', fn($ctx) => ...)
->patch('/users/{id}/', fn($ctx) => ...)
->put('/users/{id}/', fn($ctx) => ...)
->delete('/users/{id}/', fn($ctx) => ...);
Быстрые методы
Полный список методов можно найти в трейте PTS\Next2\Layer\Store\FastMethodsTrait
. Он реализует самые популярные методы. Вы можете добавить свои методы для сахара
Декларативная кофнигурация
Очень удобно конфигурировать маршруты и порядок декларативно, например посредством yml файлов, простой пример такой конфигурации выглядит так:
# middlewares
ThrowableToResponse:
controller: Site\Middleware\ThrowableToResponse:process
# 404 page
otherwise:
handler: Site\Controller\Action\Page404:__invoke
name: otherwise
priority: 10000
# actions
main:
path: /
methods: 'GET|HEAD'
handler: Site\Controller\OtherwiseController:mainPage
posts:
path: /post/
methods: 'GET'
handler: Site\Controller\PostController:getList
cat:
path: /cat/{id}/
restrictions:
id: \d+
methods: 'GET'
handler: Site\Controller\CategoryController:getCat
Можно расширять декларативную конфигурацию посредством расширения класса PTS\Next2\LayerLayerFactory
или реалзиации PTS\Next2\LayerFactoryInterface
интерфейса.
Inline ограничения параметров в uri
Ограничения на параметр в path
можно описать дополнительно прямо в path
помимо конфигурации опции restrictions
у слоя. Inline формат записи проще для простых регулярок. Для сложных и длинных регулярок лучше использовать явно опцию restrictions
, чтобы конфигурация слоя оставалась простой для чтения и интерпритации. В общем виде такая форма записи выглядит так {name:restrictRegExp}
. Следующие конфигурации будут эквивалентны:
cat:
path: /cat/{id}/
restrictions:
id: \d+
cat2:
path: /cat/{id:\d+}/
Приоритет
Inline ограничения будут проигнорированы, если для параметра по имени параметра сконфигурировано в конфиге слоя ограничение
Flow
Каждый слой может что-то делать до вызова $ctx->next()
на этапе запроса, также может что-то делать после вызова $ctx->next()
на этапе ответа. Пример полного выполнения запроса, который проходит сквозь все слои:
Пример flow, кога запрос досрочно может быть обработан одним из слоев, без делегирования обработки через $ctx->next()
:
Каждый слой решает сам, создать http response и пректатить обработку, либо вызвать следующий слой и делегировать ему тем самым обработку.
PSR-15 Middleware
Интеграция
Приложение реализует интерфейс Psr\Http\Server\RequestHandlerInterface
из PSR-15
. За счет этого оно может интегрировться в любые приложения как middleware, принимая на вход PSR7 Request
и возвращая PSR7 Response
.
Схематично это выглядит как psr15
-> psr15
-> next2
.
Если микро приложение должно делегировать обработку запроса далее в классическую PSR-15 Middleware
, то можно обернуть в такую middleware:
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use PTS\Next2\Adapter\Psr7RunnerAdapter;
use PTS\Next2\Context\ContextInterface;
use PTS\Next2\Layer\Layer;
use PTS\Next2\MicroApp;
class NextMiddleware implements MiddlewareInterface
{
/** @var Layer[] */
protected array $layers;
protected Psr7RunnerAdapter $runner;
public function __construct(MicroApp $app)
{
$this->layers = $app->store->getLayers();
$this->runner = new Psr7RunnerAdapter;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$callable = function(ContextInterface $ctx) use ($handler) {
$response = $handler->handle($ctx->getRequest());
$ctx->setResponse($response);
};
$adapterLayer = new Layer([$callable]);
$layers = [...$this->layers, $adapterLayer];
$this->runner->run($layers, $request);
return $handler->handle($request);
}
}
Схематично это выглядит как psr15
-> psr15
-> next2
-> psr15
.
Такие интеграции позволяют точечно заменять части старого приложения или переносить небольшими кусочками часть функционала.
TODO
- подумать о передачи в
$ctx->next()
аргументов, которые придут экстра аргументами в следующий handler помимо контекста. - подумать об ограничении uri параметров до регулядки [A-Za-z0-9_-]) по дефолту
- подумать о автозаполнителях и валидаторах параметров из запроса как в koaJs router param (сейчас легко реализуются обработчиком или слоем с сохранением в конеткст, без ввода новой абстракции или сахара)