Application components
A Winter application is not necessarily a web server. It is assembled from components, and the web is only one of four: long-lived processes, supervised worker fleets and a task scheduler can run alongside it. Any of them can be the only one.
What components are and why
A component is a self-contained part of the application with its own way of working: the web answers requests, a process runs a loop, a daemon holds a fleet of workers, the scheduler starts tasks by the clock.
The problem. An application rarely has just one shape. A service with an API
almost always needs something else: draining a queue, polling a device, cleaning old
records at night. Usually that is solved from outside — separate scripts under
cron, systemd units, a third-party supervisor. And then one application becomes
several deployments: different startup methods, different configurations, different
logs, with the shared code dragged between them as a package.
The solution. Winter describes the application’s make-up inside the
application. The entry class lists with attributes what it consists of, and
php call run raises all of it together, under one configuration, with a shared
dependency container and shared logs. Need a worker — that is a line in the
manifest, not a new deployment.
The manifest
Everything is declared with attributes on the application class:
<?php
use Flytachi\Winter\Kernel\App\Attribute\EnableDaemon;
use Flytachi\Winter\Kernel\App\Attribute\EnableProcess;
use Flytachi\Winter\Kernel\App\Attribute\EnableScheduler;
use Flytachi\Winter\Kernel\App\Attribute\EnableWeb;
use Flytachi\Winter\Kernel\WinterApplication;
require __DIR__ . '/vendor/autoload.php';
#[EnableWeb] // the HTTP server
#[EnableScheduler] // scheduled tasks
#[EnableProcess(\Main\Process\SnmpPoller::class)] // one worker
#[EnableDaemon(\Main\Process\Emails::class)] // a fleet of workers
final class Application extends WinterApplication
{
public static function main(array $argv): never
{
parent::run($argv);
}
}That is the whole launcher file. There are no hooks to override: settings live in ordinary classes the scanner finds — see Dependency injection and Web-layer configuration.
The four components
| Attribute | What it raises | Repeatable |
|---|---|---|
#[EnableWeb] |
The HTTP server | no |
#[EnableProcess(Class)] |
One long-lived process | yes |
#[EnableDaemon(Class)] |
A supervised fleet of workers | yes |
#[EnableScheduler] |
The #[Scheduled] task scheduler |
no |
Repeatable means literally that: several different processes and several different daemons are declared with several lines.
#[EnableProcess(\Main\Process\SnmpPoller::class)]
#[EnableProcess(\Main\Process\Heartbeat::class)]
#[EnableDaemon(\Main\Process\Emails::class)]
#[EnableDaemon(\Main\Process\Webhooks::class)]#[EnableWeb] takes neither an address nor a port — that is a property of the
deployment, not of the application. The address is set by the --host / --port
flags, by environment variables or by the web-layer configurer.
What is not a component
Two attributes look similar but raise nothing:
| Attribute | What it does |
|---|---|
#[EnableAsync] |
Enables proxying of #[Async] methods — see Async calls |
#[EnableActuator] |
Adds diagnostic endpoints to the web tier — see Actuator / Health |
Startup
One command raises everything listed in the manifest:
php call run # production mode
php call run dev # development: restart on .php changesWhen the manifest includes the web tier, the HTTP server becomes the main process and the other components run beside it, under the same master’s supervision: a worker dies, it is brought back; the application is stopped, they all stop.
An application without a web tier
Remove #[EnableWeb] and the application comes up headless: processes, daemons and
the scheduler only.
#[EnableScheduler]
#[EnableDaemon(\Main\Process\Emails::class)]
final class Application extends WinterApplication { /* ... */ }Such an application still uses the container, the configuration, logging and database access — it simply does not listen on a port. That is the ordinary way to deploy a dedicated queue handler or a nightly routine alongside the main service.
An empty manifest is an error
At least one component has to be declared. An application with no #[Enable*] at
all will not start and says so directly: No components declared, with the list of
available attributes.
What to choose
The four tools are easy to confuse, because all of them “do something in the background”. They separate on the question of when the work should run.
| You need to | Tool |
|---|---|
| Do a piece of work without keeping the client waiting | #[Async] |
| Run one endless loop: listen to a queue, poll a device | A process |
| The same, but in several hands, with crashed ones restarted | A daemon |
| Run on a schedule: every 5 minutes, every night at 3:00 | The scheduler |
The boundaries are worth spelling out, because that is where the mistakes happen:
#[Async] is not a background job. The method goes into its own coroutine and
interleaves with the calling code on I/O waits, but the work stays inside the same
worker and lives exactly as long as it does: a restart loses it, it will not move
to another machine, and it has no queue. Good for not making the client wait; not
good for guaranteeing an email is delivered — that needs a process or a daemon with
a queue.
Process versus daemon. A process is one instance; a daemon is several under supervision, with a restart policy and scaling. If the work divides into independent units and one worker is not enough, take a daemon; if the task is one by nature — listening on a single socket, holding a single connection — take a process.
The scheduler versus a process with sleep(). A loop with sleep(300) inside a
process looks like the simple answer, but with it you take responsibility for
keeping a task from overlapping itself, for what happens after a restart, and for
schedules expressed in wall-clock terms. The scheduler already handles all of that.
Next
- Processes — one long-lived worker
- Daemons — a supervised fleet of workers
- Scheduler — tasks on a schedule
- Async calls — not making the client wait for a slow operation
- Runtime — how this executes under Swoole