Add knowledge base
This commit is contained in:
14
.claude/INDEX.md
Normal file
14
.claude/INDEX.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Pinecore Knowledge Base
|
||||
|
||||
Детальная документация по реализации. См. также [CLAUDE.md](../CLAUDE.md) для базового обзора.
|
||||
|
||||
## Разделы
|
||||
|
||||
- [Архитектура и жизненный цикл запроса](kb/architecture.md)
|
||||
- [Ядро, Config, Environment, ContainerFactory](kb/bootstrap.md)
|
||||
- [HTTP слой](kb/http.md)
|
||||
- [Worker и запуск](kb/worker.md)
|
||||
- [Аутентификация (JWT)](kb/auth.md)
|
||||
- [Логирование](kb/logging.md)
|
||||
- [ORM (MongoDB)](kb/orm.md)
|
||||
- [Console](kb/console.md)
|
||||
59
.claude/kb/architecture.md
Normal file
59
.claude/kb/architecture.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Архитектура и жизненный цикл запроса
|
||||
|
||||
## Общая схема
|
||||
|
||||
```
|
||||
HTTP → Caddy (:80) → /worker.php → FrankenPHP worker loop
|
||||
Kernel::boot($basePath) [один раз при старте]
|
||||
→ Environment::detect() // читает APP_ENV, дефолт 'dev'
|
||||
→ Config::load($configDir) // config/*.php + config/env/{env}.php + config/env/local.php
|
||||
→ ContainerFactory::build() // PHP-DI, в prod компилируется в var/cache/prod/
|
||||
loop (WorkerRunner::run()):
|
||||
frankenphp_handle_request(fn() =>
|
||||
HttpApplication::handleRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER)
|
||||
→ Request::fromGlobals() // парсит метод, путь, заголовки, JSON body
|
||||
→ OPTIONS? → CORS headers + 204 // preflight, без роутинга
|
||||
→ Router::match(method, path) // regex-компиляция {param} → именованные группы
|
||||
→ 404 / 405+Allow / RouteMatch
|
||||
→ MiddlewarePipeline::run() // каждый middleware меняет Request через withContext()
|
||||
→ $controller($request): Response // DI-resolved invokable
|
||||
→ $response->withHeaders(corsHeaders())->emit()
|
||||
)
|
||||
→ HttpApplication::terminate() // хук для закрытия ресурсов (сейчас пустой)
|
||||
→ gc_collect_cycles()
|
||||
→ если !$keepRunning или достигнут MAX_REQUESTS — выход из цикла
|
||||
```
|
||||
|
||||
## Карта компонентов
|
||||
|
||||
| Файл | Класс | Роль |
|
||||
|---|---|---|
|
||||
| `src/Kernel.php` | `Kernel` | Статический bootstrap, хранит контейнер |
|
||||
| `src/Environment.php` | `Environment` | Читает `APP_ENV`, метод `isProd()` |
|
||||
| `src/Config.php` | `Config` | Загружает `config/*.php`, deep-merge с env-оверрайдами; `get('a.b.c')` |
|
||||
| `src/ContainerFactory.php` | `ContainerFactory` | Строит PHP-DI контейнер; в prod включает compilation |
|
||||
| `src/Http/WorkerRunner.php` | `WorkerRunner` | Цикл FrankenPHP, MAX_REQUESTS, gc |
|
||||
| `src/Http/HttpApplication.php` | `HttpApplication` | CORS, роутинг, dispatch, обработка исключений |
|
||||
| `src/Http/Request.php` | `Request` | Иммутабельный; `body()`, `get(key)`, `withContext()` |
|
||||
| `src/Http/Response.php` | `Response` | `json()`, `error()`, `withHeader()`, `emit()` |
|
||||
| `src/Http/Router.php` | `Router` | Regex-компиляция маршрутов, возвращает `RouteMatch` |
|
||||
| `src/Http/MiddlewarePipeline.php` | `MiddlewarePipeline` | Последовательно прогоняет middleware через DI |
|
||||
| `src/Http/RouteDefinition.php` | `RouteDefinition` | method, path, controller, middleware[] |
|
||||
| `src/Http/HttpException.php` | `HttpException` | Выбрасывается из любого места → Response::error |
|
||||
| `src/Auth/AuthMiddleware.php` | `AuthMiddleware` | Bearer JWT → AuthContext в request |
|
||||
| `src/Auth/JwtService.php` | `JwtService` | HS256, issue/verify, lcobucci/jwt |
|
||||
| `src/Log/StdoutLogger.php` | `StdoutLogger` | JSON → stdout, PSR-3 |
|
||||
| `src/Log/FileLogger.php` | `FileLogger` | JSON → файл (если LOG_FILE задан) |
|
||||
| `src/Orm/AbstractMongoRepository.php` | `AbstractMongoRepository` | persist/findById/delete/findOneWhere |
|
||||
| `src/Orm/MongoHydrator.php` | `MongoHydrator` | hydrate/dehydrate, поддержка Embedded/EmbeddedList |
|
||||
| `src/ExceptionHandler.php` | `ExceptionHandler` | Ловит Throwable вне dispatch (critical лог) |
|
||||
|
||||
## Обработка исключений в dispatch
|
||||
|
||||
В `HttpApplication::dispatch()` ловятся:
|
||||
- `AuthException` → 401
|
||||
- `HttpException` → код из исключения
|
||||
- `\JsonException` → 400 (невалидный JSON body)
|
||||
- `\Throwable` → логируется, 500
|
||||
|
||||
Исключения, вылетающие **за пределы** `handleRequest()` (т.е. до/после dispatch), ловит `ExceptionHandler` в `WorkerRunner`.
|
||||
48
.claude/kb/auth.md
Normal file
48
.claude/kb/auth.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Аутентификация (JWT)
|
||||
|
||||
## Компоненты
|
||||
|
||||
| Файл | Класс | Роль |
|
||||
|---|---|---|
|
||||
| `src/Auth/JwtService.php` | `JwtService` | Выдача и верификация JWT (HS256, lcobucci/jwt) |
|
||||
| `src/Auth/AuthMiddleware.php` | `AuthMiddleware` | Bearer → JwtService → UserProvider → AuthContext |
|
||||
| `src/Auth/AuthContext.php` | `AuthContext` | Обёртка над объектом пользователя |
|
||||
| `src/Auth/UserProviderInterface.php` | `UserProviderInterface` | Контракт: `findById(string): ?object` |
|
||||
| `src/Auth/AuthException.php` | `AuthException` | → 401 в HttpApplication::dispatch() |
|
||||
|
||||
## JwtService (`src/Auth/JwtService.php`)
|
||||
|
||||
Конфигурируется через Config:
|
||||
- `jwt.secret` — HMAC-ключ (обязателен, иначе RuntimeException при старте)
|
||||
- `jwt.access_ttl` — TTL в секундах
|
||||
|
||||
```php
|
||||
$jwt->issue(string $userId): string // sub = $userId, iat, exp
|
||||
$jwt->verify(string $tokenString): string // возвращает sub (userId), бросает AuthException
|
||||
```
|
||||
|
||||
Использует `lcobucci/jwt`, HS256, `StrictValidAt` (проверяет exp с текущим временем).
|
||||
|
||||
## AuthMiddleware (`src/Auth/AuthMiddleware.php`)
|
||||
|
||||
1. Читает заголовок `authorization` (lowercase в Request)
|
||||
2. Ожидает формат `Bearer <token>`
|
||||
3. `JwtService::verify()` → `userId`
|
||||
4. `UserProviderInterface::findById($userId)` → user object
|
||||
5. `null` → `AuthException('User not found')`
|
||||
6. Возвращает `$request->withContext('auth', new AuthContext($user))`
|
||||
|
||||
```php
|
||||
// В контроллере:
|
||||
$auth = $request->get('auth'); // AuthContext
|
||||
$user = $auth->user; // объект пользователя (тип зависит от приложения)
|
||||
```
|
||||
|
||||
## Подключение в приложении
|
||||
|
||||
В `config/services.php` нужно забиндить `UserProviderInterface`:
|
||||
```php
|
||||
UserProviderInterface::class => fn($c) => $c->get(UserRepository::class),
|
||||
```
|
||||
|
||||
`UserRepository` должен реализовывать `UserProviderInterface` (`findById(string $id): ?object`).
|
||||
56
.claude/kb/bootstrap.md
Normal file
56
.claude/kb/bootstrap.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Ядро, Config, Environment, ContainerFactory
|
||||
|
||||
## Kernel (`src/Kernel.php`)
|
||||
|
||||
Статический класс, хранит единственный инстанс контейнера.
|
||||
|
||||
```php
|
||||
Kernel::boot(string $basePath): Container // ENV → Config → ContainerFactory
|
||||
Kernel::container(): Container // бросает RuntimeException если не вызван boot()
|
||||
```
|
||||
|
||||
## Environment (`src/Environment.php`)
|
||||
|
||||
```php
|
||||
Environment::detect(): self // getenv('APP_ENV') ?: 'dev'
|
||||
$env->name(): string
|
||||
$env->is(string $name): bool
|
||||
$env->isProd(): bool // name === 'prod'
|
||||
```
|
||||
|
||||
## Config (`src/Config.php`)
|
||||
|
||||
**Загрузка** (`Config::load($configDir, $env)`):
|
||||
1. Все `$configDir/*.php` — ключ = имя файла без расширения
|
||||
2. Deep-merge `$configDir/env/{envName}.php` (если есть)
|
||||
3. Deep-merge `$configDir/env/local.php` (если есть, всегда поверх)
|
||||
|
||||
**Доступ:**
|
||||
```php
|
||||
$config->get('section.key', $default) // dot-notation, deep lookup
|
||||
$config->get('app.cors') // возвращает array
|
||||
```
|
||||
|
||||
Merge: рекурсивный для assoc-массивов, replace для списков (array_values === array).
|
||||
|
||||
## ContainerFactory (`src/ContainerFactory.php`)
|
||||
|
||||
```php
|
||||
ContainerFactory::build(Environment $env, Config $config, string $basePath): Container
|
||||
```
|
||||
|
||||
- Autowiring включён всегда
|
||||
- В prod: `$builder->enableCompilation($basePath . '/var/cache/prod/')`
|
||||
- Загружает `$basePath/config/services.php` — файл должен вернуть `callable($builder, $config, $basePath)`
|
||||
|
||||
**Пример `config/services.php`:**
|
||||
```php
|
||||
return function (ContainerBuilder $builder, Config $config, string $basePath): void {
|
||||
$builder->addDefinitions([
|
||||
UserProviderInterface::class => fn($c) => $c->get(UserRepository::class),
|
||||
Router::class => fn() => new Router([
|
||||
new RouteDefinition('GET', '/users/{id}', GetUserController::class, [AuthMiddleware::class]),
|
||||
]),
|
||||
]);
|
||||
};
|
||||
```
|
||||
64
.claude/kb/console.md
Normal file
64
.claude/kb/console.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Console
|
||||
|
||||
## Компоненты
|
||||
|
||||
| Файл | Класс | Описание |
|
||||
|---|---|---|
|
||||
| `src/Console/ConsoleApplication.php` | `ConsoleApplication` | Точка входа: парсит argv, матчит команду, запускает handler |
|
||||
| `src/Console/ConsoleRouter.php` | `ConsoleRouter` | Аналог HTTP Router для консольных команд |
|
||||
| `src/Console/ConsoleDefinition.php` | `ConsoleDefinition` | signature, handler class, description, options[] |
|
||||
| `src/Console/OptionDefinition.php` | `OptionDefinition` | name, description, default (false = флаг) |
|
||||
| `src/Console/ConsoleInput.php` | `ConsoleInput` | Распарсенные path params + options |
|
||||
| `src/Console/ConsoleOutput.php` | `ConsoleOutput` | Вывод, exit code |
|
||||
| `src/Console/ConsoleMatch.php` | `ConsoleMatch` | Результат матчинга (аналог RouteMatch) |
|
||||
|
||||
## ConsoleDefinition
|
||||
|
||||
```php
|
||||
new ConsoleDefinition(
|
||||
signature: 'users:create {role}', // {param} — path param из сигнатуры
|
||||
handler: CreateUserCommand::class,
|
||||
description: 'Create a new user',
|
||||
options: [
|
||||
new OptionDefinition('dry-run', 'Do not persist', false), // флаг (default=false)
|
||||
new OptionDefinition('email', 'User email', null), // опция (default=null)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Handler
|
||||
|
||||
Invokable-класс, резолвится через DI:
|
||||
```php
|
||||
final class CreateUserCommand
|
||||
{
|
||||
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
|
||||
{
|
||||
$role = $input->pathParams['role'];
|
||||
$dryRun = $input->option('dry-run'); // bool для флагов
|
||||
$email = $input->option('email'); // string|null для опций
|
||||
|
||||
$output->writeln('Done');
|
||||
$output->setExitCode(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
```php
|
||||
// bin/console
|
||||
Kernel::boot(__DIR__ . '/..');
|
||||
$app = Kernel::container()->get(ConsoleApplication::class);
|
||||
exit($app->run($argv));
|
||||
```
|
||||
|
||||
Команды: `bin/console <signature> [--option=value] [--flag]`
|
||||
Help: `bin/console help` или `bin/console help <signature>`
|
||||
|
||||
## Argv parsing
|
||||
|
||||
`ConsoleInput::parse($commandStr, $pathParams, $args, $options)`:
|
||||
- `$args` = `array_slice($argv, 2)` (после имени команды)
|
||||
- `--name=value` → опция
|
||||
- `--name` без `=` → флаг (true)
|
||||
70
.claude/kb/http.md
Normal file
70
.claude/kb/http.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# HTTP слой
|
||||
|
||||
## RouteDefinition (`src/Http/RouteDefinition.php`)
|
||||
|
||||
```php
|
||||
new RouteDefinition(
|
||||
method: 'GET',
|
||||
path: '/users/{id}',
|
||||
controller: GetUserController::class,
|
||||
middleware: [AuthMiddleware::class], // опционально
|
||||
)
|
||||
```
|
||||
|
||||
## Router (`src/Http/Router.php`)
|
||||
|
||||
- Компилирует `{param}` → именованные regex-группы `(?P<param>[^/]+)`
|
||||
- `match(method, path): RouteMatch`
|
||||
- Сначала проверяет все маршруты на совпадение пути
|
||||
- Если путь совпал, но метод нет → `RouteMatch::methodNotAllowed($allowedMethods)`
|
||||
- Если ничего → `RouteMatch::notFound()`
|
||||
|
||||
## MiddlewarePipeline (`src/Http/MiddlewarePipeline.php`)
|
||||
|
||||
Резолвит каждый middleware через DI и вызывает `process(Request): Request` последовательно.
|
||||
Middleware может изменять request через `withContext()` или бросать исключение.
|
||||
|
||||
**Интерфейс middleware:**
|
||||
```php
|
||||
interface MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request): Request;
|
||||
}
|
||||
```
|
||||
|
||||
## Request (`src/Http/Request.php`)
|
||||
|
||||
Иммутабельный объект. Конструктор принимает именованные параметры.
|
||||
|
||||
```php
|
||||
$request->method // 'GET', 'POST', ...
|
||||
$request->path // '/users/123'
|
||||
$request->query // $_GET
|
||||
$request->pathParams // ['id' => '123'] — устанавливается роутером
|
||||
$request->headers // lowercase-hyphen: 'content-type', 'authorization'
|
||||
$request->cookies
|
||||
$request->files
|
||||
$request->body() // decoded JSON array (только если Content-Type: application/json)
|
||||
$request->get('auth') // значение из context, установленного middleware
|
||||
$request->withContext('key', $value): self // иммутабельное добавление в context
|
||||
```
|
||||
|
||||
**Разбор заголовков:** `HTTP_*` из `$_SERVER` → lowercase-hyphen. `CONTENT_TYPE` тоже нормализуется.
|
||||
|
||||
## Response (`src/Http/Response.php`)
|
||||
|
||||
```php
|
||||
Response::json($data, $status = 200): self // Content-Type: application/json
|
||||
Response::error($message, $status): self // json(['error' => $message], $status)
|
||||
|
||||
$response->withHeader($name, $value): self
|
||||
$response->withHeaders($headers): self
|
||||
$response->emit(): void // http_response_code + headers + echo body
|
||||
```
|
||||
|
||||
## HttpException (`src/Http/HttpException.php`)
|
||||
|
||||
```php
|
||||
throw new HttpException('Forbidden', 403);
|
||||
// → перехватывается в HttpApplication::dispatch() → Response::error(message, code)
|
||||
```
|
||||
53
.claude/kb/logging.md
Normal file
53
.claude/kb/logging.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Логирование
|
||||
|
||||
## Классы
|
||||
|
||||
| Файл | Класс | Описание |
|
||||
|---|---|---|
|
||||
| `src/Log/StdoutLogger.php` | `StdoutLogger` | JSON → php://stdout |
|
||||
| `src/Log/FileLogger.php` | `FileLogger` | JSON → файл |
|
||||
| `src/Log/CompositeLogger.php` | `CompositeLogger` | Делегирует нескольким логгерам |
|
||||
| `src/Log/NullLogger.php` | `NullLogger` | Отбрасывает все сообщения |
|
||||
|
||||
Все реализуют `Psr\Log\LoggerInterface`.
|
||||
|
||||
## Формат вывода (StdoutLogger)
|
||||
|
||||
JSON-строка на каждое сообщение:
|
||||
```json
|
||||
{"ts":"2026-04-06T12:00:00.000Z","level":"error","channel":"app","message":"...","context":{...}}
|
||||
```
|
||||
|
||||
- `ts` — ISO 8601 с миллисекундами
|
||||
- `channel` — из `log.channel` в config (дефолт `app`)
|
||||
- `context` — сериализуется; `Throwable` → `{class, message, file, line, trace}`
|
||||
- PSR-3 интерполяция `{key}` из context работает для строк/scalar
|
||||
|
||||
## Конфигурация
|
||||
|
||||
| Env / Config key | Описание |
|
||||
|---|---|
|
||||
| `LOG_LEVEL` / `log.level` | Минимальный уровень (debug/info/notice/warning/error/critical/alert/emergency), дефолт `debug` |
|
||||
| `LOG_FILE` / `log.file` | Путь к файлу → `FileLogger` включается автоматически |
|
||||
| `log.channel` | Имя канала в выводе, дефолт `app` |
|
||||
|
||||
## Добавление бэкенда
|
||||
|
||||
В `config/services.php` в фабрике `LoggerInterface`:
|
||||
```php
|
||||
LoggerInterface::class => fn($c) => new CompositeLogger(array_filter([
|
||||
$c->get(StdoutLogger::class),
|
||||
$config->get('log.file') ? $c->get(FileLogger::class) : null,
|
||||
// добавить сюда
|
||||
])),
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
```php
|
||||
// Inject LoggerInterface через DI
|
||||
$this->logger->error('Something failed', [
|
||||
'exception' => $e,
|
||||
'path' => $request->path,
|
||||
]);
|
||||
```
|
||||
80
.claude/kb/orm.md
Normal file
80
.claude/kb/orm.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# ORM (MongoDB)
|
||||
|
||||
## Атрибуты
|
||||
|
||||
| Атрибут | Применяется к | Описание |
|
||||
|---|---|---|
|
||||
| `#[Collection(name: 'users')]` | класс entity | Имя коллекции MongoDB |
|
||||
| `#[Id]` | свойство | Кастомное поле `id` (строка), НЕ MongoDB `_id` |
|
||||
| `#[Field]` | свойство | Обычное поле; имя в MongoDB = имя свойства |
|
||||
| `#[Field(name: 'x')]` | свойство | Поле с другим именем в MongoDB |
|
||||
| `#[Embedded]` | свойство | Вложенный объект (не Entity, просто класс) |
|
||||
| `#[EmbeddedList]` | свойство | Массив вложенных объектов |
|
||||
| `#[ForEntity(Foo::class)]` | класс репозитория | Связывает репозиторий с entity |
|
||||
|
||||
## Объявление Entity
|
||||
|
||||
```php
|
||||
#[Collection(name: 'users')]
|
||||
final class User implements MongoEntity
|
||||
{
|
||||
public function __construct(
|
||||
#[Id] public readonly string $id,
|
||||
#[Field] public readonly string $email,
|
||||
#[Field] public readonly ?string $name = null,
|
||||
// массив string[] — BSONArray конвертируется автоматически
|
||||
#[Field] public readonly array $roles = [],
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
- `#[Id]` хранится в документе как поле `id` (не `_id`)
|
||||
- MongoDB `_id` остаётся нативным ObjectId, пинекор его игнорирует
|
||||
- `array` поля (в т.ч. `string[]`) — `BSONArray` конвертируется в PHP array прозрачно
|
||||
- Если `$id === ''` при `persist()`, генерируется новый ID через `IdGenerator`
|
||||
|
||||
## Объявление репозитория
|
||||
|
||||
```php
|
||||
#[ForEntity(User::class)]
|
||||
final class UserRepository extends AbstractMongoRepository
|
||||
{
|
||||
// Публичный типизированный метод — не override protect persist()
|
||||
public function save(User $user): User
|
||||
{
|
||||
return $this->persist($user);
|
||||
}
|
||||
|
||||
public function findByEmail(string $email): ?User
|
||||
{
|
||||
return $this->findOneWhere(['email' => $email]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AbstractMongoRepository (`src/Orm/AbstractMongoRepository.php`)
|
||||
|
||||
Инжектируется через DI: `Database`, `MongoHydrator`, `IdGenerator`.
|
||||
|
||||
| Метод | Описание |
|
||||
|---|---|
|
||||
| `persist(object $entity): object` | upsert по полю `id`; если `id === ''`, генерирует новый |
|
||||
| `findById(string $id): ?object` | поиск по полю `id` |
|
||||
| `findOneWhere(array $filter): ?object` | произвольный MongoDB filter |
|
||||
| `delete(string $id): void` | deleteOne по полю `id` |
|
||||
| `collection(string $entityClass): Collection` | MongoDB\Collection для entity |
|
||||
| `entityClass(): string` | читает `#[ForEntity]`, кешируется статически |
|
||||
|
||||
## MongoHydrator (`src/Orm/MongoHydrator.php`)
|
||||
|
||||
- `hydrate(string $class, array|BSONDocument $doc): object` — BSONDocument/array → typed entity через named constructor args
|
||||
- `dehydrate(object $entity): array` — entity → array для MongoDB
|
||||
- Рекурсивно обрабатывает `#[Embedded]` и `#[EmbeddedList]`
|
||||
- Embedded при dehydrate: сериализует все свойства ReflectionClass (без атрибутов)
|
||||
|
||||
## EntityMap (`src/Orm/EntityMap.php`)
|
||||
|
||||
Кеширует метаданные entity (reflection). `EntityMap::of(ClassName::class)` возвращает объект с:
|
||||
- `collectionName` — из `#[Collection]`
|
||||
- `fields` — список `FieldMetadata` (property, field name, флаги embedded)
|
||||
- `idField()` — `FieldMetadata` поля с `#[Id]`
|
||||
59
.claude/kb/worker.md
Normal file
59
.claude/kb/worker.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Worker и запуск
|
||||
|
||||
## Entrypoint (`worker.php` приложения)
|
||||
|
||||
FrankenPHP запускается как `frankenphp run --config Caddyfile` и сам стартует PHP-воркеры.
|
||||
|
||||
```php
|
||||
use Pronchev\Pinecore\Kernel;
|
||||
use Pronchev\Pinecore\Http\WorkerRunner;
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
Kernel::boot(__DIR__);
|
||||
Kernel::container()->get(WorkerRunner::class)->run();
|
||||
```
|
||||
|
||||
`WorkerRunner` резолвится через DI autowiring — конфигурировать не нужно.
|
||||
|
||||
## WorkerRunner (`src/Http/WorkerRunner.php`)
|
||||
|
||||
```php
|
||||
final class WorkerRunner
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpApplication $app,
|
||||
private readonly ExceptionHandler $exceptionHandler,
|
||||
) {}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$maxRequests = (int) ($_SERVER['MAX_REQUESTS'] ?? 0); // 0 = без лимита
|
||||
|
||||
for ($n = 0; !$maxRequests || $n < $maxRequests; ++$n) {
|
||||
$keepRunning = frankenphp_handle_request(function (): void {
|
||||
try {
|
||||
$this->app->handleRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
|
||||
} catch (\Throwable $e) {
|
||||
$this->exceptionHandler->handleException($e); // critical лог
|
||||
}
|
||||
});
|
||||
|
||||
$this->app->terminate(); // хук: закрытие ресурсов (сейчас пустой)
|
||||
gc_collect_cycles();
|
||||
|
||||
if (!$keepRunning) break; // FrankenPHP сигнализирует об остановке
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Env-переменные
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `APP_ENV` | `dev` | Среда исполнения |
|
||||
| `MAX_REQUESTS` | `0` | Лимит запросов на воркер (0 = без лимита) |
|
||||
| `LOG_LEVEL` | `debug` | Минимальный уровень логирования |
|
||||
| `LOG_FILE` | — | Путь к файлу лога (активирует FileLogger) |
|
||||
| `JWT_SECRET` | обязателен | HMAC-ключ для JWT |
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
.idea
|
||||
/vendor/
|
||||
var/
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
121
CLAUDE.md
121
CLAUDE.md
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
Guidance for Claude Code when working with this repository.
|
||||
|
||||
## Package
|
||||
|
||||
@@ -10,117 +10,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
No lint or test commands configured yet.
|
||||
|
||||
## Architecture
|
||||
## Knowledge base
|
||||
|
||||
**Request lifecycle:**
|
||||
Detailed implementation docs: [.claude/INDEX.md](.claude/INDEX.md)
|
||||
|
||||
```
|
||||
HTTP → Caddy (:80) → /worker.php → FrankenPHP worker loop
|
||||
Kernel::boot() [once on startup]
|
||||
→ Environment::detect() → Config::load() → ContainerFactory::build()
|
||||
loop:
|
||||
→ HttpApplication::handleRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER)
|
||||
→ Request::fromGlobals()
|
||||
→ OPTIONS? → CORS headers + 204 (no routing)
|
||||
→ Router::match() → 404 / 405 / RouteMatch
|
||||
→ MiddlewarePipeline::run()
|
||||
→ Controller::__invoke(Request): Response
|
||||
→ Response + CORS headers → emit()
|
||||
→ Application::terminate()
|
||||
→ gc_collect_cycles()
|
||||
```
|
||||
|
||||
**Key components:**
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `src/Kernel.php` | One-time bootstrap |
|
||||
| `src/Http/` | `HttpApplication`, `WorkerRunner`, `Request`, `Response`, `Router`, `RouteDefinition`, `MiddlewarePipeline`, `HttpException` |
|
||||
| `src/Console/` | `ConsoleApplication`, `ConsoleRouter`, `ConsoleInput`, `ConsoleOutput`, `ConsoleDefinition` |
|
||||
| `src/Auth/` | `JwtService`, `AuthMiddleware`, `AuthContext`, `AuthException`, `UserProviderInterface` |
|
||||
| `src/Log/` | `StdoutLogger`, `FileLogger`, `CompositeLogger`, `NullLogger` |
|
||||
| `src/Orm/` | `AbstractMongoRepository`, `MongoHydrator`, `EntityMap`, `IdGenerator`, attributes |
|
||||
| `src/Model/` | Marker interfaces: `Entity`, `MongoEntity`, `SqlEntity`, `Dto` |
|
||||
| `src/ExceptionHandler.php` | Catches `Throwable` escaping the worker loop |
|
||||
| `src/Config.php` | Static config loader: `Config::get('section.key')` |
|
||||
| `src/ContainerFactory.php` | Builds PHP-DI container from `config/services.php` |
|
||||
|
||||
## Worker entrypoint
|
||||
|
||||
FrankenPHP запускается бинарником (`frankenphp run --config Caddyfile`), который сам стартует PHP-воркеры. `worker.php` приложения должен:
|
||||
|
||||
```php
|
||||
use Pronchev\Pinecore\Kernel;
|
||||
use Pronchev\Pinecore\Http\WorkerRunner;
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
Kernel::boot(__DIR__);
|
||||
Kernel::container()->get(WorkerRunner::class)->run();
|
||||
```
|
||||
|
||||
`WorkerRunner` резолвится через DI autowiring, конфигурировать не нужно.
|
||||
`MAX_REQUESTS` env var ограничивает число запросов на воркер (0 = без лимита).
|
||||
|
||||
## HTTP
|
||||
|
||||
```php
|
||||
// RouteDefinition: method, path, controller class, middleware array
|
||||
new RouteDefinition('GET', '/users/{id}', GetUserController::class, [AuthMiddleware::class])
|
||||
|
||||
// Controller — invokable, resolved via DI
|
||||
final class GetUserController {
|
||||
public function __invoke(Request $request): Response {
|
||||
$id = $request->pathParams['id']; // path param
|
||||
$user = $request->get('auth')->user; // from AuthMiddleware
|
||||
return Response::json([...]);
|
||||
}
|
||||
}
|
||||
|
||||
// Throw from anywhere — caught centrally
|
||||
throw new HttpException('Forbidden', 403);
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
`AuthMiddleware` requires `UserProviderInterface` to be bound in DI:
|
||||
```php
|
||||
// UserProviderInterface: findById(string $id): object
|
||||
// In app's config/services.php:
|
||||
UserProviderInterface::class => fn($c) => $c->get(UserRepository::class),
|
||||
```
|
||||
|
||||
`JwtService::issue(string $userId): string` — issues a JWT. Accepts only a string ID, no dependency on app models.
|
||||
|
||||
## Logging
|
||||
|
||||
```php
|
||||
// Inject LoggerInterface via DI constructor
|
||||
$this->logger->error('Something failed', ['exception' => $e, 'path' => $request->path]);
|
||||
```
|
||||
|
||||
Output: JSON to stdout — `ts`, `level`, `channel`, `message`, `context`.
|
||||
Level: `LOG_LEVEL` env var (default `debug`).
|
||||
File logging: set `LOG_FILE=/path/to/file.log` — `FileLogger` activates automatically.
|
||||
To add a backend: extend the `array_filter([...])` in the `LoggerInterface` factory in `config/services.php`.
|
||||
|
||||
## ORM
|
||||
|
||||
Entities implement `MongoEntity`, use PHP 8 attributes:
|
||||
```php
|
||||
#[Collection(name: 'users')]
|
||||
final class User implements MongoEntity {
|
||||
public function __construct(
|
||||
#[Id] public readonly string $id,
|
||||
#[Field] public readonly string $email,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
Repository extends `AbstractMongoRepository`, declare `#[ForEntity(Foo::class)]` on the class.
|
||||
Implement typed `save(Foo $e): Foo { return $this->persist($e); }` — not an override of the protected `persist()`.
|
||||
`#[Id]` maps to a custom field (`id`), not MongoDB's `_id` (which remains a native ObjectId).
|
||||
`array` fields (e.g. `string[]`) are hydrated automatically — BSONArray is converted transparently.
|
||||
- [Architecture & request lifecycle](.claude/kb/architecture.md)
|
||||
- [Bootstrap: Kernel, Config, Environment, ContainerFactory](.claude/kb/bootstrap.md)
|
||||
- [HTTP layer: Request, Response, Router, Middleware](.claude/kb/http.md)
|
||||
- [Worker entrypoint & WorkerRunner](.claude/kb/worker.md)
|
||||
- [Auth: JWT, AuthMiddleware](.claude/kb/auth.md)
|
||||
- [Logging](.claude/kb/logging.md)
|
||||
- [ORM: MongoDB, entities, repositories](.claude/kb/orm.md)
|
||||
- [Console](.claude/kb/console.md)
|
||||
|
||||
## Code Style
|
||||
|
||||
|
||||
Reference in New Issue
Block a user