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
composer create-project flytachi/winter my-app
cd my-app
php call run devComposer finishes the setup itself after the install — its output says so:
> @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 bf4ee7d770a565c21598e57e87d46fde6637c0181cac13f4c37fd7b4c194c652The 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.
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
mkdir my-app && cd my-app
composer require flytachi/winter-kernel2. 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\.
mkdir mainNow tie them together in composer.json:
{
"autoload": {
"psr-4": {
"Main\\": "main/"
}
},
"require": {
"php": ">=8.4",
"flytachi/winter-kernel": "^4.0"
}
}composer dump-autoload3. The application class — bootstrap.php
This is the only configuration file. It pulls in the autoloader and declares what the application is made of:
<?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:
#!/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);chmod +x callThe 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
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:
WINTER_KEY=<the generated key>
TIME_ZONE=UTC
DEBUG=trueThe rest of the variables — database, logs, server settings — are added as they are needed; see Configuration.
6. The first controller
php call make -c .Main # → main/MainController.phpThe 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:
php call mapping show | [ 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
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 directoriesA resources/ directory for views and translations appears when you need it. What is
responsible for what: Project structure.
The first run
php call run devThe 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:
php call run dev --port=9000
php call run dev --host=127.0.0.1 --port=9000 # local onlyWhat 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:
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 devEvery 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.
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 JWKSUntil a package is installed, the commands that need it do not blow up with a stack trace — they say what is missing:
| [!] The 'db' command needs the database layer, which is not installed.
| [i] Add it with: composer require flytachi/winter-ppaWhere to go next: PPA, Redis, Ecosystem.
Two more things worth having
Shell completion — hints for the commands and their arguments:
php call cfg completion -iThe Docker configuration — a Dockerfile, a docker-compose.yml and a docker/
directory with a Swoole entry point:
php call cfg dockerThe mode is picked by the DEV variable, and it is off by default:
docker compose up # production mode
DEV=true docker compose up # development: restart on file changeNext
- Quickstart — the first route in five minutes
- Project structure — what is responsible for what
- Application composition — web, processes, daemons, scheduler
- Configuration — the environment variables