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?
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.
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! 🪄