Support the ongoing development of Laravel.io →

Say Hi to PHP attributes

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, me and my fellow backend developers 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 if I could mark an action as disabled or ignored. And guess what? With attributes, this turned out to be super easy, and super clean also.

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 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, 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 can 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! 🪄

Last updated 3 weeks ago.

driesvints, mohaaosman, muetze, ya27cine, josemalcher, abdullah-alhabal liked this article

6
Like this article? Let the author know and give them a clap!
oussamamater (Oussama Mater) I'm a software engineer and CTF player. I use Laravel and Vue.js to turn ideas into applications 🚀

Other articles you might like

November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
November 11th 2024

🍣 Sushi — Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.