CEKTA FRAMEWORK
Это набор абстракций и их имплементаций для быстрой и удобно разработки, а также для деплоя в прод решений: Создание API это одно из основных использований фреймворка.
Используются следующие решения:
- PSR-15: HTTP Server Request Handlers реализуете вы нужные приложению endpoints.
- CEKTA ROUTING реализованно с помощью адаптера cekta/routing-fastroute для nikic/fast-route.
- Roadrunner Server RR.
Чем отличается от других фреймворков
- Минимализм и простота.
- Модульность и расширяемость.
- Быстрый старт (skeleton).
- Использование проверенных решений.
Минимализм
FRAMEWORK не является fullstack framework и не тянет за собой работу с FRONTEND, так как это целый отдельный мир, который лучше реализовывать отдельным репозиторием, по стандартам FRONTEND.
В framework реализуются только те абстракции которые наиболее необходимы backend в большинстве проектов.
Например, framework не предоставляет абстракций для работы с БД, так как предпочтения работы с БД могут быть различными. Одним нужно работать с соединением напрямую (PDO или минимальные обертки), другим нужны крупны ORM вроде doctrine/orm, третьи не видят работы с бд без Active Record, четвертые предпочитают сами реализовывать репозитории и тд.
Все это взаимоисключающие требования и заниматься созданием новых уникальных библиотек по работе с БД в рамках фреймворка не лучшая идея, проще сфокусироваться на развитие других полезных вещей, а библиотеку для работы выберет конечный пользователь (разработчик) исходя из требований проекта и навыков команды, таких библиотек огромное количество.
Аналогичным образом выбор какую БД использовать в проекте pgsql или mysql и тд, какой версии ? Это все индивидуальные особенности каждого проекта и предпочтений команды, задача фреймворка это позволить сделать такой выбор конечному пользователю (разработчику).
Мы стремимся к тому чтобы после установки проекта нам не приходилось “выпиливать” ничего из проекта, при этом должно быть минимум тело движений для его запуска в разработке или сборке для прода.
Модульность и расширяемость.
Раз фреймворк предоставляет минимально необходимый набор инструментов, значит у большинства будет необходимость в расширение стандартной поставки, это должна быть штатная и ожидаемая вещь.
Расширяемость достигается за счет гибкой системы внедрения зависимостей cekta/di, а также абстракций framework в виде Module и Project и адекватным инструментам разработки вроде docker.
Быстрый старт (skeleton).
Как бы не было легко расширять фреймворк, все равно современный проект содержит большое количество дефолтных конфигурационных файлов (composer.json, .gitignore, docker-compose.yml, rr.yaml и тд).
Хочется просто взять и начать работать, для этого сделан пример проекта SKELETON который позволяет как начать разработку, так и подготовить его к production окружению!!!
Использование проверенных решений.
Некоторые решения нет смысла создавать с нуля в них могут вложены человека/годы разработки например fastroute, roadrunner (RR).
Гораздо лучше их использовать адаптируя под свои ожидания, это позволяет получить максимальную производительность фреймворка минимальной ценой и низкий порог входа.
Skeleton
Это минимально возможная структура проекта, для того чтобы создавать ваше приложение.
По умолчанию она оптимизированная для разработки, но имеет встроенную продуктовую сборку!!!
Требования:
- docker - обязательно.
- git - чтобы склонировать проект или пушить обновления
- make - опционально можно команды выполнять cli.
DEV (для разработки)
По умолчанию из коробки все настроено для разработки, так как в первую очередь с этим работают разработчики, но есть возможность установить настройки для production.
Usage
git clone https://github.com/cekta/skeleton.git {you_project_name}
cd {you_project_name}
git remote set-url origin {git url для вашего репозитория}
make dev
По умолчанию проект будет доступен на http://localhost:8080
Shell
Войти в dev окружение
make shell
make {command} - это команды выполняемые с host системы
composer {command} - это команды которые внутри dev окружения
Например, эта команда запускается на host системе.
make test
Эта команда делает те же действия, но из dev окружения.
composer test
Внутри dev окружения могут быть и другие полезные команды.
Update
Если вы изменили в проекте PHP файлы, конструктор класса, то чтобы изменения применились необходимо выполнить build.
make build
из dev окружения:
composer build
Full UPDATE
Чтобы применить обновления вашего проекта из репозитория, на host системе выполните:
git pull # обновляемся из origin
make refresh # удаляем все что было и собираем снова
make dev # запускаем новую версию
PRODUCTION (сборка для деплоя)
В разных компаниях и разных проектах используются разные политики выкладки релизов (build и deploy) и разные инструменты, их выбор индивидуальный, нельзя сделать решение для всех, но можно сделать основу которую можно кастомизировать под ваши реалии.
Внутри SKELETON есть минимальный пример сборки production image для docker, который можно запушить в docker registry и от туда распространять по вашему продуктовому окружению, вашими инструментами деплоя.
Сборка production версии app
make image # только app без сборки зависимостей вроде db, s3 и тд
Вы можете открыть инструкции по сборке и кастомизировать их, например добавить свой tag, указать registry, передать аргументы сборки, изменить релизные конфиги и тд.
После сборки проекта обычно image необходимо запушить на docker registry, а уже в дальнейшем его разворачивают на боевых или тестовых серверах.
Пример запуска production app у вас на компьютере:
docker run -p 8090:8080 --rm cekta-app:latest
Продуктовая версия будет на http://localhost:8090
Обратите внимание другие сервисы такие как db(mysql/pgsql), redis, opensearch, s3 и тд. Необходимо администрировать вам, вы лишь можете с переменных окружения задавать актуальные адреса и учетные данные.
Отличия base, dev, production.
Как бы нам не хотелось иметь абсолютно одинаковые окружения, production окружение отличается от окружения разработки, отличия:
- На production не нужен xdebug, на dev среде он может быть полезным.
- На production не нужны phpunit, phpcs, статический анализ, это все надо только на этапе разработки.
- На production не нужен даже composer, управление пакетами нужно только на этапе разработки и сборке релизной версии.
- Логирование сообщений в json формате, отключение различных debug режимов, оптимизация производительности и тд.

На картинке видно что имеется base которая является общей как для dev так и для production.
В base необходимо прописывать общие зависимости, например php extension pdo или любые другие extension которые нужны в обоих случаях, соответственно в dev нужно прописывать то что нужно ТОЛЬКО на dev версии, а в production ТОЛЬКО то что нужно в production.
Например в production можно добавлять различные конфиги оптимизирующие производительность.
Для сборки production версии в начале сборка осуществляется в dev версии, убеждаемся что ее можно собирать для релиза,
потом убираем dev пакеты из сборки, делаем composer build только теперь можно эти файлы добавлять в production
окружение.
Project
Это основная информация о вашем проекте, его основная задача получить объект реализующий
Psr\Container\ContainerInterface с помощью которого дальше разрешаются все зависимости.
Параметры для создания
- modules - массив объектов реализующих
\Cekta\Framework\Contract\Module - container_filename - полный путь до файла где будет осуществляться build класса Container.
- container_fqcn - FQCN класса Container, следуйте psr4, он должен иметь автозагрузку via composer.
- discover_filename - имя файла где будет храниться кэш discovery.
- class_loader - callback функция предоставляющая список всех классов проекта.
Project предоставляет модулям общий жизненный цикл, а также необходимую информацию (список всех классов проекта),
чтобы модули сформировали необходимую конфигурацию для создания объекта реализующего ContainerInterface.
Жизненный цикл
- Discovery - исследование всех reflection классов проекта.
- Build - генерация файла класса реализующего
ContainerInterface. - Create - создание экземпляра класса реализующего
ContainerInterfaceи передача ему необходимы параметров.
Discovery
Это процесс когда каждый модуль получает экземпляр ReflectionClass всех объектов проекта, каждый модуль может их
проанализировать discover(ReflectionClass $class): void и использовать эту информацию на следующих этапах.
Можно посмотреть все классы которые реализуют interface, или имеют php attribute (GET, POST и тд).
После выполнения discovery каждый модуль опрашивается getCacheableData(): mixed каждый модуль может сохранить любую
информацию для себя, эта информация запишется в файлик (cache), требуется чтобы данные можно было преобразовать в
json с помощью json_encode.
В последствии данные для каждого модуля берутся из этого файла, это позволяет не загружать каждый раз callback по получению всех классов проекта, не делать ресурсоемкий анализ всех классов проекта, а делать это единожды.
Модули могут игнорировать этап discovery, ничего не анализируя и возвращать пустой массив, реализация этого этапа опциональна для модулей.
Build
После осуществления discovery необходимо сгенерировать файл класса реализующий ContainerInterface.
Для этого опрашивается каждый модуль onBuildDefinitions($cachedData) передается информация с этапа discovery, этот
метод должен вернуть набор параметров entries, alias, factories, singletons которые используются при
генерации класса с помощью cekta/di.
Во время этапа build также вызывается onCreateParameters(mixed $cachedData): array которая возвращает params
имена параметров используются во время build, чтобы получить обязательные параметры, которые необходимо
передавать на этапе create. Значения параметров будут браться на этапе Create.
Подробней о entries, params, alias, factories, singletons в документации cekta/di.
Этап build вызывается единожды, в последующем имеется сгенерированный файл класса runtime/AppContainer.php с
результатом выполнения данного этапа.
Create
На этом этапе создается экземпляр класса реализующего ContainerInterface, для создания объекта нужно передать
значения параметров.
Project у каждого модуля вызываете onCreateParameters(mixed $cachedData): array для получения params,
значение которых используются для внедрения в зависимости.
Этот этап вызывается каждый раз (при обработке http запроса или выполнения cli команды),
onCreateParameters(mixed $cachedData): array оптимизировано с точки зрения производительности.
Project в SKELETON
Project в SKELETON расширяет базовую реализацию
Project.
Параметры для создания
env - параметры окружения, актуальные настройки сервера и тд.
Внутри project в skeleton хранится основная настройка приложения и инициализация всех модулей.
Мы получаем единое место настройки приложения, а также подключения и управления всеми модулями.
Некоторые модули могут иметь обязательные и опциональные параметры, которые мы можем конфигурировать в одном месте.
Создание своего HTTP обработчика.
Создание HTTP обработчиков это одна из основных задач разработчика API.
- Создайте класс (в любом месте) реализующий \Psr\Http\Server\RequestHandlerInterface
- Используйте php
attribute \Cekta\Framework\HTTP\Route:
- pattern - url который должен обрабатываться
- method - http method (GET, POST, PATCH, …) есть alias где его задавать не нужно
\Cekta\Framework\HTTP\Route\POST
\Cekta\Framework\HTTP\Route\DELETE и
тд.
По умолчанию:GET. - middlewares -
имена psr/middleware
реализаций которые необходимо вызывать.
По умолчанию:[].
- Сделайте build проекта
make build - Можно открывать endpoint с указанным method и pattern.
В качестве примера обработчика можно изучить App\Welcome
Зависимости внедрять через autowiring в конструктор, они будут подгружаться автоматически, необходимые параметры будут запрошены во время build.
Параметры в url
При создании API мы хотим передавать параметры в url
GET /api/v1/items/{id}
В качестве маршрутизации используется fastroute. Можно пользоваться всеми возможностями задавая паттерн и регулярные выражения для значений.
Встреченные атрибуты можно получать с помощью $request->getAttribute('имя атрибута').
Пример
src/Example.php - расположить можно где угодно, главное следовать psr4.
<?php
declare(strict_types=1);
namespace App;
use Cekta\Framework\HTTP\Response\JSONFactory;
use Cekta\Framework\HTTP\Route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
#[Route\GET('/api/v1/items/{id:\d+}')]
final readonly class Example implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create([
'item_id' => $request->getAttribute('id'),
]);
}
}
make restart
Открываем http://localhost:8080/api/v1/items/345
Смотрим результат и видим номер 345 отправленный нами, причем в качестве id могут быть только цифры (должно соответствовать regexp указанными при регистрации), например http://localhost:8080/api/v1/items/abc вернет 404.
Управление страницей 404 (NOT FOUND)
В случае если запрашивают url, pattern которого не зарегистрирован, то вызывается endpoint указанный для страницы 404.
Вы можете переопределить стандартный обработчик, а также добавить свои middlewares.
Настройка этих параметров осуществляется в App\Project
при создании \Cekta\Framework\HTTP\Module() можно передать опциональные параметры:
handler_404 имя класса обработчика который использовать для обработки 404 страницы,
должен реализовать интерфейс \Psr\Http\Server\RequestHandlerInterface.
DEFAULT: \Cekta\Framework\HTTP\Handler\NotFound::class
middlewares_404 - массив строк имен классов middlewares которые будут вызываться при обработке страницы 404,
каждый класс должен реализовывать \Psr\Http\Server\MiddlewareInterface.
DEFAULT: []
Изменение страницы 404.
src/Project.php
new \Cekta\Framework\HTTP\Module(
handler_404: App\MyHandler404::class,
),
src/MyHandler404.php - расположить можно где угодно, следуйте psr-4.
<?php
declare(strict_types=1);
namespace App;
use Cekta\Framework\HTTP\Response\JSONFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final readonly class MyHandler404 implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create(
['message' => 'new 404 handler'],
404
);
}
}
Перезапустим (build и перезапуск app):
make restart
Откроем 404 страницу, http://localhost:8080/some-not-exist-url.
Увидим обновленную 404 страницу.
Управление страницей 405 (NOT ALLOWED)
В случае если запрашивают url, pattern которого зарегистрирован, но вы вызываете с неправильным HTTP METHOD,
например зарегистрирован GET /, а вы вызываете POST /.
Вы можете переопределить стандартный обработчик, а также добавить свои middlewares.
Настройка этих параметров осуществляется в App\Project
при создании \Cekta\Framework\HTTP\Module() можно передать опциональные параметры:
handler_405 имя класса обработчика который использовать для обработки 405 страницы,
должен реализовать интерфейс \Psr\Http\Server\RequestHandlerInterface.
DEFAULT: \Cekta\Framework\HTTP\Handler\NotFound::class
middlewares_405 - массив строк имен классов middlewares которые будут вызываться при обработке страницы 405,
каждый класс должен реализовывать \Psr\Http\Server\MiddlewareInterface.
DEFAULT: []
Изменение страницы 405.
src/Project.php
new \Cekta\Framework\HTTP\Module(
handler_405: App\MyHandler405::class,
),
src/MyHandler405.php - расположить можно где угодно, следуйте psr-4.
<?php
declare(strict_types=1);
namespace App;
use Cekta\Framework\HTTP\Response\JSONFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final readonly class MyHandler405 implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create([
'message' => 'new 405 handler',
...$request->getAttributes(),
],
405
);
}
}
Перезапустим (build и перезапуск app):
make restart
Откроем 405 страницу, например с помощью curl.
curl --request POST http://localhost:8080
Увидим обновленную 405 страницу.
CLI Управление командами
Для работы с CLI на текущий момент используется symfony/console, вы можете использовать документацию для этого компонента
!!!Предупреждение!!!
На текущий момент для работы с cli командами используется symfony/console это практически стандарт де факто.
Мне не нравится эта реализация по следующим причинам:
- Внутри команд каждый раз работа с reflection
- Есть большое количество способов сделать одно и тоже разными путями для поддержки обратной совместимости (legacy).
Возможно в будущем реализация изменится и это потребует вам актуализировать, но пока we have what we have.
Добавление вашей команды
- Создайте класс расширяющий
\Symfony\Component\Console\Command\Command - Реализуйте метод
protected function execute(InputInterface $input, OutputInterface $output): int - Используйте php attribute
\Symfony\Component\Console\Attribute\AsCommandчтобы указать имя команды. - Обновите конфигурацию
make build # или make restart - Ваша команда доступна внутри app
make shell ./app.php
В SKELETON идет пример команды
Используйте оффицальную документацию по symfony/console component