Приложение: конфиг, discovery, события, консоль¶
Keel\App\App собирает приложение из каталога проекта по конвенциям. Всё остальное
(контейнер, роутер, ядро, рантаймы) можно использовать и без него.
Структура проекта¶
config/ *.php, каждый возвращает объект конфига или замыкание (Env, string $projectDir): object
src/ сканируется discovery: контроллеры, сервисы с атрибутами, слушатели, команды
public/index.php
bin/console
var/cache/<env>/ discovery.php, routes.php, container.php
.env необязательно
// public/index.php
exit(App::boot(dirname(__DIR__))->run());
// bin/console
exit(ConsoleFactory::create(App::boot(dirname(__DIR__)))->run());
App::boot($projectDir, ?Env $env, ?\Closure $configure):
Env: снимокgetenv(),$_ENV,$_SERVERплюс.env, если файл есть; реальное окружение имеет приоритет над файлом. Читается один раз на процесс. Переменные запроса из$_SERVER(HTTP_*,REDIRECT_*,REQUEST_URI,REMOTE_ADDR, ...) не берутся, если их нет вgetenv()или$_ENV: под php-fpm они описывают один запрос, а не процесс.config/*.php→ объекты, зарегистрированные в контейнере под своим классом. Известные ядру:AppConfig(env, debug, cacheDir),HttpConfig(глобальные middleware),DiscoveryConfig(каталоги сканирования, по умолчаниюsrc/),ServicesConfig(списокServiceProvider). Отсутствующие создаются с дефолтами.- Discovery (см. ниже), затем
Definitions:Env, все конфиги, провайдеры, сервисы с атрибутами,ListenerProvider/EventDispatcher, дефолты ядра, замыкание$configure. - Роутер: в debug строится из контроллеров на каждый запуск, в prod читается из
routes.php(создаётся при первом запуске). - Контейнер: в debug —
Containerна рефлексии, в prod — скомпилированный класс изcontainer.php(создаётся при первом запуске). Имя класса включает хэш проекта и окружения, поэтому несколько приложений уживаются в одном процессе. Kernelс middleware изHttpConfig.
Старт из кэша передаёт скомпилированному контейнеру только Env, объекты конфигов и
Router, поэтому Definitions::instance() в провайдере (или $configure) для чего-то ещё
сработал бы один раз и ронял бы каждый следующий запуск. Компиляция таких определений
отклоняется с ContainerException; соберите объект в статической фабрике
($d->set($id, Foo::class)->factory(FooFactory::class . '::create')) или верните его из файла
config/*.php.
В prod с готовыми кэшами старт идёт по короткому пути: конфиги, кэш роутов, скомпилированный
контейнер и список слушателей из кэша discovery. Definitions, сканер, discovery-классы и
рефлексия не загружаются вовсе; keel:compile собирает всё это заново отдельным процессом.
Кэши prod не обновляются сами: после деплоя выполните bin/console keel:compile или
удалите var/cache/<env>/. В debug кэшируется только discovery, с проверкой по числу
файлов и максимальному mtime (проход stat(); пока ничего не изменилось, файлы не читаются).
PHP не умеет перезагружать класс: если container.php переписан в процессе, который его уже
загрузил (App::compile() и следом App::boot(), долгоживущий worker), следующий запуск
сравнивает файл с CONTENT_HASH загруженного класса и бросает RuntimeException с просьбой
начать новый процесс вместо того, чтобы молча обслуживать старый контейнер.
Конфиг¶
Конфиг — обычный readonly-объект; сервисы получают его через конструктор по типу:
// config/mail.php
return static fn(Env $env): MailConfig => new MailConfig(dsn: $env->require('MAILER_DSN'));
final class Mailer { public function __construct(private MailerConfig $config) {} }
Env: get, require, bool (1/true/yes/on), int, has, with. Разбор .env:
KEY=value, кавычки одинарные и двойные (в двойных работают \"; значение в кавычках
заканчивается на закрывающей кавычке, всё после неё — комментарий), комментарии # после
значений без кавычек, без подстановки переменных.
Discovery¶
Discoverer сканирует только каталоги из DiscoveryConfig::$paths, никогда vendor/.
Файлы без #[ и без extends пропускаются до токенизации (наследник получает методы с
атрибутами от родителя, не повторяя их); имена классов берутся из токенов без загрузки
файла, рефлексия создаётся только для классов, которые умеет загрузить автозагрузчик
(один класс на файл по PSR-4). Класс, который не загружается — например, его родитель из
неустановленного пакета, — пропускается, а не роняет запуск. Кэш проверяется при
восстановлении: методы слушателей должны существовать, команды — наследовать Command,
а #[Listener(event: ...)] — называть существующий класс или интерфейс.
Встроенные discovery:
| Класс | Что ищет | Куда попадает |
|---|---|---|
RouteDiscovery |
классы с методами, помеченными #[Route]/#[Get]/… |
контроллеры роутера |
ServiceDiscovery |
классы с #[Alias], #[Tag], #[Lazy], #[Transient], #[Shared] |
Definitions::set(), чтобы алиасы и теги работали |
ListenerDiscovery |
публичные методы с #[Listener] |
ListenerProvider |
CommandDiscovery |
наследники Symfony\Component\Console\Command\Command с #[AsCommand] |
консоль |
Своя discovery: реализуйте DiscoveryInterface (discover(ReflectionClass), result(),
restore(array)) и добавьте в Discoverer::add().
События (PSR-14)¶
final class GreetingLog
{
#[Listener(priority: 10)]
public function onGreeted(Greeted $event): void { /* ... */ }
}
$this->events->dispatch(new Greeted($name)); // EventDispatcherInterface из контейнера
Тип события — тип первого параметра или #[Listener(event: Foo::class)]. Слушатели на
родительский класс или интерфейс получают и наследников. Порядок: приоритет по убыванию,
затем порядок регистрации. StoppableEventInterface останавливает цепочку. Объект-слушатель
создаётся контейнером при первом событии, поэтому у него могут быть зависимости.
Ядро и рантаймы рассылают через ту же шину свои события: RequestReceived, ExceptionRaised,
ResponseSent, WorkerStarted и WorkerStopping, см. Рантаймы.
Логирование¶
Если приложение не зарегистрировало Psr\Log\LoggerInterface, App::boot() подключает
Keel\Log\StreamLogger: PSR-3 без зависимостей, одна JSON-строка на запись в php://stderr
(их собирают FrankenPHP, RoadRunner и Docker). Настройки — LoggerConfig в config/logger.php.
Исключения в контексте превращаются в класс, сообщение и место; {placeholders} подставляются
из контекста.
// config/logger.php
return static fn(string $projectDir): LoggerConfig => new LoggerConfig(
stream: 'php://stderr', level: 'debug', channel: 'app', json: true, // канал по умолчанию
channels: [
'audit' => new ChannelConfig(stream: $projectDir . '/var/log/audit.log', level: 'info', rotate: 'daily'),
'security' => new ChannelConfig(syslog: 'myapp'), // локальный syslog под этим ident
],
);
- Каналы.
Loggers::channel('audit')— PSR-3-логгер именованного канала; каждыйChannelConfigперекрывает только те умолчания, что задаёт (поток, уровень, JSON или текст, ротация, syslog). Канал по умолчанию —LoggerInterfaceконтейнера. - Ротация.
rotate: 'daily'превращает файловый потокvar/log/app.logвvar/log/app-2026-10-22.logи переоткрывает его при смене даты; старые файлы удаляйте сами. - Контекст запроса.
LogContextMiddleware, указанный вHttpConfigпослеRequestIdMiddleware,TrustedProxiesMiddlewareиAuthMiddleware, заполняетLogContextполямиrequest_id,method,path,client_ipиprincipal; каждая запись запроса несёт их (собственный контекст записи побеждает).LogContext::set()добавляет свои (tenant); контейнер сбрасывает его между запросами.context: falseв конфиге отключает слияние. - Monolog. Загрузите
MonologProvider(composer require monolog/monolog), и тот же конфиг управляет Monolog: поMonolog\Loggerна канал соStreamHandler,RotatingFileHandlerилиSyslogHandler,JsonFormatterилиLineFormatter, контекст запроса вextra;LoggerInterfaceиLoggersрезолвятся в логгеры Monolog. Любой другой PSR-3-логгер по-прежнему подключается алиасомLoggerInterfaceв провайдере.
Консоль¶
ConsoleFactory::create($app) возвращает Symfony Console Application c командами:
keel:routes— таблица маршрутов с middleware, ролями#[Auth]и настройками#[RateLimit];keel:compile— пересобратьdiscovery.php,routes.php,container.php;keel:cache-clear— удалить их;keel:debug:container [filter]— таблица сервисов, алиасов, параметров и экземпляров, из которых собирается контейнер;keel:doctor [--skip-health]— осмотр установки: PHP и OPcache, расширения apcu, curl и pdo,APP_ENV/APP_DEBUG,config/и пути discovery, каталог кэша и его файлы, рантайм, который выберетKEEL_RUNTIME, и лимиты воркеров (MAX_REQUESTS,KEEL_MAX_MEMORY), необязательные пакеты, контейнер (прогон всех сервисов «вхолостую», см. Контейнер, плюс компиляция в памяти), маршруты, каталоги сессий и кэша и зарегистрированные проверки здоровья.OK,WARNилиFAILна строку; код выхода 1, если что-то упало, так что команда уместна в пайплайне деплоя.--skip-healthпропускает проверки здоровья, когда они ходят в базу;keel:make:controller,keel:make:command,keel:make:middleware,keel:make:listener,keel:make:check(проверка здоровья с тегом),keel:make:authenticator(PrincipalResolver, а с--scheme— полныйAuthenticator),keel:make:policy Post [abilities...](Policyс тегом дляGate) — генераторы классов в namespace проекта (изcomposer.json), без перезаписи без--force;keel:preload— записатьvar/cache/<env>/preload.phpдляopcache.preload: все файлы, которые приложение загружает на старте и при запросе, в порядке загрузки. С preload классы линкуются один раз в общей памяти opcache, а не в каждом процессе php-fpm и не в каждом воркере; особенно это касается PSR-7 классов с трейтами, которые opcache иначе не разделяет. В php.ini:opcache.preload=/path/to/var/cache/prod/preload.php;keel:openapi [--output=FILE]— документ OpenAPI 3.1, см. OpenAPI;keel:serve [--host] [--port] [--docroot] [--frankenphp] [--worker] [--watch]— сервер для разработки. По умолчанию встроенный сервер PHP;--frankenphpиспользует бинарник FrankenPHP ($FRANKENPHP_BINARYилиfrankenphpвPATH),--worker— его worker mode,--watchперезапускает воркеры при измененииsrc/,config/или.env. Worker mode в разработке — самый быстрый способ поймать состояние, утекающее между запросами;keel:install [component...] [--dry-run] [--no-composer] [--force]: подключает компонент фреймворка одним шагом:composer requireдля его пакетов (тех, что ещё не стоят), его провайдер дописывается вServicesConfigвconfig/services.php, middleware — вHttpConfigвconfig/http.php(правка на месте, комментарии и разметка сохраняются; файл создаётся, если его нет), и стартовый конфиг для того, что ему нужно (config/redis.php,config/jwt.php, ...), без перезаписи существующего без--force. Без аргументов выводит список компонентов:cache,redis,memcached,dbal,pdo,twig,latte,monolog,storage,flysystem,i18n,mailer,http-client,jwt,security-headers,cors,health,metrics,openapi,opentelemetry,roadrunner,console. Компонент может тянуть другие (jwtприноситhttp-clientиcache); список, который не удалось найти в конфиге, команда называет, и его нужно поправить руками (код выхода 1);keel:ai:init [--force] [--agents=AGENTS.md] [--llms=public/llms.txt] [--stdout]: пишетAGENTS.mdдля кодовых агентов, собранный из самого приложения (стек, команды, структура, правила worker-режима, глобальные middleware, провайдеры, объекты конфига, маршруты и ссылки на документацию Keel, установленную вvendor/), иllms.txt(llmstxt.org) с описанием HTTP API по его документу OpenAPI, который отдаётся изpublic/.--llms=noneили--agents=noneпропускает один из них;- все найденные
#[AsCommand], создаются лениво через контейнер (конструктор автовайрится).
symfony/console — опциональная зависимость (composer require symfony/console).