Быстрый старт
Соберём первый маршрут от начала до видимого результата: создадим проект из базовой сборки, добавим контроллер, поднимем сервер и получим JSON-ответ. Ниже подробно, что именно происходит на каждом шаге — это первая страница, с которой начинается работа с фреймворком.
Что такое flytachi/winter
Пакетов два, и путать их не нужно.
| Пакет | Что это | Когда берут |
|---|---|---|
flytachi/winter-kernel |
Ядро. Библиотека: маршрутизация, контейнер, процессы, консоль | Встраивание в существующий проект |
flytachi/winter |
Базовая сборка. Готовый каркас проекта, который уже подключил ядро | Новый проект — обычный случай |
Ядро — это type: library: оно ничего не создаёт на диске и не знает, как разложен ваш
проект. Собрать каркас руками можно, и на странице Установка
показано как — файл за файлом.
Базовая сборка избавляет от этой работы. flytachi/winter — это type: project:
composer не кладёт его в vendor/, а разворачивает как ваш проект, после чего сборка
исчезает из картины. Вы получаете рабочий каркас и дальше пишете в нём свой код; обновляется
потом только ядро, а не сборка.
1. Проект
composer create-project flytachi/winter my-app
cd my-appРазворачивается вот что:
my-app/
├── bootstrap.php класс приложения: из чего оно состоит
├── call точка входа для всех команд, включая сервер
├── composer.json автозагрузка Main\ → main/ и зависимость от ядра
├── .env WINTER_KEY, TIME_ZONE, DEBUG
├── main/
│ └── MainController.php пример контроллера
└── storage/
├── cache/
└── logs/Три вещи сборка делает за вас сразу после установки — это её
post-create-project-cmd:
| Шаг | Что происходит |
|---|---|
call storage init |
создаёт storage/cache и storage/logs |
chmod -R 777 storage |
чтобы туда мог писать и веб-процесс, и консоль |
call cfg init |
создаёт .env и генерирует свой WINTER_KEY |
Ключ генерируется на месте, а не приезжает из репозитория, — им подписываются данные, уходящие в фоновые процессы, и общий ключ на два проекта был бы дырой.
Ни `public/`, ни каталога с конфигами здесь нет
Точки входа для веб-сервера не существует: приложение само является сервером, и
директории документов у него нет. Конфигурации тоже нет — то, что в других фреймворках
лежит в config/*.php, здесь либо переменная окружения, либо обычный класс, который
находит сканер. Подробнее — Структура проекта.
2. Контроллер
Сгенерируем контроллер командой make. Точка перед именем обязательна, суффикс
Controller генератор добавит сам:
php call make -c .Greet # → main/GreetController.phpПриведём его к такому виду — маршрут GET /api/hello/{name}, возвращающий JSON:
<?php
namespace Main;
use Flytachi\Winter\Kernel\Http\Request\Annotation\PathVariable;
use Flytachi\Winter\Kernel\Http\Response\ResponseEntity;
use Flytachi\Winter\Kernel\Http\Stereotype\Controller;
use Flytachi\Winter\Kernel\Route\Annotation\GetMapping;
use Flytachi\Winter\Kernel\Route\Annotation\RequestMapping;
#[RequestMapping('api')]
class GreetController extends Controller
{
#[GetMapping('hello/{name}')]
public function hello(#[PathVariable] string $name): ResponseEntity
{
return ResponseEntity::ok(['message' => "Hello, {$name}"]);
}
}Что здесь происходит:
#[RequestMapping('api')]на классе — общий префикс/apiдля всех методов.#[GetMapping('hello/{name}')]— маршрутGET /api/hello/{name}.#[PathVariable]привязывает сегмент{name}к аргументу$name.ResponseEntity::ok([...])отдаёт200с телом в JSON.
Регистрировать маршрут нигде не нужно: при старте приложение обходит проект и собирает таблицу маршрутов из атрибутов. Файл положен — маршрут есть.
3. Запуск
Через Docker — рекомендуемый способ
Веб-слой Winter работает на Swoole, а к нему обычно добавляются драйверы баз данных и клиент Redis. Ставить всё это в систему ради первого маршрута не нужно: сборка умеет сгенерировать себе окружение.
php call cfg docker # Dockerfile, docker-compose.yml, docker/
DEV=true docker compose upПервая команда кладёт в проект четыре вещи:
| Файл | Зачем |
|---|---|
Dockerfile |
образ на phpswoole/swoole — Swoole и opcache уже внутри |
docker-compose.yml |
порт, монтирование исходников, переключатель DEV |
docker/entrypoint.sh |
решает, как запускать приложение внутри контейнера |
docker/dependencies/*.sh |
драйверы: bcmath, pgsql, mysql, redis — лишние удаляют |
DEV=true меняет поведение контейнера целиком:
DEV=true |
без переменной (прод) | |
|---|---|---|
| Команда внутри | call run dev |
call run |
| Исходники | смонтированы, правка видна сразу | из образа |
| opcache | выключен | включён и настроен |
| Перед стартом | — | call di build; упал — контейнер не поднимется |
Исходники смонтированы в контейнер, поэтому правите вы их обычным редактором у себя, а
перезапускается приложение само. Порт задаётся одним числом — SERVER_PORT в окружении
или в .env, по умолчанию 8000.
Что для этого нужно на машине
Только Docker. Ни Swoole, ни pdo_pgsql, ни phpredis ставить в систему не придётся —
они живут в образе. PHP на хосте тоже не обязателен: как обойтись без него совсем,
показано ниже.
Если PHP на машине нет вовсе
Обе команды выше — create-project и cfg docker — сами написаны на PHP. Но запускать
их можно в одноразовом контейнере: официальный образ composer несёт PHP внутри, а
проект лежит в примонтированном каталоге и остаётся у вас после того, как контейнер
исчезнет.
Развернуть проект:
docker run --rm -v "$PWD":/app -w /app composer:2 create-project flytachi/winter my-app --ignore-platform-reqs--ignore-platform-reqs здесь не «замять ошибку». Ядро требует ext-pcntl и
ext-posix — они нужны работающему приложению, а не установщику, и в рабочем образе
они есть. Без флага composer откажется ставить пакет в образ, где их нет:
flytachi/winter-kernel[v4.0.0, ..., v4.1.0] require ext-pcntl *
-> it is missing from your system.Установка проходит целиком, вместе с шагами сборки: каталоги созданы, .env записан,
WINTER_KEY сгенерирован.
Дальше — консоль тем же способом. У образа composer точка входа — сам composer,
поэтому её подменяют на php:
cd my-app
docker run --rm -v "$PWD":/app -w /app --entrypoint php composer:2 call cfg docker
docker run --rm -v "$PWD":/app -w /app --entrypoint php composer:2 call make -c .Greet
docker run --rm -v "$PWD":/app -w /app --entrypoint php composer:2 call mapping showСтрока длинная, поэтому её обычно прячут в псевдоним — и дальше пишут просто
winter make -c .Greet:
alias winter='docker run --rm -v "$PWD":/app -w /app --entrypoint php composer:2 call'Одноразовый контейнер годится для генераторов, но не для сервера
В нём нет Swoole, поэтому call run там честно откажется:
`call run` with a web tier needs ext-swoole (pecl install swoole).И это правильное разделение труда: генераторы и разовые команды — в лёгком контейнере,
само приложение — в своём образе через docker compose up, где Swoole уже собран.
Когда приложение уже поднято, отдельный контейнер не нужен — команду выполняют прямо в работающем:
docker compose exec server php call mapping show
docker compose exec server php call db migrateЭто единственный способ выполнить то, чему нужна живая среда приложения: миграции,
db ping, db pool.
Локально, если PHP уже настроен
php call run devphp call runРазница между ними ровно одна — наблюдение за файлами:
| Команда | Поведение |
|---|---|
call run |
обычный запуск; правка .php подхватится только рестартом |
call run dev |
сторож следит за .php и перезапускает приложение сам |
Разработку ведут на run dev: таблица маршрутов и список классов собираются один раз
при старте, поэтому без перезапуска новый контроллер сервер не увидит.
Адрес и порт переопределяются флагами:
php call run dev --port=9000
php call run dev --host=127.0.0.1 --port=9000 # только локальноДля этого способа нужен PHP 8.4+ с ext-swoole; без него call run откажется стартовать
и скажет об этом. Полный список требований — на странице
Установка.
4. Результат
curl http://localhost:8000/api/hello/Winter{"message":"Hello, Winter"}Тот же адрес можно открыть в браузере — увидите тот же JSON.
Маршрут не найден?
Посмотрите, что зарегистрировалось:
php call mapping show
Если вашего маршрута в списке нет — контроллер не попал в скан. Проверьте, что файл
лежит не в resources/ и не в storage/ (эти каталоги исключены), класс не
абстрактный, а метод объявлен public.
5. Чуть больше: параметр запроса
Добавим необязательный ?shout=true:
#[GetMapping('hello/{name}')]
public function hello(
#[PathVariable] string $name,
#[RequestParam] bool $shout = false,
): ResponseEntity {
$message = "Hello, {$name}";
return ResponseEntity::ok(['message' => $shout ? strtoupper($message) : $message]);
}curl "http://localhost:8000/api/hello/Winter?shout=true"{"message":"HELLO, WINTER"}Значение по умолчанию делает параметр необязательным, а тип bool — обязательным к
разбору: true, 1, yes, on дадут true, всё остальное осмысленное — false, а
негодное значение вернёт 400 ещё до входа в метод.
Весь код целиком
<?php
namespace Main;
use Flytachi\Winter\Kernel\Http\Request\Annotation\PathVariable;
use Flytachi\Winter\Kernel\Http\Request\Annotation\RequestParam;
use Flytachi\Winter\Kernel\Http\Response\ResponseEntity;
use Flytachi\Winter\Kernel\Http\Stereotype\Controller;
use Flytachi\Winter\Kernel\Route\Annotation\GetMapping;
use Flytachi\Winter\Kernel\Route\Annotation\RequestMapping;
#[RequestMapping('api')]
class GreetController extends Controller
{
#[GetMapping('hello/{name}')]
public function hello(
#[PathVariable] string $name,
#[RequestParam] bool $shout = false,
): ResponseEntity {
$message = "Hello, {$name}";
return ResponseEntity::ok(['message' => $shout ? strtoupper($message) : $message]);
}
}Дальше
Маршрут работает — дальше дорога расходится по тому, что вы строите.
Веб-приложение
Запрос пришёл, ответ ушёл — всё, что между ними:
- Маршрутизация — глаголы, префиксы, параметры пути
- Контроллеры — что возвращать и как получать зависимости
- Запросы и привязка параметров — тело, заголовки, файлы
- Валидация — проверка входных данных до входа в метод
- Ответы — JSON, файлы, потоки, коды и заголовки
- Обработка ошибок — что увидит клиент, когда что-то пошло не так
Фоновая работа
Всё, что живёт дольше запроса или запускается без него:
- Состав приложения — что можно объявить рядом с вебом
- Процессы — импорт, рассылка, обработчик очереди
- Демоны — флот воркеров с перезапуском и масштабированием
- Планировщик —
#[Scheduled]: по интервалу или по календарю - Асинхронные вызовы — параллельные обращения внутри одного запроса
Данные
Ставятся отдельными пакетами, ядро их не тянет:
- PHP Persistence API — репозитории, сущности, миграции
- Redis — сторы, хеши, списки, стримы с пулом соединений
- Базовые подключения — если пакеты не нужны, а соединение нужно
Разобраться в устройстве
- Ключевые понятия — как связаны сканер, контейнер и стереотипы
- Внедрение зависимостей — откуда контроллер берёт сервисы
- Конфигурация —
.env, манифест приложения, конфигураторы - Философия — почему решения именно такие