CEKTA FRAMEWORK
Это набор готовых к использованию инструментов для удобного создания приложений на PHP, например API.
Основные задачи фреймворка:
- Простой запуск проекта.
- Удобное использование проекта и расширение его возможностей.
- Оптимальная продуктовая сборка готовая к deploy.
Запуск проекта
Создание проекта
docker run \
--rm \
-it \
--volume $PWD:/app \
--user $(id -u):$(id -g) \
composer \
create-project \
-s dev \
--ignore-platform-reqs \
cekta/skeleton \
{project_name}
Создает начальную структуру проекта.
cd {project_name}
Переходим в папку с проектом.
Запуск окружения разработки.
make dev
После запуска команды, можно проверить работу http://localhost:8080
Shell приложения.
make shell
Позволяет открыть консоль app сервера, например можно подключать новые пакеты composer require {package} или любое
управление проектом.
make - команды делается на host машине!!!.
composer - внутри shell application, имеются полезные пользовательские скрипты.
./app.php - внутри shell application, основная точка входа.
Использование проекта
Вы можете создать HTTP обработчик, а также cli command или любой другой раздел данной документации.
make build
Не забываем делать данную команду, чтобы применить изменения в проекте.
Production сборка
make image
Собирает релизный docker image, который можно отправить в docker registry для последующего разворачивания на серверах.
Если вам не нравится docker, можно использовать инструкции по сборке и разворачивать на bare metal, это не принципиально
Пример запуска production сборки
docker run -p 8090:8080 --rm cekta-app:latest
Позволяете запустить приложение в любом месте, главно доставить туда docker image.
Параметры приложения можно переопределять с помощью env во время запуска.
Доставка, а также администрирование зависимостей для production это ваша зона ответственности.
Создание своего HTTP обработчика.
Создание HTTP обработчиков это одна из основных задач разработчика API.
пример обработчика который обработает GET /example и выведет json {"message": "this is example}.
src/Example.php - расположить можно где угодно, главное следовать psr4.
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('/example')]
final readonly class Welcome implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create(['message' => 'this is example']);
}
}
CLI: нужно сделать build или полный перезапуск
make restart
Проверяем успешный результат открываем GET http://localhost:8080/example.
Зависимости внедрять через autowiring в конструктор, они будут подгружаться автоматически, необходимые параметры будут запрошены во время build.
Алгоритм в общем виде.
- Создайте класс (в любом месте) реализующий \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проекта илиrestartmake restart - Можно открывать endpoint с указанным method и pattern.
Обработчик и параметры в url
Обработчик и параметры в 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.
Вы можете указывать свои регулярные выражения, если ничего не укажите то [^/]+ (любое значение, кроме /).
Middlewares для маршрута
Можно регистрировать middlewares во время регистрации маршрута, эти middlewares будут вызываться каждый раз при обработке маршрута.
Например, логировать обращения или делать другую типовую работу.
src/ExampleMiddleware.php - расположить можно где угодно, главное следовать psr4.
<?php
declare(strict_types=1);
namespace App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
final readonly class ExampleMiddleware implements MiddlewareInterface
{
public function __construct(private LoggerInterface $logger)
{
}
/**
* @inheritDoc
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->logger->info('you can log request');
$response = $handler->handle($request);
$this->logger->info('you can log response');
return $response;
}
}
В php attribute маршрутов, есть параметр отвечающий за middlewares, можно там передавать необходимые middlewares.
// ...
#[Route\GET(
pattern: '/',
middlewares: [
\App\ExampleMiddleware::class
]
)]
// ...
полный пример:
src/Welcome.php
<?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(
pattern: '/',
middlewares: [
\App\ExampleMiddleware::class
]
)]
final readonly class Welcome implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create(['message' => 'welcome to cekta']);
}
}
для применения изменений необходимо сделать build или restart
make restart
Теперь открывая маршруты будет вызываться наш middleware.
Middlewares для маршрута
Можно регистрировать middlewares для всех http маршрутов приложения.
Например, логировать обращения или делать другую типовую работу.
src/ExampleMiddleware.php - расположить можно где угодно, главное следовать psr4.
<?php
declare(strict_types=1);
namespace App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
final readonly class ExampleMiddleware implements MiddlewareInterface
{
public function __construct(private LoggerInterface $logger)
{
}
/**
* @inheritDoc
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->logger->info('you can log request');
$response = $handler->handle($request);
$this->logger->info('you can log response');
return $response;
}
}
В файле src/Application.php при создании модуля \Cekta\Framework\HTTP\Module()
можно указать имена middlewares для страницы 405, через параметр middlewares.
// ...
new \Cekta\Framework\HTTP\Module(
middlewares: [
\App\ExampleMiddleware::class,
),
// ...
для применения изменений необходимо сделать build или restart
make restart
Теперь открывая любой маршрут будет вызываться на middleware.
Управление обработчиком страницы 404.
Вы можете сделать свою 404 страницу, для этого необходимо:
- Создать обработчик (класс реализующий
ServerRequestHandler). - Использовать его для страницы 404.
src/Example404.php - расположить можно где угодно, главное следовать psr4.
<?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 Example404 implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create(['my custom page 404']);
}
}
В файле src/Application.php при создании модуля \Cekta\Framework\HTTP\Module()
можно задать обработчик для страницы 404 через параметр handler_404.
// ...
new \Cekta\Framework\HTTP\Module(
handler_404: \App\Example404::class,
),
// ...
Перезапустим (минимум build) чтобы изменения вступили в силу.
make restart
Теперь для обработки 404 страницы будет вызываться наш обработчик с json сообщением my custom page 404.
Middlewares для страницы 404
Вы можете задавать middlewares для страницы 404.
Для этого необходимо:
- Создать middleware или использовать существующую реализацию
\Psr\Http\Server\MiddlewareInterface. - Указать middlewares которые должны использоваться.
src/Middleware404.php расположить можно где угодно, главное следовать psr4.
<?php
declare(strict_types=1);
namespace App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Middleware404 implements MiddlewareInterface
{
/**
* @inheritDoc
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// you can handle request here!!!
return $handler->handle($request);
}
}
В файле src/Application.php при создании модуля \Cekta\Framework\HTTP\Module()
можно указать имена middlewares для страницы 404, через параметр middlewares_404.
// ...
new \Cekta\Framework\HTTP\Module(
middlewares_404: [
\App\Example404::class,
]
),
// ...
Перезапустим (минимум build) чтобы изменения вступили в силу.
make restart
Теперь при появлении страницы 404, будут вызываться указанные middlewares.
Управление обработчиком страницы 405.
Вы можете сделать свою 405 страницу, для этого необходимо:
- Создать обработчик (класс реализующий
ServerRequestHandler). - Использовать его для страницы 405.
src/Example405.php - расположить можно где угодно, главное следовать psr4.
<?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 Example405 implements RequestHandlerInterface
{
public function __construct(
private JSONFactory $factory
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->factory->create([
'message' => 'my custom page 405',
...$request->getAttributes(),
]);
}
}
В файле src/Application.php при создании модуля \Cekta\Framework\HTTP\Module()
можно задать обработчик для страницы 405 через параметр handler_405.
// ...
new \Cekta\Framework\HTTP\Module(
handler_405: \App\Example405::class,
),
// ...
Перезапустим (минимум build) чтобы изменения вступили в силу.
make restart
Теперь для обработки 405 страницы будет вызываться новый обработчик, с новым сообщением и деталями ошибки.
Middlewares для страницы 405
Вы можете задавать middlewares для страницы 405.
Для этого необходимо:
- Создать middleware или использовать существующую реализацию
\Psr\Http\Server\MiddlewareInterface. - Указать middlewares которые должны использоваться.
src/Middleware405.php расположить можно где угодно, главное следовать psr4.
<?php
declare(strict_types=1);
namespace App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Middleware405 implements MiddlewareInterface
{
/**
* @inheritDoc
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// you can handle request here!!!
return $handler->handle($request);
}
}
В файле src/Application.php при создании модуля \Cekta\Framework\HTTP\Module()
можно указать имена middlewares для страницы 405, через параметр middlewares_405.
// ...
new \Cekta\Framework\HTTP\Module(
middlewares_405: [
\App\Example405::class,
]
),
// ...
Перезапустим (минимум build) чтобы изменения вступили в силу.
make restart
Теперь при появлении страницы 405, будут вызываться указанные middlewares.
Универсальный способ внедрения зависимостей.
Внедрение зависимостей это не только код, но и пакетный менеджер, наличие запущенных нужных сервисов (db, minio, kafka и тд), различные extension (pdo, mbstring, sockets и тд) и установленный софт и системные библиотеки.
У команды есть свои компетенции (экспертиза) и предпочтения.
Если команда работала с очередями в kafka (pull модель), им потребуется время для переключения на rabbitmq (push модель), mysql или postgres и так далее.
Команды могут выбирать технологии даже если отсутствует компетенция, но есть желание ее получить.
Специфика проекта индивидуальна, если вы строите OLAP систему с аналитическими запросами, использовать ORM не лучшая идея даже при наличии компетенций в ORM.
Все эти требования со временем могут меняться, например проект начинался с memcached, потом перешли на redis, потом перешли на dragonfly или valkey и тд.
Эти изменения постоянный процесс развития проекта и необходимо предоставить возможность делать это удобно и предсказуемо.
Конкретные примеры внедрений зависимостей, для примера:
- Простейший случай установки зависимости (guzzlehttp/guzzle).
- Использование абстракции и регистрация реализации(psr/http-client).
- Зависимости с дополнительными сервисами (postgres).
- Использование
Module(migration для db).
Это универсальный алгоритм следуя которыми можно внедрить любую зависимость:
- Определяемся с целями.
- Настрайваем docker image.
- Настрайваем сервисы для разработки.
- Обновляем окружение разработки и запускаем его
- Устанавливаем необходимые пакеты.
- Подключаем новые модули или настрайваемые существующие.
- Используем зависимости в своем коде.
- Настрайваем
App\Module. - build или restart окружения.
- Проверяем результат.
В зависимости от ситуации, некоторые пункты могут быть пропущены.
Подробней о некоторых пунктах:
1. Определяемся с целями.
Вы определяетесь с тем что вам необходимо и каких версий, как это будет на этапе разработки или в production.
Во время разработки можно запускать свои сервисы (db, minio и тд), а можно по сети подключаться к серверам разработки расположенным в интернете (s3, db и тд), по возможности предпочитайте первый вариант.
Как приложение будет в production режиме работать, с какими сервисами будет взаимодействовать, старайтесь чтобы версии совпадали и тд.
2. Настрайваем docker image.
Основная работа с файлом Dockerfile в котором перечислены различные images.
base image будет установлено во все другие image, например там можно установить php extension (sockets, pdo и тд) которые используются в других окружениях. Можно установить пакеты (библиотеки) для alpine которые могут понадобиться вашему приложению.
dev image то что нужно на этапе разработки, это может быть пакетный менеджер composer, xdebug extension, pcov extension, make и другие вещи полезные в разработке.
prod image здесь можно настроить инструменты в production, обновить конфигурационные файлы для production, включить opcache, настроить логирование в json и тд.
3. Настрайваем сервисы для разработки.
Основная работа с docker-compose.yml, сервисы запускаем у разработчика на устройстве.
Например это могут быть: postgres, mysql, minio(s3), redis, memcached, opensearch, clickhouse, kafka или rabbitmq и тд.
Помимо указания самих сервисов, здесь можно их настроить (логины и пароли), а также указать зависимости между ними.
4. Обновляем окружение разработки и запускаем его.
Обновляем окружение разработки (останавливаем если запущенно, удаляем то что было, собираем актуальную версию)
make refresh
Запускаем собранную версию.
make dev
5. Устанавливаем необходимые пакеты.
Используя composer управляем пакетами
Делаем все команды внутри app shell, чтобы окружения было одинаковым.
make shell
Устанавливаем пакет для разработки и production.
composer require vendor/package[:version]
Устанавливаем пакет только для разработки
composer require --dev vendor/package[:version]
6. Подключаем новые модули или настрайваемые существующие.
Основная работа с src/Application.php.
После установки пакетов, могут быть Module которые помогают с конфигурацией DI и выполнением необходимой интеграцией
в ваше приложение.
Например, поиск всех классов реализующих интерфейсов (вне зависимости от их расположения в директориях), работа с php attribute (поиск http обработчиков, middlewares, cli commands, migrations и тд).
Некоторые модули могут иметь как обязательные аргументы которые потребуется задавать, например App\Module
зависит от текущих переменных окружения.
Некоторые модули могут иметь опциональные атрибуты, с помощью которых можно делать часто встречающиеся действия.
Например, переопределить обработчик страницы 404, возможность зарегистрировать произвольные CLI command, указать http
middlewares для всего приложения и тд.
7. Используем зависимости в своем коде.
Внедряем зависимости в свой код с помощью конструктора и используем их в прикладных целях.
8. Настрайваем App\Module.
Основная работа с src/Module.php.
Конфигурируем зависимости, задаем значения параметров обязательных или опциональных.
Оставляем возможность изменить параметры с помощью переменных окружений (environment) для devops.
Регистрируем имплементации для интерфейсов.
Управляем жизненным циклом зависимостей (scoped, singleton, factory).
9. build или restart окружения.
build
make build
В некоторых случаях build может быть неудачным, когда зависимости не настроены.
Например, не переданные обязательные поля или незарегистрированы интерфейсы, или абстрактные классы.
В сообщение с ошибкой указано какая именно зависимость не может быть разрешена и цепочка зависимостей.
restart
make restart
Останавливает текущую dev версию, запускает снова (build + запуск).
10. Проверяем результат.
Проверяем что все корректно работает в вашем коде, в обработчиках или консольных командах.
Полезные команды можно выносить в Makefile или composer scripts.
Makefile команды запускаются с хостовой системы.
composer scripts запускаются из shell app.
Important
Однажды настроенная зависимость может быть переиспользована в любом месте.
Простейший пример guzzlehttp/guzzle
В любом проекте, однажды возникает необходимость во взаимодействии с внешними сервисами, http самый распространенный протокол, давайте рассмотрим от самого простого случая к более сложным.
В мире PHP в качестве HTTP CLIENT используется guzzlehttp/guzzle, давайте рассмотрим пример его использования.
Данный пример внедрения можно рассматривать как простейший шаблон, меняя лишь целевой пакет и конфигурацию.
-
Устанавливаем необходимые пакеты.
composer require guzzlehttp/guzzle -
Используем зависимости в своем коде. src/Welcome2 - расположение файла может быть любым, главное следовать psr4.
<?php declare(strict_types=1); namespace App; use Cekta\Framework\HTTP\Route; use GuzzleHttp\Client; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; #[Route\GET('/w2')] final readonly class Welcome2 implements RequestHandlerInterface { public function __construct( private Client $client ) { } public function handle(ServerRequestInterface $request): ResponseInterface { return $this->client->get('https://api.restful-api.dev/objects/7'); } } -
build или restart окружения.
make restart -
Настрайваем
App\Module. (опционально)При создании клиента, можно передать config для настройки клиента, если мы хотим их переопределить необходимо сделать следующее:
src/Module - Конфигурация вашего приложения (onCreateParameters).
// ... public function onCreateParameters(mixed $cachedData): array { return [ \GuzzleHttp\Client::class . '$config' => [ // Ваше желаемая конфигурация 'timeout' => 2.0, ] ]; } // ...Запись вида
\GuzzleHttp\Client::class . '$config'читается как - для зависимости с именем\GuzzleHttp\Client::classв аргумент конструктора с именем$configотправить следующее указанное значение (массив с ключом timeout). -
Проверяем результат.
Откроем страницу http://localhost:8080/w2 и наш запрос проксируется во внешнюю заглушку, ответ из которой продемонстрируется нам.
Внедрение psr/http-client
Рассмотрим следующую ситуацию, вам необходимо взаимодействовать с внешним сервисом по HTTP, при этом вы не хотите использовать конкретную реализацию, а хотите использовать абстракцию psr/http-client.
Данный пример можно рассматривать как самый распространенный шаблон внедрения, меняя лишь названия пакетов и конфигурацию.
-
Устанавливаем необходимые пакеты.
composer require psr/http-client guzzlehttp/guzzle psr/http-factorypsr/http-client- нужная нам абстракция.psr/http-factory- позволит абстрактно создавать запросы.guzzlehttp/guzzle- конкретная реализация абстракций которую будем использовать. -
Используем зависимости в своем коде.
src/Welcome3 - расположение может быть любым, главное следовать psr4.
<?php declare(strict_types=1); namespace App; use Cekta\Framework\HTTP\Route; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; #[Route\GET('/w3')] final readonly class Welcome3 implements RequestHandlerInterface { public function __construct( private ClientInterface $client, private RequestFactoryInterface $factory, ) { } public function handle(ServerRequestInterface $request): ResponseInterface { return $this->client->sendRequest( $this->factory->createRequest('GET', 'https://api.restful-api.dev/objects/7') ); } } -
Настрайваем
App\Module.src/Module.php - в
onBuildDefinitionsукажем alias.// ... public function onBuildDefinitions(mixed $cachedData): array { return [ 'alias' => [ \Psr\Http\Client\ClientInterface::class => \GuzzleHttp\Client::class, \Psr\Http\Message\RequestFactoryInterface::class => \GuzzleHttp\Psr7\HttpFactory::class, // ... ], // ... ]; } // ...Читается как тем кто запрашивает
\Psr\Http\Message\RequestFactoryInterface::classпередавать зависимость\GuzzleHttp\Psr7\HttpFactory::class.
Кто запрашивает\Psr\Http\Client\ClientInterface::classпередавать зависимость\GuzzleHttp\Client::class. -
build или restart окружения.
make restart -
Проверяем результат
Открываем http://localhost:8080/w3 убеждаемся что все работает.
Внедрение db (pgsql/mysql/sqlite)
Одна из самых распространенных зависимостей это реляционная БД, которая может быть представлена различными решениями: pgsql, mysql, sqlite и тд.
Аналогичным способом можно внедрять:
- очереди (kafka, rabbitmq и тд),
- быстрые nosql базы (redis, dragonfly, valkey, memcached, mongadbи тд),
- поисковые движки (opensearch, elasticksearch, sphinx и тд),
- колоночные базы для аналитки (clickhouse и тд)
- файловые хранилища s3 (minio и тд).
Рассмотрим максимально простой вариант по настройке postgres.
-
Определяемся с целями.
- В разработке, у каждого разработчика свой сервис postgres:18.
- В production, пусть это будет managed кластер, нам предоставляют credentials для входа.
- Возможность изменять credential после продуктовой сборки.
- В коде внедряем PDO как зависимость.
- Для работы PDO требуется extension pdo, pdo-pgsql.
-
Настрайваем docker image.
Dockerfile
FROM php:8.4-cli-alpine AS base # ... RUN install-php-extensions \ pdo \ pdo_pgsql \ # ... # ...Устанавливать надо именно в base чтобы эти extension были доступны как в dev, так и prod сборке.
-
Настрайваем сервисы для разработки.
docker-compose.yml
services: app: # ... depends_on: db: condition: service_healthy db: image: postgres:18-alpine3.22 environment: - PGUSER=postgres # user for pg_isready - POSTGRES_PASSWORD=cekta ports: - "5432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready" ] interval: 10s timeout: 5s retries: 5Мы добавили новый сервис db, указали пароль
cekta, описали healthcheck, указали зависимость app от db. -
Обновляем окружение разработки и запускаем его
make refresh make dev -
Используем зависимости в своем коде.
src/Test.php
<?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('/test')] final readonly class Test implements RequestHandlerInterface { public function __construct( private JSONFactory $factory, private \PDO $pdo, ) { } public function handle(ServerRequestInterface $request): ResponseInterface { return $this->factory->create( $this->pdo->query('SHOW ALL') ->fetchAll(\PDO::FETCH_ASSOC) ); } }Он отображает текущие настройки postgres, так как для нормальной работы с бд надо инструмент миграций, для упрощения пока пропустим, сделаем это отдельно.
-
Настрайваем
App\Module.// ... public function onCreateParameters(mixed $cachedData): array { return [ \PDO::class . '$username' => $this->env['DB_USERNAME'] ?? 'postgres', \PDO::class . '$password' => $this->env['DB_PASSWORD'] ?? 'cekta', \PDO::class . '$dsn' => new \Cekta\DI\Lazy\Closure(function (ContainerInterface $c) { $host = $this->env['DB_HOST'] ?? 'db'; $db = $this->env['DB_NAME'] ?? 'postgres'; return "pgsql:host=$host;dbname=$db;"; }), ]; }Это production ready вариант конфигурации, чтобы была возможность изменять credential после релиза, при этом в разработке используются удобные параметры, небольшое объяснение:
PDO зависит от 4 аргументов (dsn, username, password, options), один из аргументов обязательный (dsn), остальные опциональные (имеют значения по умолчанию).
Итого нам необходимо определить 3 параметра: dsn, username, password.\PDO::class . '$username'- для зависимости с именем\PDO::classаргумент с именемusernameпередать следующее значение, которое указано справа.this->env['DB_USERNAME'] ?? 'postgres'- если в environment указано DB_USERNAME то используем его, в остальных случаях используем значение по умолчаниюpostgres.Обратите внимание на значение
dsn, если значение реализует интерфейсCekta\DI\Lazyзначит значение вычисляется после сборки в runtime, в данном случае значение генерируется callback функцией, которая генерирует dsn строку, на основе параметров из env или значений по умолчанию. -
build или restart окружения.
make restart -
Проверяем результат.
Открываем http://localhost:8080/test - Получим текущие настройки postgres.
Внедрение миграций (db migration)
При работе с БД, хорошей практикой считается фиксировать изменения базы данных в миграция (изменения структуры БД).
В PHP существуют различные инструменты для управления миграциями, например doctrine/migrations, но мы не будем его использовать.
Продемонстрируем работу с Module (модулями), для этого мы используем cekta/migrator.
-
Настройте подключение к БД.
-
Устанавливаем необходимые пакеты.
composer require cekta/migrator -
Подключаем новые модули или настрайваемые существующие.
src/Application.php добавим новый модуль в список.
// .. new \Cekta\Framework\CLI\Module( // существующий модуль, добавим команды. command_map: [ 'db:migrate' => \Cekta\Migrator\Command\Migrate::class, 'db:rollback' => \Cekta\Migrator\Command\Rollback::class, ]), new \Cekta\Migrator\Module(), // новый модуль // ...Мы к нашему приложению подключили новый модуль, а в модуль по работе с CLI добавили две новый команды, имя которым можно задавать произвольное.
-
Используем зависимости в своем коде.
src/InitMigration.php - располагать файл можно где угодно, главное следовать PSR4.
<?php declare(strict_types=1); namespace App; class InitMigration implements \Cekta\Migrator\Migration { public function __construct(private \PDO $pdo) { } public function up(): void { $this->pdo->exec('CREATE TABLE test1 (id int)'); } public function down(): void { $this->pdo->exec('DROP TABLE test1'); } /** * @inheritDoc */ public static function id(): int { return 1772793536; // unix timestamp, date +%s } } -
build или restart окружения.
make restart -
ППроверяем результат.
Выполним миграции.
./app.php db:migrateОткатим миграции.
./app.php db:rollback
Для удобства команды можно вынести в Makefile
Makefile
// ...
migrate:
docker compose run --rm -it app ./app.php db:migrate
rollback:
docker compose run --rm -it app ./app.php db:rollback
db:migrate можно добавить в entrypoint dev контейнера, чтобы выполнять ее автоматически при включении,
но это уже по вкусу.
CLI: Создание команды.
Вы можете создавать свои команды и использовать их в приложение.
src/HelloCommand.php - расположить можно где угодно, главное следовать psr4.
<?php
declare(strict_types=1);
namespace App;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('hello', 'description')]
class HelloCommand extends Command
{
/**
* @inheritDoc
*/
public function __construct(
private LoggerInterface $logger
) {
parent::__construct();
}
/**
* @inheritDoc
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->logger->info('log info message');
$output->writeln('hello world');
return Command::SUCCESS;
}
}
CLI: нужно сделать build или полный перезапуск
make restart
Проверим результат
make shell # переход в консоль приложения
./app.php hello
hello world
Зависимости внедрять с помощью __construct.
Для работы с CLI на текущий момент используется symfony/console, вы можете использовать документацию для этого компонента.
!!!Предупреждение!!!
На текущий момент для работы с cli командами используется symfony/console это практически стандарт де-факто.
Мне не нравится эта реализация по следующим причинам:
- Внутри команд каждый раз работа с reflection
- Есть большое количество способов сделать одно и тоже разными путями для поддержки обратной совместимости (legacy).
Возможно в будущем реализация изменится и это потребует вам актуализировать, но пока we have what we have.
Вступить в СЕКТУ
Если вам нравится проект, желаете стать “сектантом”, это может сделать любой человек, нужны не только разработчики
Чем можно помочь проекту:
- Разработка
- Написание документации
- Возвращать обратную связь
- Помощь с тестированием (ручным и автотесты).
- Внедрение в реальные проекты.
- Распространение. (популяризация)
Любому человеку желающему помочь не словом, а делом найдется, место.
Как “вступить в секту”:
- войти в чат https://t.me/dev_ru.
- Написать:
@KuvshinovEE хочу вступить в секту:) - следовать инструкциям от @KuvshinovEE (другие там насоветуют).
