Add meta-data to Slim routes using PHP 8 attributes

August 31, 2022

PHP 8.0 introduced attributes. They can be used to add meta-data to classes, methods and more. Here we look at how we can use attributes with Slim to add meta-data to routes.

Introduction

As of PHP 8.0, it is possible to use attributes (also known as annotations or decorators in other languages). Attributes can be added to classes, methods, properties, parameters and more. Here is what an attribute looks like on a class.

#[ClassAttribute(foo: "bar")]
class ExampleClass {
    /* ... */
}

I have been using attributes to make it more convenient to write route endpoints with Slim. For example, we can protect certain routes with an authentication attribute.

My typical Slim setup

I have been using the Slim framework for several projects. The route definitions usually look something like this.

$app->get('/users', [UserController::class, 'getUsers']);
$app->post('/users', [UserController::class, 'postUsers']);

We often need to restrict access to certain routes based on whether a user is authenticated and has the right privileges. My usual solution has been to write a middleware called AuthMiddleware that checks the route name against an array of protected route names. We can now make it more convenient by using attributes.

An authentication attribute

Let's create an attribute to restrict access to routes where we require an authenticated user. We start by creating the attribute.

<?php

namespace App\Attribute;

use Attribute;

#[Attribute]
class AuthRequired {
    public function __construct(?boolean $admin = false) {}
}

This attribute also has an optional boolean argument, to restrict the route to admin users. We can now put the attribute on endpoints in our controllers.

<?php

use App\Attribute\AuthRequired;

class UserController {
    /* ... */

    #[AuthRequired]
    function getUsers(Request $request, Response $response) {
        /* ... */
    }

    #[AuthRequired(admin: true)]
    function postUsers(Request $request, Response $response) {
        /* ... */
    }
}

Now our endpoints have the attribute, but it is not being used anywhere. We need to write a middleware class that checks if the endpoint has our attribute, and then checks if a user is authenticated or has admin rights.

<?php

namespace App\Middleware;

use Slim\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Slim\Routing\RouteContext;

class AuthMiddleware {
    public function __invoke(Request $request, Handler $handler): Response {
        $routeContext = RouteContext::fromRequest($request);
        $basePath = $routeContext->getRoute();
        $callable = $basePath->getCallable();
        
        $reflection = new ReflectionMethod($callable[0], $callable[1]);
        $authAttributes = $reflection->getAttributes(AuthRequired::class);

        if ($authAttributes) {
            $attribute = $authAttributes[0]->newInstance();

            $isAuthenticated = /** your validation logic */
            $isAdmin = /** your validation logic */
    
            if (!$isAuthenticated || ($attribute->admin && !$isAdmin)) {
                return (new Response())->withStatus(403);
            }
        }

        $response = $handler->handle($request);
        return $response;
    }
}

The middleware starts by getting the class and method name for the endpoint (#12-14) and then gets the attributes for that method using reflection (#16-17). If an attribute is found, we check that a user is authenticated (and optionally, admin). If the check fails, it returns a 403 forbidden response. Otherwise we proceed to call the request handler.

Conclusion

Using attributes like this can give us short and more readable code in my opinion. There are definitely more use cases for attributes on controller endpoints, for example if we need rate limiting on certain endpoints, or we want to specify an input validation schema. Maybe I will explore it more in a later post :)