Ключевые понятия
Семь идей, из которых следует всё остальное. Каждая объясняется до того, как вы встретите её в коде: прочитав страницу, вы будете понимать не только как писать в Winter, но и почему именно так.
1. Приложение живёт, а не поднимается на каждый запрос
Это главное отличие, и из него следует почти всё остальное.
Как было в классическом PHP. Приходит запрос — PHP поднимает процесс, читает конфиги, строит контейнер, обрабатывает один запрос и умирает. Всё, что вы положили в статическое свойство, исчезает. Утечка памяти безвредна: процесс всё равно завершится через миллисекунды.
Как в Winter. Приложение запускается один раз и продолжает работать. Мастер-процесс загружает код и порождает воркеры; каждый воркер обслуживает запросы годами, не перезагружаясь.
мастер загружает код один раз
├── воркер 1 обслуживает запросы, живёт долго
├── воркер 2 └── корутина ← запрос A
├── воркер 3 └── корутина ← запрос B (одновременно)
└── воркер 4 └── корутина ← запрос CВнутри воркера запросы выполняются корутинами, а не потоками. Когда запрос ждёт базу или HTTP-вызов, воркер не простаивает — он переключается на другой запрос. Один воркер обслуживает сотни одновременных запросов, оставаясь однопоточным: гонок за память нет, блокировки не нужны.
Три следствия, которые вы почувствуете сразу:
| Старт стоит дорого — один раз | Скан проекта, построение контейнера, чтение конфигов происходят до первого запроса. Сам запрос уже ничего не поднимает |
| Состояние переживает запрос | Статическое свойство, заполненное в одном запросе, увидит следующий. Иногда это то, что нужно; чаще — источник ошибки |
| Долгая операция не блокирует остальных | Но только если она умеет уступать управление. Обычные PHP-функции этого не умеют |
Что теперь может пойти не так
Данные одного пользователя, сохранённые в статическое свойство, увидит следующий — и это будет не «странность», а утечка между пользователями. Из-за корутин это случается даже в пределах одного запроса по времени: два запроса выполняются вперемежку.
Поэтому в Winter состояние либо принадлежит объекту, который создаётся на запрос, либо хранится в контексте корутины. Всё, что фреймворк держит «текущим» — язык, часовой пояс, поля логирования — устроено именно так.
2. Обнаружение вместо регистрации
В Winter нет файла со списком контроллеров, нет routes.php, нет реестра сервисов.
При старте фреймворк обходит проект и находит всё сам.
старт → обход всех .php от корня проекта
(кроме vendor/, storage/, resources/)
→ в каждом файле ищется объявление класса
→ найденный класс подключается и разбирается:
контроллер? → маршруты в таблицу
#[Configuration]? → фабрики в контейнер
конфигуратор? → применить настройки
процесс, демон? → зарегистрироватьОбход один на всё приложение, а не отдельный для маршрутов, отдельный для DI. Создали класс — он работает. Не нужно ничего дописывать в списки, и не бывает ситуации «написал контроллер, а маршрут не появился, потому что забыл зарегистрировать».
Плата за это — старт: обход читает каждый файл. Поэтому в рабочем режиме результат
кешируется, и при DEBUG=false повторный старт уже не ходит по диску. В режиме
разработки кеш выключен: новый класс должен подхватываться сразу.
Отсюда правило про чужие .php
Сканер не отличает класс приложения от чего угодно другого — он читает файл и
подключает его, если нашёл объявление класса. Именно поэтому шаблоны живут в
resources/, а генерируемый код — в storage/: оба каталога исключены из обхода.
Подробнее — в Структуре проекта.
3. Атрибуты вместо конфигов
Поведение описывается рядом с кодом, к которому относится — атрибутом PHP, а не записью в отдельном файле.
#[RequestMapping('users')] // prefix for the whole class
class UserController extends Controller
{
#[Autowired] private UserService $service; // dependency
#[GetMapping('{id}')] // GET /users/{id}
public function show(#[PathVariable] int $id): ResponseEntity
{
return ResponseEntity::ok($this->service->find($id));
}
}Здесь пять решений — префикс, зависимость, метод, путь, источник параметра — и все пять видны в одном экране. В конфиг-файле их пришлось бы искать в четырёх местах, а при переименовании метода они молча разъехались бы с кодом.
Атрибуты покрывают все слои фреймворка:
| Слой | Атрибуты | Где читать |
|---|---|---|
| Маршруты | #[RequestMapping], #[GetMapping], #[PostMapping], #[Timeout], #[CrossOrigin] |
Маршрутизация |
| Привязка запроса | #[PathVariable], #[RequestParam], #[RequestQuery], #[RequestBody], #[RequestJson], #[RequestForm], #[RequestFile], #[RequestHeader] |
Запросы |
| Валидация | #[Valid] и 24 ограничения: #[NotBlank], #[Email], #[Size], #[In]… |
Валидация |
| Зависимости | #[Autowired], #[Inject], #[Lazy], #[Singleton], #[Request], #[Transient] |
Внедрение зависимостей |
| Конфигурация | #[Configuration], #[Bean], #[Value], #[Import] |
Конфигурация |
| Состав приложения | #[EnableWeb], #[EnableProcess], #[EnableDaemon], #[EnableScheduler], #[EnableAsync], #[EnableActuator] |
Состав приложения |
| Фон и расписание | #[Async], #[Scheduled] |
Асинхронность, Планировщик |
| Ошибки | #[AdviceException] |
Обработка ошибок |
| Схема БД | #[Table], #[Id], #[Varchar], #[ForeignKey], #[Index]… |
База данных |
Запоминать их не нужно: каждое семейство разбирается на своей странице, а редактор подсказывает аргументы — это обычные PHP-классы.
4. Стереотипы
Стереотип — базовый класс с определённой ролью. Наследуясь от него, вы говорите фреймворку, чем является ваш класс, и получаете готовое поведение.
| Стереотип | Роль | Наследуют |
|---|---|---|
Controller |
Принимает HTTP-запрос, возвращает ответ | Контроллеры |
Middleware |
Работает до и после контроллера | Middleware |
Repository |
Доступ к таблице; есть RepositoryCrud и RepositoryView |
Репозитории |
Process |
Единица работы вне запроса: разово, в фоне, по требованию | Процессы |
Daemon |
Долгоживущий процесс с воркерами и авто-перезапуском | Демоны |
Cmd, CmdCustom |
Консольная команда | Свои команды |
Сервис — это обычный класс
Стереотипа Service в Winter нет, и это намеренно. Бизнес-логика не нуждается ни в
каком поведении от фреймворка: обычный класс отлично внедряется, тестируется без
всякого окружения и не тянет за собой базовый класс.
php call make -s .Order создаёт именно такой класс — без наследования.
5. Контейнер собирает объекты за вас
Вы не пишете new для контроллеров, сервисов и репозиториев. Объявляете, что вам
нужно, — контейнер создаёт и передаёт.
class OrderService
{
#[Autowired] private OrderRepository $orders;
#[Autowired] private PaymentGateway $payments;
}Регистрировать эти классы нигде не нужно: контейнер умеет создать любой класс, у которого разрешимы зависимости конструктора.
Область видимости — сколько живёт объект
| Область | Объект создаётся | Когда применять |
|---|---|---|
| Transient — по умолчанию | Каждый раз заново | Почти всегда |
#[Singleton] |
Один раз на воркер | Дорогое и не хранящее данных запроса: пулы, клиенты |
#[Request] |
Один раз на запрос | То, что описывает этот запрос: текущий пользователь, контекст |
По умолчанию — transient, а не singleton
Если вы приходите из Spring или Laravel, это отличие стоит запомнить: там компонент по умолчанию один на приложение, здесь — новый на каждое внедрение.
Выбор сделан ради резидентного рантайма. Синглтон в приложении, которое живёт неделями, — это объект, чьё состояние копится всё это время, и по умолчанию такой ценой платить не стоит.
Фреймворк проверяет сочетания областей при старте: #[Singleton], держащий внутри
#[Request]-объект, — это данные первого запроса, замёрзшие на всё время жизни
воркера. Приложение не поднимется и укажет цепочку. Подробнее — в
Внедрении зависимостей.
6. Одно приложение — несколько рантаймов
Winter — не только веб. Из чего состоит приложение, объявляется атрибутами на классе-точке входа:
#[EnableWeb] // HTTP server
#[EnableScheduler] // scheduled methods
#[EnableProcess(QueueWorker::class)] // background workers
final class Application extends WinterApplication
{
public static function main(array $argv): never
{
parent::run($argv);
}
}Убрали #[EnableWeb] — получилось приложение без HTTP: планировщик и очередь, тот
же код, те же сервисы, тот же контейнер. Отдельного «консольного ядра» и второго
набора конфигов для этого не требуется.
Файл входа при этом один — call. Он же запускает сервер, он же выполняет команды.
7. Конвейер запроса
Между сокетом и вашим методом стоит цепочка шагов. Каждый можно настроить, но по умолчанию все они уже работают.
запрос
→ маршрут подобрать метод по пути и HTTP-методу
→ middleware before(): аутентификация, контекст, логирование
→ привязка из URL, query, тела, файлов — в аргументы метода
→ валидация #[Valid] и ограничения → 422 при провале
→ ваш метод получает готовые, проверенные, типизированные данные
→ ответ ResponseEntity, ResponseView, ResponseFile
→ middleware after(): доработать ответ
ответСмысл конвейера — в том, что до вашего метода доходят уже разобранные и
проверенные данные. В теле не встречается ни $_GET, ни json_decode, ни
проверок «а пришло ли поле».
Исключение выходит из конвейера отдельным путём: его перехватывает обработчик,
помеченный #[AdviceException], и превращает в ответ. Свои классы ошибок с
собственным телом ответа заводятся именно так — см.
Обработку ошибок.
Работа вне запроса
Четыре механизма, и выбор между ними — частый вопрос:
| Механизм | Что это | Когда брать |
|---|---|---|
#[Async] |
Метод выполняется параллельно, результат забирается через Future |
Несколько независимых обращений внутри одного запроса |
Process |
Отдельная единица работы; запускается разово, в фоне или по требованию | Импорт, отчёт, рассылка — то, что имеет начало и конец |
Daemon |
Долгоживущий процесс с воркерами, перезапуском и масштабированием | Очередь, потребитель шины, постоянный обработчик |
#[Scheduled] |
Метод вызывается по расписанию или интервалу | Регулярное: выгрузка, очистка, синхронизация |
Ответ на «что выбрать» обычно даёт длительность: секунды внутри запроса — #[Async],
задача с концом — Process, работа без конца — Daemon, работа по времени —
#[Scheduled]. Разбор с примерами — на странице
Фоновые процессы.
Настройка — это код
Отдельного формата конфигурации нет: то, что нужно настроить, описывается классом, который находит тот же обход проекта.
final class WebConfig extends WebConfigurerAdapter
{
public function configureCors(CorsRegistry $cors): void
{
$cors->addMapping('/api/**')->allowedOrigins('https://app.example.com');
}
public function configureServer(ServerSettings $server): void
{
$server->workers(8)->requestTimeout(30);
}
}Регистрировать класс не нужно. Так же настраиваются каналы логов
(LoggingConfigurer) и собираются нестандартные объекты
(#[Configuration] с методами #[Bean]). Значения, зависящие от окружения, живут в
.env — см. Конфигурацию.
Собираем вместе
Что происходит с GET /users/1:
ДО ЗАПУСКА (один раз)
php call run
→ обход проекта: контроллеры, конфигурации, конфигураторы
→ контейнер собран, таблица маршрутов построена
→ воркеры порождены, сервер слушает порт
НА ЗАПРОС (каждый раз)
GET /users/1
→ воркер принимает, открывает корутину
→ маршрут: #[GetMapping('{id}')] → UserController::show
→ middleware before()
→ контейнер создаёт UserController, внедряет UserService
→ #[PathVariable] привязывает id = 1
→ show(1) → ResponseEntity::ok(...)
→ middleware after() → ответ клиенту
→ объекты запроса уничтожены, воркер берёт следующийОбратите внимание на границу: всё дорогое — слева, до запуска. Справа остаётся только работа, относящаяся к конкретному запросу.
Дальше
- Установка — создать проект с нуля
- Быстрый старт — пройти этот путь руками
- Структура проекта — где что лежит
- Маршрутизация — начало веб-слоя