Skip to content

Refactor Router & ModuleLoader: Introduce Route as Single-Route Entity and Decouple Module Loading #383

Description

@armanist

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:

  • Route acts 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:

  • RouteController must 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 Router or RouteController

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 // array

After:

/** @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[] from RouteController
  • 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)

  1. ModuleLoader loads enabled module route closures
  2. RouteBuilder executes closures → Route[]
  3. RouteController::setRoutes(Route[])
  4. Router::findRoute() matches request
  5. RouteController::setCurrentRoute(Route)
  6. 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 Route objects
  • ModuleLoader no longer builds or mutates routes
  • RouteController stores Route[] and Route
  • 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);

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions