DEV Community

Franky Braem
Franky Braem

Posted on • Originally published at zumuta.be

Symfony routing with attributes (standalone)

Attributes

As of PHP 8, PHP Attributes are available. Attributes can be used to add metadata to classes, methods, functions, parameters, properties and class constants. The advantage of using attributes, is that you can keep the metadata close to the code.

Symfony routing

As of version 6, the Symfony routing package supports Attributes. If there is one place where metadata is interesting to use, it is routing. In previous versions of PHP, this could be solved using comments (annotations). With attributes, the dependency on doctrine/annotations is not needed anymore.

I'm using Symfony router in my own framework and I also wanted to take advantage of using attributes. Each route is linked to an action class. Attributes applied to the action class will define how it can be reached. Because I don't use the Symfony framework, I needed to figure out a way to load the routes from the action classes.

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Symfony\Component\Routing\Annotation\Route;

#[Route(
    path: '/trainings/{id}',
    name: 'trainings.get',
    requirements: [
        'id' => '\d+'
    ]
)]
final class GetTrainingAction
{
    public function __invoke(Request $request, Response $response, array $args): Response
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Symfony routing provides several ways of loading routes. There are Loaders and the one we particularly need is the AnnotationClassLoader. It's an abstract class, so we need to create a new class that derives from it. We need to define an implementation for the configureRoute method.

class RouteClassLoader extends AnnotationClassLoader
{
    protected function configureRoute(
        Route $route,
        \ReflectionClass $class,
        \ReflectionMethod $method,
        object $annot
    ) {
        $route->setDefault('_action', $class);
    }
};
Enter fullscreen mode Exit fullscreen mode

The configureRoute can be used to add some configuration to the route. In our case we set _action to the class that is currently loaded. This class is a reflection to our action class GetTrainingAction. In my framework, the action class always implements an invoke method, so we can ignore $method here.

Now we can load the action class into our route collection:

    $loader = new RouteClassLoader();
    $collection = new RouteCollection();
    $collection->addCollection($loader->load(GetTrainingAction::class));
Enter fullscreen mode Exit fullscreen mode

In the Route middleware (PSR-15), we try to match the route. When a match is found the Reflection class will be added as attribute to the request.

    public function __construct(
        private RouteCollection $routes
    ) {
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $symfonyRequest = (new HttpFoundationFactory())->createRequest($request);
        $context = new RequestContext();
        $context->fromRequest($symfonyRequest);

        $matcher = new UrlMatcher($this->routes, $context);
        try {
            $parameters = $matcher->matchRequest($symfonyRequest);
            $route = $parameters['_route'] ?? null;
            unset($parameters['_route']);
            $action = $parameters['_action'] ?? null;
            unset($parameters['_action']);
            $request = $request
                ->withAttribute('kwai.route', $route)
                ->withAttribute('kwai.action', $action)
                ->withAttribute('kwai.action.args', $parameters)
            ;
        } catch (\Exception $e) {
            throw new RouteException(message: 'Could not find a route', previous: $e);
        }
        return $handler->handle($request);
    }
Enter fullscreen mode Exit fullscreen mode

The actual execution of the action is done in the RequestHandlerMiddleware

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $action = $request->getAttribute('kwai.action');

        if ($action instanceof \ReflectionClass) {
            try {
                $callableAction = $action->newInstance();
            } catch (ReflectionException $e) {
                throw new RuntimeException(
                    message: "Could not create an instance of the action class: $action",
                    previous: $e
                );
            }
        } else {
            throw new \RuntimeException('Invalid request handler set');
        }
        return $callableAction($request, $this->response, $request->getAttribute('kwai.action.args'));
    }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)