Getting started

Quick start

One route from nothing to a visible result: create a project from the base build, add a controller, bring the server up and get a JSON response. Below, in detail, what happens at each step — this is the first page of working with the framework.

Build flytachi/winterRun Docker or locallyTime ~5 minutes

What flytachi/winter is

There are two packages, and they should not be confused.

Package What it is When you take it
flytachi/winter-kernel The kernel. A library: routing, container, processes, console Fitting it into an existing project
flytachi/winter The base build. A ready project skeleton with the kernel already wired A new project — the usual case

The kernel is a type: library: it creates nothing on disk and knows nothing about how your project is laid out. Building the skeleton by hand is possible, and Installation shows how — file by file.

The base build spares you that. flytachi/winter is a type: project: composer does not put it in vendor/, it unrolls it as your project, after which the build is out of the picture. You get a working skeleton and write your code in it; what gets updated later is the kernel, not the build.

1. The project

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

Here is what unrolls:

text
my-app/
├── bootstrap.php     the application class: what it consists of
├── call              the entry point for every command, the server included
├── composer.json     autoloading Main\ → main/ and the kernel dependency
├── .env              WINTER_KEY, TIME_ZONE, DEBUG
├── main/
│   └── MainController.php     an example controller
└── storage/
  ├── cache/
  └── logs/

Three things the build does for you right after installing — its post-create-project-cmd:

Step What happens
call storage init creates storage/cache and storage/logs
chmod -R 777 storage so both the web process and the console can write there
call cfg init creates .env and generates your own WINTER_KEY

The key is generated on the spot rather than shipped in the repository — it signs data handed to background processes, and one key shared by two projects would be a hole.

There is no `public/` and no config directory

There is no entry point for a web server: the application is the server, and it has no document root. There is no configuration directory either — what other frameworks keep in config/*.php is either an environment variable or an ordinary class the scan finds. More in Project structure.

2. The controller

Generate a controller with make. The leading dot is required; the generator appends the Controller suffix itself:

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

Bring it to this shape — the route GET /api/hello/{name} returning 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}"]);
  }
}

What is going on here:

  • #[RequestMapping('api')] on the class — the /api prefix shared by every method.
  • #[GetMapping('hello/{name}')] — the route GET /api/hello/{name}.
  • #[PathVariable] binds the {name} segment to the $name argument.
  • ResponseEntity::ok([...]) answers 200 with a JSON body.

The route needs registering nowhere: at startup the application walks the project and assembles the route table from the attributes. The file is there, so the route is there.

3. Running it

Winter’s web tier runs on Swoole, and database drivers and a Redis client usually join it. None of that needs installing into your system for a first route: the build can generate its own environment.

bash
php call cfg docker        # Dockerfile, docker-compose.yml, docker/
DEV=true docker compose up

The first command puts four things into the project:

File What for
Dockerfile an image on phpswoole/swoole — Swoole and opcache already inside
docker-compose.yml the port, the source mount, the DEV switch
docker/entrypoint.sh decides how the application starts inside the container
docker/dependencies/*.sh drivers: bcmath, pgsql, mysql, redis — delete what you do not need

DEV=true changes the container’s behaviour wholesale:

DEV=true without the variable (production)
Command inside call run dev call run
Sources mounted, an edit is live from the image
opcache off on and tuned
Before starting — call di build; if it fails the container will not come up

The sources are mounted into the container, so you edit them in your own editor and the application restarts itself. The port is one number — SERVER_PORT in the environment or in .env, 8000 by default.

What that needs on your machine

Docker only. Neither Swoole nor pdo_pgsql nor phpredis has to be installed in your system — they live in the image. PHP on the host is not required either: doing without it entirely is shown below.

If there is no PHP on the machine at all

Both commands above — create-project and cfg docker — are themselves written in PHP. But they can run in a throwaway container: the official composer image carries PHP inside, and the project lives in a mounted directory that stays with you after the container is gone.

Unroll the project:

bash
docker run --rm -v "$PWD":/app -w /app composer:2   create-project flytachi/winter my-app --ignore-platform-reqs

--ignore-platform-reqs here is not “silence the error”. The kernel requires ext-pcntl and ext-posix — they are needed by the running application, not by the installer, and the runtime image has them. Without the flag composer refuses to install into an image that does not:

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

The install completes in full, the build steps included: the directories are created, the .env is written, the WINTER_KEY is generated.

The console goes the same way. The composer image’s entry point is composer itself, so it is replaced with 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

The line is long, so it usually goes into an alias — and from then on it is just winter make -c .Greet:

bash
alias winter='docker run --rm -v "$PWD":/app -w /app --entrypoint php composer:2 call'

A throwaway container is for generators, not for the server

It has no Swoole, so call run there refuses honestly:

`call run` with a web tier needs ext-swoole (pecl install swoole).

And that is the right division of labour: generators and one-off commands in a light container, the application itself in its own image through docker compose up, where Swoole is already built.

Once the application is up, no separate container is needed — the command runs inside the running one:

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

That is the only way to run what needs the application’s live environment: migrations, db ping, db pool.

Locally, if PHP is already set up

bash
php call run dev
bash
php call run

Exactly one thing separates them — watching the files:

Command Behaviour
call run a plain start; a .php edit is picked up only by a restart
call run dev a watcher follows .php files and restarts the application itself

Development is done on run dev: the route table and the class list are assembled once at startup, so without a restart the server does not see a new controller.

The address and port are overridden by flags:

bash
php call run dev --port=9000
php call run dev --host=127.0.0.1 --port=9000   # local only

This way needs PHP 8.4+ with ext-swoole; without it call run refuses to start and says so. The full list of requirements is on Installation.

4. The result

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

The same address opens in a browser and shows the same JSON.

Route not found?

Look at what actually registered:

php call mapping show

If your route is not in the list, the controller did not reach the scan. Check that the file is not under resources/ or storage/ (those are excluded), that the class is not abstract, and that the method is public.

5. A little more: a query parameter

Add an optional ?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"}

The default value makes the parameter optional, and the bool type makes it mandatory to parse: true, 1, yes, on give true, anything else meaningful gives false, and a value that makes no sense answers 400 before the method is entered.

The whole thing

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]);
  }
}

Next

The route works — from here the road forks by what you are building.

A web application

The request arrived, the response left, and everything in between:

Background work

Everything that outlives a request or starts without one:

  • Components — what can be declared beside the web tier
  • Processes — an import, a mailing, a queue consumer
  • Daemons — a fleet of workers with restarts and scaling
  • Scheduler — #[Scheduled]: by interval or by calendar
  • Asynchronous calls — parallel calls inside one request

Data

Installed as separate packages; the kernel does not pull them:

Understanding how it works