Структура проекта
Проект Winter плоский: несколько файлов в корне и одна папка под ваш код. Нет
config/, нет routes/, нет обязательного app/.
Раскладку внутри своей папки вы выбираете сами — фреймворк находит классы обходом
файлов, а не по списку в конфиге.
После установки
my-app/
├── bootstrap.php класс приложения: из чего оно состоит
├── call точка входа для всех команд
├── composer.json автозагрузка и зависимости
├── composer.lock зафиксированные версии
├── .env переменные окружения
├── .gitignore
│
├── main/ ваш код, namespace Main\
│ └── MainController.php
│
├── storage/ служебные данные, содержимое в репозиторий не идёт
│ ├── cache/
│ └── logs/
│
└── vendor/Это всё, что есть сразу после composer create-project. Остальное появляется по мере
надобности:
| Что | Когда появляется |
|---|---|
storage/runnable/ |
при первом запуске процесса или демона |
resources/ |
создаёте вы — под шаблоны, переводы, статику |
Dockerfile, docker-compose.yml, docker/ |
php call cfg docker |
каталоги внутри main/ |
создаёте вы или генератор make |
Корень проекта
bootstrap.php
Класс приложения. Объявляет, из чего приложение состоит, и служит точкой входа.
#[EnableWeb]
final class Application extends WinterApplication
{
public static function main(array $argv): never
{
parent::run($argv);
}
}Атрибуты #[Enable*] — единственное место, где перечислен состав приложения. См.
Состав приложения.
Этот файл заодно задаёт корень проекта: фреймворк берёт каталог, в котором лежит
класс приложения, и от него выводит все остальные пути — .env, storage,
resources, начало обхода файлов. Поэтому bootstrap.php лежит в корне, а не в
подпапке.
call
Запускает всё: сервер, консольные команды, генераторы. Внутри — проверка версии PHP,
chdir(__DIR__), подключение bootstrap.php и передача управления классу приложения.
php call run dev # сервер разработки
php call mapping show # список маршрутов
php call make -c .User # создать контроллерchdir внутри означает, что команды работают из любого каталога:
php /path/to/my-app/call run поднимет сервер с правильными путями.
composer.json
Кроме зависимостей задаёт PSR-4 — соответствие пространства имён каталогу:
{
"autoload": {
"psr-4": {
"Main\\": "main/"
}
}
}Один префикс Main\ — не предел. Добавьте свои, и они начнут работать после
composer dump-autoload:
"psr-4": {
"Main\\": "main/",
"Api\\": "api/",
"Admin\\": "admin/"
}Отдельные корни удобны, когда части приложения живут своей жизнью: у публичного API и у админки разные маршруты, разные middleware и часто разные люди. Сканируются все корни одинаково — фреймворк обходит проект целиком, а не перечисленные пути.
.env
Значения, зависящие от окружения. В репозиторий не коммитится — в .gitignore он и
vendor/.
php call cfg env -s # что реально загрузилось
php call cfg env -i # создать .env из шаблона
php call cfg key -g # перевыпустить WINTER_KEYПолный список переменных — на странице Конфигурация.
main/ — ваш код
Единственная папка, которую вы наполняете. Внутри неё структура ваша: фреймворк не
требует ни Controllers/, ни Models/, ни какого-либо другого имени.
main/
├── MainController.php
├── User/
│ ├── UserController.php
│ ├── UserService.php
│ └── UserRepository.php
└── Order/
├── OrderController.php
└── OrderProcess.phpЗдесь классы сгруппированы по предметной области, а не по типу — но раскладка «по
типу» (Controllers/, Services/, Repositories/) работает ровно так же. Разницы
для фреймворка нет: он ищет классы, а не каталоги.
Куда кладёт файлы make
Генератор идёт от пространства имён. Правил всего два.
Указали путь точками — он и решает. Путь ищется среди корней PSR-4, vendor/
исключён:
php call make -c api.user.Profile # main/Api/User/ProfileController.php
php call make -c admin.Report # admin/ReportController.php (если есть корень Admin\)Путь не указали (.Name) — генератор смотрит, нет ли уже привычного каталога.
Берётся первый существующий из списка:
| Тип | Флаг | Ищет каталог (первый существующий) |
|---|---|---|
| Контроллер | -c |
Rests, Rest, Controllers, Controller |
| Middleware | -m |
Middlewares, Middleware, Controllers/Middlewares, Controller/Middleware |
| Сервис | -s |
Services, Service |
| Репозиторий | -r |
Repositories, Repository |
| Redis-хранилище | -t |
Stores, Store |
| Сущность | -e |
Entity, Entities |
| DTO | -d |
Dto, DTOs, Entity/Dto, Entities/Dto |
| Процесс | -P |
Processes, Process |
| Демон | -N |
Daemons, Daemon |
| Конфиг БД | -D |
Configs/Databases, Config/Database, Configs, Config |
| Конфиг Redis | -R |
Configs/Redis, Configs, Config |
| Консольная команда | -n |
Cmd |
Ни одного такого каталога нет — файл ляжет в корень PSR-4, то есть прямо в main/.
Создавать каталоги заранее не нужно: достаточно один раз создать тот, который вам
нравится, и генератор будет попадать в него сам.
php call make -c .User # main/UserController.php
mkdir main/Controllers
php call make -c .Order # main/Controllers/OrderController.phpОпция --mvc не ищет, а создаёт: она подставляет каталог по типу
(Controllers/, Services/, Repositories/…) независимо от того, есть он или нет.
Подробности и все флаги — call make.
Что сканируется при старте
Фреймворк не читает список классов из конфига — он обходит файлы. Порядок такой:
- каждый пакет, импортированный через
#[Import], в порядке объявления; - затем корень проекта — целиком, от
bootstrap.phpвниз.
Приложение идёт последним намеренно: там, где вклад пакета и вклад приложения конфликтуют, побеждать должно приложение.
В каждом каталоге читаются только файлы .php. Из файла собираются все
объявленные в нём классы, после чего файл подключается. Так находятся контроллеры,
конфигурации, процессы, команды — регистрировать их нигде не требуется.
Из обхода исключены три каталога:
| Каталог | Почему |
|---|---|
vendor/ |
Зависимости не участвуют в поиске классов приложения; исключено самим сканером |
storage/ |
Там лежит сгенерированный код — обход читал бы результат прошлого обхода |
resources/ |
Там лежат шаблоны: PHP-файлы по природе, не классы |
Исключения привязаны не к именам, а к настроенным путям: перенесли ресурсы в
assets/ через pathResource — исключение переедет вместе с ними.
Шаблоны исключены не для скорости
Сканер ищет объявление класса в каждом .php и подключает файл, где его нашёл. Для
шаблона это означает выполнение: он выведет свою разметку в вывод приложения и
запустит всё, что есть у него на верхнем уровне. Это проверено на живом проекте, а не
предположено.
Поэтому шаблоны держите под resources/. Каталог с шаблонами, положенный внутрь
main/, попадёт под обход.
Отсюда практическое правило: всё, что не является классом приложения, должно жить в
resources/ или storage/. Скрипты, дампы, миграционные однострочники в корне
проекта будут прочитаны и, если объявляют класс, подключены.
storage/ — служебные данные
| Каталог | Что там | Кто создаёт |
|---|---|---|
storage/cache/ |
То, что вы кладёте сами через Kernel::store() |
storage init |
storage/logs/ |
Файлы логов при LOG_OUTPUT=file |
storage init |
storage/runnable/ |
Состояние запущенных процессов и демонов | Первый запуск такого процесса |
runnable/ — не блокировки, а записи: по подкаталогу на класс, имя подкаталога —
точечная запись имени класса.
storage/runnable/
└── Main.Daemon.Emails/
└── 2bb0b4ff2c7e… PID, состояние, время старта, число рестартовЭто то, что читают call process status и call daemon list; из-за общего каталога
консоль видит процесс, запущенный из-под веб-приложения, и наоборот. Сюда же
winter-ppa кладёт ppa.pool — статистику пула соединений
для /actuator/health.
Содержимое каталога в репозиторий не идёт, а структура идёт: storage/.gitignore
игнорирует всё, кроме себя, cache/ и logs/ — чтобы после git clone каталоги
были на месте.
php call storage init # создать каталоги
php call storage clean # очистить, сохранив сами cache/ и logs/Кеш загрузки лежит не в storage/
Всё, что фреймворк генерирует для себя, по умолчанию пишется во временный каталог системы, а не в проект:
<sys_get_temp_dir()>/flytachi.winter.volatile.<имя-каталога-проекта>
├── di.php список найденных классов
├── async.php список классов с #[Async]
└── async/ сгенерированные прокси #[Async]В Linux и в контейнере это /tmp/flytachi.winter.volatile.my-app; на macOS временный
каталог свой, внутри /var/folders/…. Гадать не нужно — точные пути печатает
php call di.
Так сделано, чтобы сгенерированный код не попадал в образ и не переживал перезагрузку
машины. Практическое следствие: чистить storage/ для сброса этого кеша бесполезно —
есть отдельная команда.
php call di build # собрать кеш и проверить контракты #[Async]
php call di clean # удалить кеш и все проксиПри DEBUG=true кеша нет вообще
Кеш пишется и читается только при DEBUG=false. В отладке каждый старт заново обходит
файлы — новый класс виден сразу, без di build и без «почему мой контроллер не
находится».
Обратная сторона того же: на проде класс, добавленный без пересборки кеша, не
появится. Поэтому di build — шаг сборки образа, а не ручная операция.
Положить каталог внутрь проекта можно флагом isTmpVolatile: false в Kernel::init()
— тогда это будет storage/volatile/, и каталог обязан быть доступен на запись
пользователю, от которого работает приложение.
resources/ — шаблоны, переводы, статика
Каталога изначально нет; создайте, когда понадобится. Имена подкаталогов — соглашение фреймворка, менять их не нужно.
| Путь | Что там |
|---|---|
resources/views/ |
Шаблоны и макеты для ResponseView |
resources/lang/ |
Словари переводов: ru.php, en.php |
resources/static/ |
Статика, если раздаёте её приложением |
Раздача статики включается явно, в настройке веб-слоя:
public function configureServer(ServerSettings $server): void
{
$server->staticPath('resources/static');
}Такие запросы обслуживаются сервером напрямую и в PHP не попадают: middleware, CORS и логирование запросов к ним не применяются.
Файлы контейнера
Команда php call cfg docker добавляет в проект готовую сборку:
my-app/
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
└── docker/
├── entrypoint.sh запуск: dev или прод
├── php-opcache.ini настройки opcache для прода
├── php-memory.ini
└── dependencies/ скрипты установки расширений
├── 10-bcmath.sh
├── 20-pgsql.sh
├── 30-mysql.sh
└── 40-redis.shРежим выбирается переменной: DEV=true docker compose up — разработка с перезапуском
по изменению файлов, docker compose up — рабочий режим с opcache.
Скрипты в dependencies/ выполняются при сборке образа по порядку номеров. Лишние
удаляйте, свои добавляйте — как именно, описано в
cfg docker.
Свои пути
Раскладка по умолчанию выводится из корня, а корень — из расположения класса
приложения. Всё это переопределяется в configure():
protected static function configure(ApplicationArguments $args): void
{
Kernel::init(
pathRoot: __DIR__,
pathResource: __DIR__ . '/assets',
pathStorage: '/var/lib/my-app',
);
}| Параметр | По умолчанию |
|---|---|
pathRoot |
каталог файла с классом приложения |
pathResource |
<root>/resources |
pathStorage |
<root>/storage |
pathStorageLog |
<storage>/logs |
pathStorageCache |
<storage>/cache |
pathStorageRunnable |
<storage>/runnable |
isTmpVolatile |
true — кеш загрузки во временном каталоге системы |
Вынести storage за пределы проекта обычно требуется в контейнере, где код лежит на
слое только для чтения, а писать нужно в примонтированный том. Переопределять пути по
одному, а не всё сразу — нормально: незаданное считается от заданного.
Дальше
- Конфигурация —
.envи классы-конфигураторы - Состав приложения — атрибуты
#[Enable*] - Быстрый старт — первый маршрут
call make— генераторы и все флаги- Пакеты —
#[Import]и чужой код в обходе