Начало работы

Структура проекта

Проект Winter плоский: несколько файлов в корне и одна папка под ваш код. Нет config/, нет routes/, нет обязательного app/. Раскладку внутри своей папки вы выбираете сами — фреймворк находит классы обходом файлов, а не по списку в конфиге.

Ваш код main/Служебное storage/Не сканируется vendor · storage · resources

После установки

text
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

Класс приложения. Объявляет, из чего приложение состоит, и служит точкой входа.

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 и передача управления классу приложения.

bash
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 — соответствие пространства имён каталогу:

composer.json
{
  "autoload": {
      "psr-4": {
          "Main\\": "main/"
      }
  }
}

Один префикс Main\ — не предел. Добавьте свои, и они начнут работать после composer dump-autoload:

composer.json
"psr-4": {
  "Main\\":  "main/",
  "Api\\":   "api/",
  "Admin\\": "admin/"
}

Отдельные корни удобны, когда части приложения живут своей жизнью: у публичного API и у админки разные маршруты, разные middleware и часто разные люди. Сканируются все корни одинаково — фреймворк обходит проект целиком, а не перечисленные пути.

.env

Значения, зависящие от окружения. В репозиторий не коммитится — в .gitignore он и vendor/.

bash
php call cfg env -s     # что реально загрузилось
php call cfg env -i     # создать .env из шаблона
php call cfg key -g     # перевыпустить WINTER_KEY

Полный список переменных — на странице Конфигурация.

main/ — ваш код

Единственная папка, которую вы наполняете. Внутри неё структура ваша: фреймворк не требует ни Controllers/, ни Models/, ни какого-либо другого имени.

text
main/
├── MainController.php
├── User/
│   ├── UserController.php
│   ├── UserService.php
│   └── UserRepository.php
└── Order/
  ├── OrderController.php
  └── OrderProcess.php

Здесь классы сгруппированы по предметной области, а не по типу — но раскладка «по типу» (Controllers/, Services/, Repositories/) работает ровно так же. Разницы для фреймворка нет: он ищет классы, а не каталоги.

Куда кладёт файлы make

Генератор идёт от пространства имён. Правил всего два.

Указали путь точками — он и решает. Путь ищется среди корней PSR-4, vendor/ исключён:

bash
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/. Создавать каталоги заранее не нужно: достаточно один раз создать тот, который вам нравится, и генератор будет попадать в него сам.

bash
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.

Что сканируется при старте

Фреймворк не читает список классов из конфига — он обходит файлы. Порядок такой:

  1. каждый пакет, импортированный через #[Import], в порядке объявления;
  2. затем корень проекта — целиком, от 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/ — не блокировки, а записи: по подкаталогу на класс, имя подкаталога — точечная запись имени класса.

text
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 каталоги были на месте.

bash
php call storage init     # создать каталоги
php call storage clean    # очистить, сохранив сами cache/ и logs/

Кеш загрузки лежит не в storage/

Всё, что фреймворк генерирует для себя, по умолчанию пишется во временный каталог системы, а не в проект:

text
<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/ для сброса этого кеша бесполезно — есть отдельная команда.

bash
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/ Статика, если раздаёте её приложением

Раздача статики включается явно, в настройке веб-слоя:

php
public function configureServer(ServerSettings $server): void
{
  $server->staticPath('resources/static');
}

Такие запросы обслуживаются сервером напрямую и в PHP не попадают: middleware, CORS и логирование запросов к ним не применяются.

Файлы контейнера

Команда php call cfg docker добавляет в проект готовую сборку:

text
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():

bootstrap.php
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 за пределы проекта обычно требуется в контейнере, где код лежит на слое только для чтения, а писать нужно в примонтированный том. Переопределять пути по одному, а не всё сразу — нормально: незаданное считается от заданного.

Дальше