Skip to content

feat(middleware): auto-resolve transitive dependencies from registry #380

@usernane

Description

@usernane

Problem Statement

When a middleware declares dependencies via getDependencies(), assigning only that middleware to a route does not auto-include its dependencies. The developer must manually list the entire chain.

For example, if middleware E depends on D, D depends on C, C depends on B, B depends on A:

// Expected: assigning E pulls in D, C, B, A automatically
RouteOption::MIDDLEWARE => ['mw-e']

// Actual: only E runs. D, C, B, A are silently skipped.
// Developer must write:
RouteOption::MIDDLEWARE => ['mw-e', 'mw-d', 'mw-c', 'mw-b', 'mw-a']

This makes getDependencies() behave as an execution order hint rather than a true dependency declaration. A developer who assigns only E will get silent failures with no error or warning.

Proposed Solution

Add a resolveDependencies() step in RouterUri::getMiddleware() that walks the dependency graph (BFS) and pulls in missing middleware from the MiddlewareRegistry before sorting:

public function getMiddleware(): array {
    if (count($this->assignedMiddlewareList) != count($this->sortedMiddleeareList)) {
        $resolved = $this->resolveDependencies($this->assignedMiddlewareList);
        $this->sortedMiddleeareList = $this->sortByDependencies($resolved);
    }

    return $this->sortedMiddleeareList;
}

private function resolveDependencies(array $middlewareList): array {
    $byName = [];

    foreach ($middlewareList as $mw) {
        $byName[$mw->getName()] = $mw;
    }

    $queue = $middlewareList;

    while (!empty($queue)) {
        $current = array_shift($queue);

        foreach ($current->getDependencies() as $depName) {
            if (isset($byName[$depName])) {
                continue;
            }

            $dep = MiddlewareManager::getMiddleware($depName);

            if ($dep !== null) {
                $byName[$depName] = $dep;
                $queue[] = $dep;
            }
        }
    }

    return array_values($byName);
}

Execution order is determined by the existing sortByDependencies() (Kahn's algorithm):

  • Dependency order takes precedence (A before B if B depends on A)
  • Priority is used as tiebreaker for unrelated middleware
  • Circular dependencies are detected and throw RoutingException

Cycle safety: $byName acts as a visited set — each middleware is processed at most once. The existing sortByDependencies() catches any circular dependency that slips through.

Alternatives Considered

  1. Keep current behavior, document it — but this defeats the purpose of getDependencies() and forces developers to maintain dependency knowledge manually.
  2. Throw an error when a dependency is not assigned — more explicit but less ergonomic. Developers would still need to list the full chain.

Additional Context

Verified with a 5-middleware chain example (A→B→C→D→E). When all are assigned (even in reverse order), the topological sort correctly produces: before A→B→C→D→E, after E→D→C→B→A. The proposed change makes this work even when only E is assigned.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprove performace or existing feature.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions