A minimal PHP framework for code that explains itself.
Verge::app()->group('/weather', function ($app) {
$app->singleton(WeatherService::class, WeatherApi::class);
$app->controller(WeatherController::class);
$app->get('/health', fn () => 'Weather service is running');
})->run(); Four principles that guide every design decision
Build features by composing small, focused modules. Each module registers its own bindings, routes, and event listeners.
$app->module(fn(App $app) => [
$app->singleton(Cache::class),
$app->get('/cache', CacheCtrl::class),
]); No auto-discovery, no hidden conventions, no surprise behavior. Every route, binding, and middleware is explicitly registered.
// You register it, you see it
$app->bind(Logger::class, FileLogger::class);
$app->use(CorsMiddleware::class);
$app->get('/api', ApiCtrl::class); Clear, readable APIs that say what they do. Method names match their behavior. No framework-specific DSL to learn.
$app->group('/admin', fn() => [
$app->get('/users', ListUsers::class),
$app->post('/users', CreateUser::class),
])->use(AdminOnly::class); Four core primitives—Container, Router, Middleware, Events—combine to build anything. Create packages, add drivers, hook into lifecycle.
$app->on('app.ready', fn() =>
$app->get('/swagger', SwaggerGen::class)
);
$app->driver('cache', 'redis', RedisDriver::class); Everything in Verge is built from these composable building blocks
PSR-11 dependency injection with singleton, scoped, and transient bindings
$app->singleton(Cache::class) Fast route matching with parameters, groups, and named routes
$app->get('/users/{id}', ...) PSR-15 pipeline for auth, CORS, logging, and request transformation
$app->use(AuthMiddleware::class) PSR-14 dispatcher with wildcard listeners for decoupled hooks
$app->on('user.created', ...) The driver pattern is built into the framework. Swap service implementations via environment variables. Build your own driver-based services using the same pattern the framework uses for cache and logging.
use App\Queue\SyncQueue;
use App\Queue\RedisQueue;
app()
->driver('queue', 'sync', fn() => new SyncQueue())
->driver('queue', 'redis', fn(App $app) => new RedisQueue(
$app->env('REDIS_URL')
))
->defaultDriver('queue', 'sync');
// Switch via QUEUE_DRIVER environment variable
$queue = app()->driver('queue'); Listen to native framework events or dispatch your own. Hook into the request lifecycle, react to application events, and build event-driven features without external dependencies.
// Listen to native framework events
app()->listen('request.received', function(Request $req) {
logger()->info('Request received', ['path' => $req->path()]);
});
// Dispatch and handle custom events
app()->listen('order.created', fn($order) => sendEmail($order));
app()->post('/orders', function(EventDispatcher $events) {
$events->dispatch('order.created', ['id' => 123]);
return ['status' => 'created'];
});