Skip to content

Application Lifecycle and Startup Hooks in Winter Boot

Every Winter Boot application begins with a single annotated class and a call to a runner. From that moment the framework scans your code, builds a complete bean graph, calls lifecycle hooks, and hands control to the runtime — whether that is a Swoole HTTP server, plain PHP-FPM, a CLI tool, or a migration runner. Knowing exactly what happens at each stage helps you write correct startup code and understand why the order of #[PostConstruct] and #[OnApplicationReady] matters.

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 locates and parses every application.yml (and profile-specific variants) in the configured configDirectory paths. The merged result is available through ApplicationContext::getProperty*() before any bean is created.

  3. Scan namespaces

    The runner walks the PSR-4 namespaces listed in scanNamespaces, skipping any prefixes in scanExcludeNamespaces. Every class carrying a known stereotype attribute is recorded as a ClassResource in the registry. If autoload is true, classes that have not been loaded by Composer yet are loaded on demand during this 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 fully constructed and its dependencies are injected, any public non-abstract method annotated with #[PostConstruct] is called. Use this hook for validation, connection setup, or derived-field initialisation that depends on injected values.

  7. Load modules

    Modules listed in application.yml under the modules key are initialised. Each module can contribute its own beans and namespaces to the context.

  8. Fire #[OnApplicationReady]

    Every bean whose class is annotated with #[OnApplicationReady] has its onApplicationReady() method called. This is the correct place for work that must happen after the entire context is ready — seeding data, starting background threads, registering routes, or opening connections.

  9. Start serving

    The runner hands off to the underlying runtime — starting the Swoole HTTP server, dispatching a single PHP-FPM request, executing CLI logic, or running migrations.


The entry-point class must be annotated with #[WinterBootApplication]. This attribute is the single source of truth for all startup configuration.

MyApplication.php
use dev\winterframework\stereotype\WinterBootApplication;
use dev\winterframework\core\app\WinterWebSwooleApplication;
#[WinterBootApplication(
// Directories searched for application.yml and logger.yml
configDirectory: [__DIR__ . '/../config'],
// PSR-4 namespaces to scan: [prefix, base-directory] pairs
scanNamespaces: [
['App\\', __DIR__ . '/../src'],
],
// Autoload classes not yet loaded by Composer during namespace scan
autoload: false,
// Namespace prefixes excluded from scanning (e.g. test namespaces in prod)
scanExcludeNamespaces: ['App\\Tests\\'],
// Active profile — selects application-{profile}.yml overrides
profile: null,
// Eagerly instantiate all beans at startup (slower start, faster first request)
eager: false
)]
class MyApplication {
public static function main(): void {
(new WinterWebSwooleApplication())->run(MyApplication::class);
}
}
  • `` array (required) List of directory paths to search for application.yml and logger.yml.

  • `` array (required) PSR-4 [prefix, directory] pairs to scan for stereotype attributes.

  • `` bool (default: false) Load classes from disk if not already in memory during scanning.

  • `` array (default: []) Namespace prefixes to skip entirely during scanning.

  • `` string | null (default: null) Active environment profile. Loads application-{profile}.yml overrides on top of the base configuration.

  • `` bool (default: false) Instantiate all beans at startup instead of lazily on first access. Results in a slower startup but a faster first request.


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

WinterWebSwooleApplication

High-performance HTTP server powered by OpenSwoole. Starts a persistent multi-worker server that handles concurrent requests in a single PHP process. The right choice for production APIs and microservices.

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

WinterWebApplication

Traditional PHP-FPM / Apache web runner. Bootstraps the full context once per request under a conventional web server. Use when Swoole is unavailable or when integrating with an existing PHP hosting environment.

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

WinterCliApplication

Command-line runner. Boots the full DI context, fires #[OnApplicationReady], then returns control so your CLI logic can execute. Ideal for console commands, one-off scripts, and integration test harnesses.

(new WinterCliApplication())->run(CliApplication::class);

WinterMigrationApplication

Database migration runner. Accepts --configDir, --sqlPath, and --migrationType CLI flags. Intentionally skips module loading and #[OnApplicationReady] events for a lean, fast migration run.

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

A single codebase can expose several starter classes, each bootstrapping a different slice of the application. This is a first-class pattern in Winter Boot — ideal for separating test, web, and worker entry points.

Multiple starters
// src/TestApplication.php
#[WinterBootApplication(
configDirectory: [__DIR__ . '/../config/test'],
scanNamespaces: [['App\\', __DIR__]],
scanExcludeNamespaces: ['App\\Web\\']
)]
class TestApplication {
public static function main(): void {
(new WinterCliApplication())->run(TestApplication::class);
}
}
// src/WebApplication.php
#[WinterBootApplication(
configDirectory: [__DIR__ . '/../config'],
scanNamespaces: [['App\\', __DIR__]],
eager: false
)]
class WebApplication {
public static function main(): void {
(new WinterWebSwooleApplication())->run(WebApplication::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 not declare a return type (it should return void).
DatabaseConnectionPool.php
use dev\winterframework\stereotype\{Service, Autowired, PostConstruct};
#[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();
}
}

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();
}
}
}

WinterModule is the base contract for optional framework modules that extend Winter Boot with additional capabilities. Modules are declared in your application.yml and are initialised after the main application context has been built.

Each module can register its own beans, scan additional namespaces, and integrate with the application context. Winter Boot’s built-in features — such as transaction management, caching, async tasks, and scheduling — are all delivered as modules.

application.yml — enabling modules
modules:
- dev\winterframework\pdox\PdoModule
- dev\winterframework\cache\CacheModule
- dev\winterframework\task\TaskModule

To create a custom module, implement the WinterModule interface and list your module class under modules in application.yml. The framework instantiates each module during the Load modules phase of startup and calls its initialisation methods in declaration order.

CustomAuditModule.php
use dev\winterframework\core\app\WinterModule;
use dev\winterframework\core\context\ApplicationContext;
class CustomAuditModule implements WinterModule {
public function init(ApplicationContext $context): void {
// Register additional beans or configure services here.
// Called after the main context is built, before OnApplicationReady fires.
}
}
application.yml — loading a custom module
modules:
- App\Module\CustomAuditModule