Repository navigation
Refactor Router & ModuleLoader: Introduce Route as Single-Route Entity and Decouple Module Loading #383
Description
Refactor Router & ModuleLoader Architecture (Route Object–Centric)
Summary
Refactor Quantum’s routing subsystem to be architecturally correct, explicit, and maintainable while preserving all existing behavior and public APIs.
This refactor focuses only on routing and module-loading concerns and is intentionally scoped to avoid overlap with the broader App boot pipeline work.
The key outcome is to make Route a first-class domain object, clarify ownership boundaries between ModuleLoader, route construction, and Router, and stabilize RouteController as the global routing state holder.
Target PHP version: 7.4+
Motivation
The current routing flow has several structural issues:
Routeacts as both:- a route definition DSL, and
- a container for multiple routes
ModuleLoader:- loads module configuration ✔
- executes route closures ❌
- mutates routing state ❌
- Routes are flattened into arrays early, losing structure and intent
- Responsibilities are implicit, making safe refactoring difficult
Despite this, the current system works and must not be broken.
This refactor preserves behavior while correcting ownership and data flow.
Hard Constraints (Must Not Change)
These constraints are non-negotiable:
RouteControllermust remain- Static routing state must remain
- All router helpers must continue to work
- Route definition syntax in modules must not change
- No providers, no kernel rewrite, no breaking changes
Core Design Decisions
1. Route Represents a Single Route (Entity)
Route becomes a single-route domain object, encapsulating:
- path / pattern
- HTTP method(s)
- module
- controller / action or callback
- middlewares
- cache settings
- name
- group (if any)
- runtime match data (params, uri, pattern)
Route does not:
- store other routes
- manage groups
- execute closures
- act as a collection
2. RouteBuilder Handles Route Composition
A new internal component (name flexible, e.g. RouteBuilder) is responsible for:
- executing module route definition closures
- managing groups
- applying middlewares
- producing Route objects
- returning
Route[]
This preserves the current DSL unchanged, while moving complexity out of Route.
3. ModuleLoader Stops Touching Routes
ModuleLoader responsibilities are reduced to:
- loading module configs
- loading module dependency definitions
- loading module route closures only
It must not:
- instantiate
Route - execute route closures
- build routes
- interact with
RouterorRouteController
4. RouteController Remains the Global Routing State
RouteController remains the single global routing state holder, because:
- all router helpers depend on it
- controllers, views, middleware rely on static access
- removing it would be a breaking rewrite
Internal Change Only
Before:
RouteController::$routes // array[]
RouteController::$currentRoute // arrayAfter:
/** @var Route[] */
RouteController::$routes;
/** @var Route|null */
RouteController::$currentRoute;Helpers are updated internally to read from Route objects, but their APIs remain unchanged.
5. Router Matches Requests Against Route Objects
Router:
- receives
Route[]fromRouteController - performs matching
- selects a single
Route - sets it as
RouteController::$currentRoute
Matching logic remains the same, but operates on objects instead of arrays.
Final Responsibility Map
| Component | Responsibility |
|---|---|
| ModuleLoader | Load module configs, dependencies, route closures |
| RouteBuilder | Execute closures, build Route[] |
| Route | Represent a single route |
| RouteController | Store global routing state |
| Router | Match Request → Route |
| RouteDispatcher | Execute controller / callback |
| Helpers | Read-only access to current Route |
Boot-Time Routing Flow (Web)
ModuleLoaderloads enabled module route closuresRouteBuilderexecutes closures →Route[]RouteController::setRoutes(Route[])Router::findRoute()matches requestRouteController::setCurrentRoute(Route)- Helpers & dispatcher read from
RouteController
What This Ticket Does NOT Do
- Does not remove static helpers
- Does not remove
RouteController - Does not refactor App boot pipeline
- Does not introduce providers
- Does not change route DSL
- Does not rewrite middleware or dispatcher
Acceptance Criteria
- Routes are represented internally as
Routeobjects ModuleLoaderno longer builds or mutates routesRouteControllerstoresRoute[]andRoute- All existing helpers continue to work
- No behavior change in routing
- No breaking public API changes
Guiding Rule
Modules define routes.
Builders construct routes.
Router matches routes.
RouteController stores routes.
This rule must hold across the routing subsystem.
What Changes in WebAppTrait::loadModules()
Before
$modulesRoutes = $moduleLoader->loadModulesRoutes();
Router::setRoutes($modulesRoutes);
After (conceptual)
$closures = $moduleLoader->loadModuleRouteClosures();
$routes = $routeBuilder->build($closures);
RouteController::setRoutes($routes);
Activity