Getting started

Installation

Winter is a library, not a skeleton with a mandatory structure. A project is assembled from a handful of files, and every one of them is explained below: what it does and why it is there. If you would rather not take it apart — the first command on this page gives you a working project.

Requirements

Requirement Value
PHP 8.4 or newer
Composer 2.x
Extensions ext-pcntl, ext-posix, ext-fileinfo
For a web application ext-swoole

The web tier only runs on Swoole

Winter does not use PHP’s built-in server. If the application declares #[EnableWeb], starting it requires the Swoole extension and refuses without it:

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

That includes development mode — it differs only by watching files; the server is the same one.

An application without a web tier — only processes, daemons or the scheduler — does not need Swoole.

The remaining extensions are installed for specific jobs:

Extension What for
ext-pdo Database access
ext-simplexml Parsing XML in the request body
ext-bcmath, ext-decimal Exact numeric types when binding parameters
ext-shmop Passing data to a child process through shared memory; a pipe is used without it

Checking the environment

php -v shows the version, php -m the extension list. swoole missing from that list is the most common reason a first run fails.

Three ways

Way When
Ready-made skeleton — create-project A new project; a working skeleton in one command
Through Docker The machine has no PHP, or not the extensions
From scratch — require winter-kernel Fitting into an existing project, your own layout, or wanting to understand every file

The ready-made skeleton

bash
composer create-project flytachi/winter my-app
cd my-app
php call run dev

Composer finishes the setup itself after the install — its output says so:

text
> @php call storage init
|	 storage ..................................................... [CREATED]
|	 storage/cache ............................................... [CREATED]
|	 storage/logs ................................................ [CREATED]
> chmod -R 777 storage
> @php call cfg init
|	 .env ........................................................ [CREATED]
|	 Old key    (empty)
| [✓] WINTER_KEY generated and saved to .env
|	 New key    bf4ee7d770a565c21598e57e87d46fde6637c0181cac13f4c37fd7b4c194c652

The directories exist and are writable, the .env is in place, and the key is yours rather than shared with someone else’s project. There is nothing left to install or configure.

The skeleton contains exactly what is built by hand below: a bootstrap.php with the application class, a call file, the PSR-4 Main\ → main/ mapping, a ready MainController, and monolog/monolog in require-dev — so logs have somewhere to go from the start.

You can stop reading here

If the skeleton suits you, move on to the Quickstart. The sections below are for building a project by hand, or for understanding what it is made of.

Through Docker, when there is no PHP

create-project is a PHP program too, but it can run in a throwaway container: the image carries PHP inside, and the project stays with you in the mounted directory.

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

--ignore-platform-reqs is needed because 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.

From there the project runs in its own container, where Swoole is already built, and console commands go the same throwaway way. The commands for all of it are in the Quickstart.

Building a project from scratch

1. The directory and the dependency

bash
mkdir my-app && cd my-app
composer require flytachi/winter-kernel

2. The code directory and autoloading

Winter imposes nothing: the directory and the namespace are yours to pick — in the examples below they are main/ and Main\.

bash
mkdir main

Now tie them together in composer.json:

composer.json
{
  "autoload": {
      "psr-4": {
          "Main\\": "main/"
      }
  },
  "require": {
      "php": ">=8.4",
      "flytachi/winter-kernel": "^4.0"
  }
}
bash
composer dump-autoload

3. The application class — bootstrap.php

This is the only configuration file. It pulls in the autoloader and declares what the application is made of:

bootstrap.php
<?php

declare(strict_types=1);

use Flytachi\Winter\Kernel\App\Attribute\EnableWeb;
use Flytachi\Winter\Kernel\WinterApplication;

require __DIR__ . '/vendor/autoload.php';

#[EnableWeb]
final class Application extends WinterApplication
{
  public static function main(array $argv): never
  {
      parent::run($argv);
  }
}

There are no hooks here that must be overridden — everything else lives in ordinary classes the scanner finds. What else can be declared alongside #[EnableWeb] is on the Application composition page.

This file’s directory becomes the project root: .env, storage/, resources/ and the start of the file walk are all measured from it.

4. The entry point — call

Every command goes through it, starting the server included:

call
#!/usr/bin/env php
<?php

if (PHP_VERSION_ID < 80400) {
  echo "Please use PHP version 8.4 or higher. Current: " . PHP_VERSION . "\n";
  exit(1);
}

chdir(__DIR__);
require './bootstrap.php';

Application::main($argv);
bash
chmod +x call

The chdir() is not optional: it anchors the relative paths — .env, storage/, resources/ — to the project directory rather than to wherever you ran the command from.

5. The environment and the directories

bash
php call cfg init       # .env from the template + a fresh WINTER_KEY
php call storage init   # the service directories inside storage/

The resulting .env is minimal:

.env
WINTER_KEY=<the generated key>
TIME_ZONE=UTC
DEBUG=true

The rest of the variables — database, logs, server settings — are added as they are needed; see Configuration.

6. The first controller

bash
php call make -c .Main   # → main/MainController.php

The generator writes a working stub: a hello() method carrying #[RequestMapping] with no arguments. Such an attribute mounts the method under the class name and answers every major HTTP method — you can check without opening the file:

bash
php call mapping show
text
 | [ Routes (5) ]
|	 GET     /main                                 → Main\MainController::hello
|	 POST    /main                                 → Main\MainController::hello
|	 PUT     /main                                 → Main\MainController::hello
|	 PATCH   /main                                 → Main\MainController::hello
|	 DELETE  /main                                 → Main\MainController::hello
| [ Routes ]

What you end up with

text
my-app/
├── bootstrap.php     — the application class: what it is made of
├── call              — the entry point for every command
├── composer.json     — autoloading and dependencies
├── .env              — the environment
├── main/             — your code
└── storage/          — the service directories

A resources/ directory for views and translations appears when you need it. What is responsible for what: Project structure.

The first run

bash
php call run dev

The server listens on port 8000 on every interface.

Open /main, not /

The root / does not answer until you declare a route there yourself — it returns an honest 404 Not Found [ GET / ]. The generated controller lives at http://localhost:8000/main; which paths are taken is always shown by php call mapping show.

The word dev turns on file watching: change any .php and the application restarts itself. In production it is started without it — php call run.

The address and port are overridden with flags:

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

What cfg init does

It is one command but several actions, and they are worth knowing because one of them cannot be undone.

Action Details
Tidies up composer.json name → project/<directory>, description → My Project <directory>, removes keywords and scripts, empties authors
Creates the .env From the template, if the file is not there yet; an existing one is left alone
Generates a WINTER_KEY Every time, over the current one
Drops a PhpStorm hint file vendor/.phpstorm.meta/.phpstorm.meta.php

Do not run cfg init on an existing project

The key is reissued unconditionally, and WINTER_KEY signs the data the kernel hands to child processes. Processes and daemons started under the old key fail the signature check once it changes.

When you need one specific step, use the specific command: cfg env -i creates the .env, cfg key -g reissues the key deliberately.

You cloned an existing project

After git clone the skeleton is already there, but everything that stays out of the repository has to be created again:

bash
composer install

php call cfg env -i       # create the .env if it is missing
php call cfg key -g       # generate your own WINTER_KEY
php call storage init     # create the directories inside storage/

php call run dev

Every installation has its own key

WINTER_KEY does not travel from someone else’s project or from the repository — generate it locally. The .env is not committed for the same reason.

The executable bit on call is kept in the repository itself, so chmod +x is normally unnecessary. If the file did arrive without it — through an archive, say — put it back: chmod +x call.

Packages as you need them

The kernel carries neither a database layer nor a Redis client: they are separate packages, installed when they are needed. The kernel picks them up on its own — there is nothing to switch on.

bash
composer require flytachi/winter-ppa      # database: repositories, entities, migrations
composer require flytachi/winter-redis    # Redis: stores, hashes, lists, streams
composer require flytachi/jwt             # JWT and JWKS

Until a package is installed, the commands that need it do not blow up with a stack trace — they say what is missing:

text
 | [!] The 'db' command needs the database layer, which is not installed.
| [i] Add it with:  composer require flytachi/winter-ppa

Where to go next: PPA, Redis, Ecosystem.

Two more things worth having

Shell completion — hints for the commands and their arguments:

bash
php call cfg completion -i

The Docker configuration — a Dockerfile, a docker-compose.yml and a docker/ directory with a Swoole entry point:

bash
php call cfg docker

The mode is picked by the DEV variable, and it is off by default:

bash
docker compose up            # production mode
DEV=true docker compose up   # development: restart on file change

Next