Skip to content

Application Lifecycle and Startup Hooks in Winter Boot

Every Winter Boot application starts from a single annotated class and a call to a runner. The framework then scans your code, builds the bean graph, calls lifecycle hooks, and hands control to the runtime — the built-in HTTP server, PHP-FPM, or the migration runner. Knowing what happens at each stage helps you put startup code in the right place, and explains why #[PostConstruct] and #[OnApplicationReady] run when they do.

The container follows a fixed, deterministic sequence every time your application boots.

  1. Bootstrap the runner

    Instantiate a runner class (e.g. WinterWebSwooleApplication) and call run(MyApplication::class). The runner reads the #[WinterBootApplication] attribute to learn which config directories and namespaces to use.

  2. Load configuration

    The runner reads application.yml (or application-{profile}.yml when a profile is set) from every directory in configDirectory, merging them in order. The result is available through ApplicationContext::getProperty*() before any bean is created.

  3. Scan namespaces

    The runner walks the namespaces listed in scanNamespaces (plus those contributed by enabled modules), skipping any prefixes in scanExcludeNamespaces. Every class carrying a known stereotype attribute is registered. If autoload is true, files Composer cannot autoload are required during the scan.

  4. Build the application context

    The WinterApplicationContext is constructed from the scanned resources. If eager is true, every bean is instantiated immediately; otherwise beans are instantiated lazily on first access.

  5. Wire dependencies

    For each instantiated bean the container resolves all #[Autowired] properties and #[Value] properties, injecting the correct objects and scalar values.

  6. Call #[PostConstruct] methods

    After a bean is constructed and its dependencies are injected, its #[PostConstruct] method is called. Use this hook for validation, connection setup, or anything else that depends on injected values.

  7. Load modules

    Modules enabled under the modules key in application.yml are initialized and started. See Module System.

  8. Fire #[OnApplicationReady]

    Every bean whose class is annotated with #[OnApplicationReady] has its onApplicationReady() method called. This is the place for work that needs the whole context — warming caches, seeding data, or checking connections.

  9. Start serving

    The runner hands off to the runtime — starting the built-in HTTP server, dispatching a single PHP-FPM request, or running migrations.


The entry-point class must be annotated with #[WinterBootApplication]. It tells the framework where the config files are (configDirectory) and which namespaces to scan (scanNamespaces), and optionally sets a profile, exclusions, and eager loading. See Configuration for every option.


Choose the runner class that matches how your application is deployed.

WinterWebSwooleApplication

Built-in HTTP server (recommended). Starts a long-running server with a pool of worker processes. Required for #[Async], #[Scheduled], daemon threads, and the local KV/queue stores. The right choice for production APIs.

(new WinterWebSwooleApplication())->run(MyApplication::class);

WinterWebApplication

PHP-FPM / Apache runner. Bootstraps the full context once per request under a conventional web server. Use it only when you must run inside existing PHP hosting; background features are not available.

(new WinterWebApplication())->run(MyApplication::class);

WinterMigrationApplication

Migration runner. Runs SQL or OpenSearch migrations and exits. Requires --configDir and --sqlPath; --migrationType is optional. Skips modules and #[OnApplicationReady] for a fast run. See Migrations.

Terminal window
php migrate.php --configDir=config --sqlPath=db/migrations

One codebase can have several entry-point classes, each with its own config and scan settings. A common pair is the web server and a migration runner that ships in the same image:

Multiple starters
// src/WebApplication.php
#[WinterBootApplication(
configDirectory: [__DIR__ . '/../config'],
scanNamespaces: [['App\\', __DIR__]]
)]
class WebApplication {
public static function main(): void {
(new WinterWebSwooleApplication())->run(WebApplication::class);
}
}
// src/MigrationApplication.php
#[WinterBootApplication(
configDirectory: [__DIR__ . '/../config'],
scanNamespaces: [['App\\Db\\', __DIR__ . '/Db']]
)]
class MigrationApplication {
public static function main(): void {
(new WinterMigrationApplication())->run(MigrationApplication::class);
}
}

#[PostConstruct] is a method-level attribute. The container calls the annotated method immediately after the bean is instantiated and all #[Autowired] and #[Value] injections have been applied.

The framework enforces these rules at startup:

  • The method must be public.
  • The method must not be abstract or a constructor.
  • The method must return void (or declare no return type).
DatabaseConnectionPool.php
use dev\winterframework\core\context\ApplicationContext;
use dev\winterframework\stereotype\{Service, Autowired, PostConstruct, Value};
#[Service]
class DatabaseConnectionPool {
#[Autowired]
private ApplicationContext $appCtx;
#[Value('${db.poolSize}')]
private int $poolSize;
private array $connections = [];
#[PostConstruct]
public function init(): void {
// $this->poolSize and $this->appCtx are already injected here
for ($i = 0; $i < $this->poolSize; $i++) {
$this->connections[] = $this->openConnection();
}
}
private function openConnection(): Connection {
$host = $this->appCtx->getPropertyStr('db.host');
return new Connection($host);
}
}

#[OnApplicationReady] is a class-level attribute. The framework calls onApplicationReady() on every annotated bean after the entire application context has been built, modules have been loaded, and the container is fully operational.

The framework enforces these requirements at startup:

  • The class must also carry #[Component], #[Service], or #[Configuration].
  • The class must implement the ApplicationReadyEvent interface (which declares onApplicationReady(): void).
AppStartupTasks.php
use dev\winterframework\stereotype\{Component, OnApplicationReady};
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\core\app\ApplicationReadyEvent;
#[Component]
#[OnApplicationReady]
class AppStartupTasks implements ApplicationReadyEvent {
#[Autowired]
private CacheWarmupService $cacheWarmup;
#[Autowired]
private FeatureFlagService $featureFlags;
public function onApplicationReady(): void {
// Every bean is available — safe to call other services
$this->featureFlags->loadFromDatabase();
$this->cacheWarmup->primeProductCatalog();
}
}

#[OnApplicationReady] runs once, in the master process. Code that needs to run inside each worker process, such as opening a per-worker connection, warming a worker-local cache, or flushing buffers on the way out, uses the worker lifecycle attributes instead. Both are class-level and only take effect with the WinterWebSwooleApplication runner.

  • #[OnWorkerStart]: the class implements WorkerStartEvent (onWorkerStart(int $workerId): void), called as each worker starts, after the framework’s own worker setup.
  • #[OnWorkerStop]: the class implements WorkerStopEvent (onWorkerStop(int $workerId): void), called as each worker stops (shutdown, reload, max_request recycling).

As with #[OnApplicationReady], the class must also carry #[Component], #[Service], or #[Configuration]. One class may carry both attributes.

WorkerResources.php
use dev\winterframework\stereotype\{Component, OnWorkerStart, OnWorkerStop};
use dev\winterframework\core\app\{WorkerStartEvent, WorkerStopEvent};
#[Component]
#[OnWorkerStart]
#[OnWorkerStop]
class WorkerResources implements WorkerStartEvent, WorkerStopEvent {
public function onWorkerStart(int $workerId): void {
// Runs in every HTTP worker and Swoole task worker.
}
public function onWorkerStop(int $workerId): void {
// Release what onWorkerStart() opened.
}
}

Winter Boot automatically registers bean destroy methods to run on process exit. When a #[Bean] factory method declares a destroyMethod, the framework registers it with PHP’s shutdown mechanism. On process exit, all registered destroy methods are called in registration order.

ResourceConfig.php
use dev\winterframework\stereotype\{Configuration, Bean};
#[Configuration]
class ResourceConfig {
#[Bean(initMethod: "open", destroyMethod: "close")]
public function connectionPool(): ConnectionPool {
return new ConnectionPool(maxSize: 10);
}
}
ConnectionPool.php
class ConnectionPool {
public function open(): void {
// Called right after the bean is created
}
public function close(): void {
// Called automatically on process shutdown
foreach ($this->connections as $conn) {
$conn->disconnect();
}
}
}

Libraries such as Kafka, Redis, or Doctrine plug into this lifecycle as modules. Enabled modules contribute their namespaces before the scan, are initialized after the context is built, and are started just before #[OnApplicationReady] fires:

config/application.yml
modules:
- module: dev\winterframework\kafka\KafkaModule
enabled: true

See Module System for the WinterModule contract and how to write your own.