Framework Bootstrap
Verge uses its own module system to bootstrap the framework. When you create a new App instance, you’re already running modules—this is how the framework eats its own dog food.
App Lifecycle
Section titled “App Lifecycle”- App construction: Container created,
AppBuilderruns - Your modules: Added via
$app->module() - First request:
boot()called,app.readyfires - Request handling: Router matches, middleware runs, handler executes
Everything before step 3 is registration. Everything after is execution.
The Bootstrap Process
Section titled “The Bootstrap Process”When you instantiate App, this happens:
$app = new App();- A new container is created
- The
Appinstance is registered in the container AppBuilderis invoked to load framework modules
That’s it. The framework is just a collection of modules registered on the container.
AppBuilder
Section titled “AppBuilder”The AppBuilder class is Verge’s internal bootstrap module. Here’s what it looks like:
class AppBuilder{ public function __invoke(App $app): void { $app->module([ EnvModule::class, ConfigModule::class, RoutingModule::class, HttpModule::class, EventsModule::class, CacheModule::class, LogModule::class, ClockModule::class, ConsoleModule::class, ]); }}Each module registers one piece of framework functionality. They run in order, with each building on what came before.
Framework Modules
Section titled “Framework Modules”Here’s what each framework module does:
EnvModule
Section titled “EnvModule”Loads environment variables and registers the Env service:
$app->singleton(Env::class, fn() => new Env());$app->bind(EnvInterface::class, fn($app) => $app->make(Env::class));This runs first because other modules need access to environment variables.
ConfigModule
Section titled “ConfigModule”Builds application configuration from environment and config files:
$app->singleton(Config::class, fn() => new Config());Depends on EnvModule for reading configuration from environment variables.
RoutingModule
Section titled “RoutingModule”Registers the router for URL-to-handler mapping:
$app->singleton(Router::class, fn() => new Router());$app->bind(RouterInterface::class, fn($app) => $app->make(Router::class));HttpModule
Section titled “HttpModule”Provides HTTP factories and the request handler:
$app->singleton(HttpFactory::class, fn() => new HttpFactory());$app->bind(RequestHandlerInterface::class, fn($app) => new RequestHandler($app));EventsModule
Section titled “EventsModule”Registers the event dispatcher:
$app->singleton(EventDispatcher::class, fn() => new EventDispatcher());$app->bind(EventDispatcherInterface::class, fn($app) => $app->make(EventDispatcher::class));CacheModule
Section titled “CacheModule”Sets up cache drivers:
$app->driver('cache', 'memory', fn() => new MemoryCacheDriver());$app->defaultDriver('cache', 'memory');$app->singleton(CacheInterface::class, fn() => $app->driver('cache'));LogModule
Section titled “LogModule”Configures logging with drivers:
$app->driver('log', 'stream', fn() => new StreamLogDriver());$app->defaultDriver('log', 'stream');$app->singleton(LoggerInterface::class, fn() => new Logger($app->driver('log')));ClockModule
Section titled “ClockModule”Provides the PSR-20 clock:
$app->singleton(Clock::class, fn() => new Clock());$app->bind(ClockInterface::class, fn($app) => $app->make(Clock::class));ConsoleModule
Section titled “ConsoleModule”Registers CLI commands:
$app->command('routes:list', RoutesListCommand::class);$app->command('cache:warm', CacheWarmCommand::class);$app->command('cache:clear', CacheClearCommand::class);The app.ready Event
Section titled “The app.ready Event”After all modules are loaded and the first request arrives, Verge fires the app.ready event:
// Inside App::boot()protected function boot(): void{ if ($this->booted) { return; }
$this->booted = true; $this->emit('app.ready');}This event fires exactly once. It’s your hook for any setup that needs the complete application context—all services registered, all routes defined.
Common uses:
$app->ready(function () use ($app) { // Generate API documentation from registered routes $app->get('/api/docs', fn() => generateDocs($app->routes()));});
$app->ready(function () use ($app) { // Register admin routes that inspect the application $app->get('/admin/health', fn() => checkHealth($app));});Why This Design?
Section titled “Why This Design?”The framework bootstraps itself with modules for several reasons:
Consistency
Section titled “Consistency”Your code uses the same patterns as framework code. There’s no special bootstrap process for the framework versus your application—it’s all modules.
Transparency
Section titled “Transparency”You can read AppBuilder to see exactly what the framework provides. No hidden initialization, no magic loading.
Extensibility
Section titled “Extensibility”You can replace or extend framework modules. Want a different cache driver by default? Register your own cache module after the framework’s.
Predictability
Section titled “Predictability”Modules run in order. If you need to override something, register your module after the framework’s and rebind the service.
Customizing the Bootstrap
Section titled “Customizing the Bootstrap”You can add modules after the framework bootstraps:
$app = new App();
// Add your modules$app->module([ DatabaseModule::class, AuthModule::class, MyAppModule::class,]);
$app->run();Or extend the framework by rebinding services:
$app = new App();
// Replace the default cache with Redis$app->driver('cache', 'redis', fn() => new RedisCacheDriver());$app->defaultDriver('cache', 'redis');
$app->run();The framework modules have already run. Your modules build on top of them or override specific bindings.