paint-brush
PHP Development: Creating Toggleable Laravel Routes with Attributesby@oussamamater
149 reads

PHP Development: Creating Toggleable Laravel Routes with Attributes

by Oussama MaterJune 3rd, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

attributes are essentially metadata added to a class. They are first-class citizens; they are **actual PHP classes**. The simplest way to use attributes is to mark an action as disabled or ignored. To make this functional, we need to create a middleware called 'IsRoute'
featured image - PHP Development: Creating Toggleable Laravel Routes with Attributes
Oussama Mater HackerNoon profile picture


Attributes offer the ability to add structured, machine-readable metadata information on declarations in code: Classes, methods, functions, parameters, properties, and class constants can be the target of an attribute.


I believe the definition is on point, and I'm confident most developers reading this article have encountered attributes at least once. If you haven't, they are essentially metadata added to a class.

At this point, you might be wondering how they differ from PHPDOCs then? Well, they are first-class citizens; they are actual PHP classes, and yes I know, it changes the whole game now; you don't have to write regular expressions to extract things from the PHPDocs, and you can even maintain some form of state within the properties.


Since I am a bit late to the party, classic examples of attributes abound. So, why not build something cool with them instead?

Making Routes Toggleable

When working with a team, I often receive messages from other developers (frontend guys, I am looking at you), notifying me that a route isn't working as expected. At times, I wish I could easily disable the route for a specific environment, like the staging environment, while maintaining its functionality locally. This way, my fellow backend developers and I can work on it, push code, and maintain our typical workflow without concerns about accidental usage. Occasionally, it's simply a new route that needs to stay exclusive to the testing environment.


So, pondering this, I thought it would be cool to mark an action as disabled or ignored. And guess what? With attributes, this turned out to be super easy and clean.


Let's start by creating an attribute. I will name mine Ignore, and it will have a single property called in

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Ignore
{
    public function __construct(
        public array $in = ['production']
    ) {
    }
}

That's it, you just created an attribute, you will also notice that we've limited its scope to classes and methods, allowing this attribute to be placed exclusively on those two entities.


Now, we can use it like so

namespace App\Http\Controllers;

use App\Attributes\Ignore;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Symfony\Component\HttpFoundation\Response

class TwoFactorQrCodeController extends Controller
{
    #[Ignore(in: ['production', 'staging'])]
    public function show(Request $request): Response
    {
        if (is_null($request->user()->two_factor_secret)) {
            return [];
        }

        return response()->json([
            'svg' => $request->user()->twoFactorQrCodeSvg(),
            'url' => $request->user()->twoFactorQrCodeUrl(),
        ]);
    }
}

You can see that this already reads well,;ignore it in production and staging. Still, we need to make this functional, and there are a few methods to achieve this. The simplest is using a middleware.


Let's create a middleware, I will name it IsRouteIgnored, feel free to choose any name you prefer

php artisan make:middleware IsRouteIgnored

Now we can implement the logic; the idea is simple: we intercept the requests of the routes that use this middleware, and we then check if the action has the Ignore attribute, if it does, we check whether the current environment is permitted to have this route or not.


For this, we will use the magic of the Reflection API, let's dive into the code.

<?php

namespace App\Http\Middleware;

use Closure;
use ReflectionMethod;
use App\Attributes\Ignore;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Symfony\Component\HttpFoundation\Response;

class IsRouteIgnored
{
    public function handle(Request $request, Closure $next): Response
    {
        $route = $request->route();

        if (!($route instanceof Route) || $route->action['uses'] instanceof Closure) {
            return $next($request);
        }

        $reflection = new ReflectionMethod($route->getControllerClass(), $route->getActionMethod());

        $attributes = $reflection->getAttributes(Ignore::class);

        if (!empty($attributes) && in_array(config('app.env'), $attributes[0]->newInstance()->in)) {
            abort(404);
        }

        return $next($request);
    }
}

We're creating a reflection of the method the route leads to so we retrieve the Ignore attribute. By default, attributes are not repeatable, meaning they can only be used once per entity. Since we've specified our interest solely in the Ignore attribute, we will end up with a single-element array. We can now instantiate the attribute by calling newInstance(), returning to the realm of regular classes. We then check the environments in which this route should be ignored within the in property. In this case, the route will return a 404 response for the production and staging environments but will function in the local and testing environments.


Afterward, you can register the middleware globally or within the API routes, as you would normally do, and you can start ignoring routes by marking them with the attribute.

Conclusion

With just a few lines of code, we've enabled toggleable routes. While the implementation was relatively basic, the example was meant to showcase the power of attributes. I mean, come on, how cool is that? Toggling routes on and off within specific environments of your choice, you can even adjust the Ignore attribute to exclude the route from all environments except for the ones you specify, and the options are endless.


Next time you ponder marking a class as something specific, consider giving Attributes a shot! 🪄