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

Быстрый старт

Соберём первый маршрут от начала до видимого результата: создадим проект из базовой сборки, добавим контроллер, поднимем сервер и получим JSON-ответ. Ниже подробно, что именно происходит на каждом шаге — это первая страница, с которой начинается работа с фреймворком.

Сборка flytachi/winterЗапуск Docker или локальноВремени ~5 минут

Что такое flytachi/winter

Пакетов два, и путать их не нужно.

Пакет Что это Когда берут
flytachi/winter-kernel Ядро. Библиотека: маршрутизация, контейнер, процессы, консоль Встраивание в существующий проект
flytachi/winter Базовая сборка. Готовый каркас проекта, который уже подключил ядро Новый проект — обычный случай

Ядро — это type: library: оно ничего не создаёт на диске и не знает, как разложен ваш проект. Собрать каркас руками можно, и на странице Установка показано как — файл за файлом.

Базовая сборка избавляет от этой работы. flytachi/winter — это type: project: composer не кладёт его в vendor/, а разворачивает как ваш проект, после чего сборка исчезает из картины. Вы получаете рабочий каркас и дальше пишете в нём свой код; обновляется потом только ядро, а не сборка.

1. Проект

bash
composer create-project flytachi/winter my-app
cd my-app

Разворачивается вот что:

text
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 генератор добавит сам:

bash
php call make -c .Greet   # → main/GreetController.php

Приведём его к такому виду — маршрут GET /api/hello/{name}, возвращающий JSON:

main/GreetController.php
<?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. Ставить всё это в систему ради первого маршрута не нужно: сборка умеет сгенерировать себе окружение.

bash
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 внутри, а проект лежит в примонтированном каталоге и остаётся у вас после того, как контейнер исчезнет.

Развернуть проект:

bash
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 откажется ставить пакет в образ, где их нет:

text
flytachi/winter-kernel[v4.0.0, ..., v4.1.0] require ext-pcntl *
-> it is missing from your system.

Установка проходит целиком, вместе с шагами сборки: каталоги созданы, .env записан, WINTER_KEY сгенерирован.

Дальше — консоль тем же способом. У образа composer точка входа — сам composer, поэтому её подменяют на php:

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

bash
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 уже собран.

Когда приложение уже поднято, отдельный контейнер не нужен — команду выполняют прямо в работающем:

bash
docker compose exec server php call mapping show
docker compose exec server php call db migrate

Это единственный способ выполнить то, чему нужна живая среда приложения: миграции, db ping, db pool.

Локально, если PHP уже настроен

bash
php call run dev
bash
php call run

Разница между ними ровно одна — наблюдение за файлами:

Команда Поведение
call run обычный запуск; правка .php подхватится только рестартом
call run dev сторож следит за .php и перезапускает приложение сам

Разработку ведут на run dev: таблица маршрутов и список классов собираются один раз при старте, поэтому без перезапуска новый контроллер сервер не увидит.

Адрес и порт переопределяются флагами:

bash
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. Результат

bash
curl http://localhost:8000/api/hello/Winter
json
{"message":"Hello, Winter"}

Тот же адрес можно открыть в браузере — увидите тот же JSON.

Маршрут не найден?

Посмотрите, что зарегистрировалось:

php call mapping show

Если вашего маршрута в списке нет — контроллер не попал в скан. Проверьте, что файл лежит не в resources/ и не в storage/ (эти каталоги исключены), класс не абстрактный, а метод объявлен public.

5. Чуть больше: параметр запроса

Добавим необязательный ?shout=true:

php
#[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]);
}
bash
curl "http://localhost:8000/api/hello/Winter?shout=true"
json
{"message":"HELLO, WINTER"}

Значение по умолчанию делает параметр необязательным, а тип bool — обязательным к разбору: true, 1, yes, on дадут true, всё остальное осмысленное — false, а негодное значение вернёт 400 ещё до входа в метод.

Весь код целиком

main/GreetController.php
<?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]);
  }
}

Дальше

Маршрут работает — дальше дорога расходится по тому, что вы строите.

Веб-приложение

Запрос пришёл, ответ ушёл — всё, что между ними:

Фоновая работа

Всё, что живёт дольше запроса или запускается без него:

Данные

Ставятся отдельными пакетами, ядро их не тянет:

Разобраться в устройстве