0

Route dispatching — finding the route, calling the action

Advanced5 min read·fw-03-003

Concept

Route dispatching takes the matched route and calls its action, returning a response. It's the bridge between the router (which found the route) and the rest of the framework (which runs the action).

Action types: A route's action can be:

  • A Closure: Call directly.
  • 'ControllerClass@method' string: Resolve the controller from the container, call the method.
  • [ControllerClass::class, 'method'] array: Same, just typed differently.
  • An invokable class (single __invoke() method): Resolve from container, call __invoke().

Resolving controllers: Controllers should be resolved through the container so their dependencies are injected. $container->make(ControllerClass::class)->method($params).

Parameter injection: Route parameters captured from the URI need to be passed to the action. For closures, inject them as arguments by name. For controller methods, use reflection to match method parameter names with route params.

Request injection: The current request object should also be injectable into controller methods by type-hint. Dispatcher uses reflection: if parameter type-hint is Request, inject the current request.

Middleware pipeline: The dispatcher typically wraps the action in the middleware pipeline (covered in fw-05). The dispatcher returns the pipeline, not just the action.

Returned values: Actions can return a Response object, a string, or an array. The dispatcher converts non-Response returns to a Response.

Code Example

php
<?php
namespace Framework\Routing;

use Framework\Container\Container;
use Framework\Http\Request;
use Framework\Http\Response;

class Dispatcher
{
    public function __construct(private readonly Container $container) {}

    public function dispatch(MatchedRoute $match, Request $request): Response
    {
        $action = $match->route->action;
        $params = $match->params;

        $result = match(true) {
            $action instanceof \Closure => $this->callClosure($action, $request, $params),
            is_array($action)           => $this->callControllerArray($action, $request, $params),
            is_string($action)          => $this->callControllerString($action, $request, $params),
            default                     => throw new \InvalidArgumentException('Invalid route action'),
        };

        return $this->toResponse($result);
    }

    private function callClosure(\Closure $closure, Request $request, array $params): mixed
    {
        // Resolve closure parameters using reflection
        $args = $this->resolveArgs(
            new \ReflectionFunction($closure),
            $request,
            $params
        );
        return $closure(...$args);
    }

    private function callControllerString(string $action, Request $request, array $params): mixed
    {
        [$class, $method] = explode('@', $action, 2);
        return $this->callControllerArray([$class, $method], $request, $params);
    }

    private function callControllerArray(array $action, Request $request, array $params): mixed
    {
        [$class, $method] = $action;
        $controller = $this->container->make($class);
        $args = $this->resolveArgs(
            new \ReflectionMethod($controller, $method),
            $request,
            $params
        );
        return $controller->$method(...$args);
    }

    private function resolveArgs(\ReflectionFunctionAbstract $fn, Request $request, array $params): array
    {
        $args = [];
        foreach ($fn->getParameters() as $param) {
            $type = $param->getType()?->getName();
            if ($type === Request::class || is_subclass_of($type, Request::class)) {
                $args[] = $request;
            } elseif (isset($params[$param->getName()])) {
                $args[] = $params[$param->getName()];
            } elseif ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();
            } else {
                $args[] = $this->container->make($type);
            }
        }
        return $args;
    }

    private function toResponse(mixed $result): Response
    {
        if ($result instanceof Response) return $result;
        if (is_string($result)) return new Response($result);
        if (is_array($result)) return new Response(json_encode($result), 200, ['Content-Type' => 'application/json']);
        throw new \RuntimeException('Action must return a Response, string, or array');
    }
}