Getting started

Project structure

A Winter project is flat: four files at the root and one directory for your code. There is no config/, no routes/, no mandatory app/. The layout inside your own directory is yours to choose — the framework finds classes by walking files, not by reading a list in a config.

Your code main/Service data storage/Not scanned vendor · storage · resources

After installation

text
my-app/
├── bootstrap.php      the application class: what it consists of
├── call               the entry point for every command
├── composer.json      autoloading and dependencies
├── .env               environment variables

├── main/              your code, namespace Main\
│   └── MainController.php

├── storage/           service data, never committed
│   ├── cache/
│   └── logs/

└── vendor/

Everything else appears when needed and is created by you: resources/ for templates and translations, docker/ for the container, directories inside main/ for however you prefer to arrange classes.

The files at the root

bootstrap.php

The application class. It declares what the application consists of and serves as the entry point.

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

The #[Enable*] attributes are the only place the application’s make-up is listed. See Components.

call

Runs everything: the server, console commands, generators. Inside are three lines: pull in the autoloader, pull in bootstrap.php, hand control to the application class.

bash
php call run dev          # development server
php call mapping show     # the route table
php call make -c .User    # create a controller

The file contains chdir(__DIR__), so commands work from any directory: php /path/to/my-app/call run starts the server with the right paths.

composer.json

Besides dependencies it defines PSR-4 — which namespace maps to which directory:

composer.json
{
  "autoload": {
      "psr-4": {
          "Main\\": "main/"
      }
  }
}

.env

Values that differ between environments. The full list is on the Configuration page. Not committed.

main/ — your code

The one directory you fill. Its internal structure is yours: the framework demands neither Controllers/ nor Models/ nor any other name.

text
main/
├── MainController.php
├── User/
│   ├── UserController.php
│   ├── UserService.php
│   └── UserRepository.php
└── Order/
  ├── OrderController.php
  └── OrderProcess.php

Here classes are grouped by domain rather than by type — but a by-type layout (Controllers/, Services/, Repositories/) works exactly the same. To the framework there is no difference.

Several roots

One Main\ prefix is not the limit. Add your own to composer.json and they start working after composer dump-autoload:

composer.json
"psr-4": {
  "Main\\": "main/",
  "Api\\":  "api/",
  "Admin\\": "admin/"
}

Separate roots are convenient when parts of the application live their own lives: a public API and an admin panel have different routes, different middleware and often different people.

Where make puts files

The generator works from the namespace, not from the type: the file path is derived from the name you give it.

bash
php call make -c .User            # main/UserController.php
php call make -c main.api.User    # main/Api/UserController.php

If a conventionally named directory already exists, the generator puts the file there:

Type Looks for
Controller Controllers/, Controller/
Service Services/, Service/
Middleware Middlewares/, Controllers/Middlewares/
Repository Repositories/, Repository/
Entity, DTO Entity/, Entity/Dto/
Process, daemon Processes/, Daemons/

No such directory — the file lands next to it, by namespace. You do not need to create them up front.

What gets scanned

At startup the framework walks the whole project from the root, reads every .php and loads the ones that declare a class. That is how controllers, configurations and processes are found — none of them needs registering.

Three directories are excluded from the walk:

Directory Why
vendor/ Dependencies take no part in finding application classes
storage/ It holds generated code — the walk would read the previous walk’s output
resources/ It holds templates: PHP files by nature, not classes

Templates are excluded for correctness, not speed

The scanner looks for a class declaration in every .php and loads the file where it finds one. For a template that means executing it: it prints its markup into the application’s output and runs whatever sits at its top level. This was observed on a live project, not theorised.

So keep templates under resources/. A template directory placed inside main/ falls under the walk.

Hence the practical rule: anything that is not an application class belongs in resources/ or storage/. Scripts, dumps and one-off migration files at the project root will be read and, if they declare a class, loaded.

storage/ — service data

Directory What is there Created by
storage/cache/ Application cache — data you put there yourself storage init
storage/logs/ Log files when LOG_OUTPUT=file storage init
storage/runnable/ Lock files for single-instance processes Appears when such a process first runs

The whole directory is in .gitignore, but the .gitignore file itself is committed — so the structure comes back after git clone.

bash
php call storage init     # create the directories
php call storage clean    # empty them, keeping cache/ and logs/

The boot cache does not live here

The class list (di.php), the #[Async] proxies and everything else the framework generates at startup are written to the system temporary directory by default — /tmp/flytachi.winter.volatile.<project-name> — not to storage/.

That keeps generated code out of the image and out of the way across reboots. The practical consequence: clearing storage/ to reset it is pointless — use php call di clean.

You can move it inside the project with isTmpVolatile: false in Kernel::init(), but then the directory must be writable by the user the application runs as.

resources/ — templates and translations

The directory does not exist initially; create it when you need it.

Path What is there
resources/views/ Templates and layouts for ResponseView
resources/lang/ Translation dictionaries: ru.php, en.php
resources/static/ Static files, if the application serves them

Serving static files is switched on explicitly, in the web-layer configuration:

php
public function configureServer(ServerSettings $server): void
{
  $server->staticPath('resources/static');
}

Those requests are served directly and never reach PHP: middleware, CORS and request logging do not apply to them.

Container files

php call cfg docker adds a ready-made build to the project:

text
my-app/
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
└── docker/
  ├── entrypoint.sh        startup: dev or production
  ├── php-opcache.ini      opcache settings for production
  ├── php-memory.ini
  └── dependencies/        your package-installation scripts

The mode is chosen by a variable: DEV=true docker compose up for development with restart-on-change, docker compose up for production with opcache.

PHP extensions and database drivers are installed by the scripts in dependencies/: what ships with it, how to drop what you do not need and how to write your own is in cfg docker.

Custom paths

The default layout is derived from where the application class sits. If it does not suit you, override it in configure():

bootstrap.php
protected static function configure(ApplicationArguments $args): void
{
  Kernel::init(
      pathRoot:     __DIR__,
      pathResource: __DIR__ . '/assets',
      pathStorage:  '/var/lib/my-app',
  );
}

Moving storage outside the project is usually needed in a container, where the code sits on a read-only layer and writes have to go to a mounted volume.

Next