PHP атрибуты в Laravel

Источник: «PHP attributes in Laravel»
В PHP 8 появились атрибуты, которые являются довольно впечатляющими и мощными. В этой статье мы будем использовать их для включения и выключения маршрутов Laravel, насколько это круто?

Поприветствуйте PHP атрибуты

Атрибуты позволяют добавлять структурированную, машиночитаемую информацию о метаданных к объявлениям в коде: Классы, методы, функции, параметры, свойства и константы класса могут быть объектом атрибута.

Я считаю, что определение верное, и уверен, что большинство разработчиков, читающих эту статью, хотя бы раз сталкивались с атрибутами. Если вы не сталкивались, то по сути это метаданные, добавляемые в класс.

В этот момент вы, возможно, задаётесь вопросом, чем же они отличаются от PHPDOC? Ну, они являются гражданами первого класса, это настоящие PHP-классы, и да, я знаю, это меняет всю игру; вам не нужно писать регулярные выражения, чтобы извлечь что-то из PHPDocs, и вы даже можете поддерживать некоторую форму состояния в свойствах.

Поскольку я немного опоздал на вечеринку и классических примеров атрибутов предостаточно. Так почему бы не построить с их помощью что-нибудь крутое?

Создание отключаемых маршрутов

Работая в команде, я часто получаю сообщения от других разработчиков (фронтенд парни, я смотрю на вас), уведомляющие меня, что маршрут работает не так, как ожидалось. Иногда мне хотелось бы легко отключить маршрут для определённого окружения, например, для окружения staging, сохранив при этом его функциональность на локальном уровне. Таким образом, я и мои коллеги разработчики бэкенда сможем работать над ним, добавлять код и поддерживать наш типичный рабочий процесс, не беспокоясь о непредвиденном использовании. Иногда бывает так, что новый маршрут должен оставаться исключительно в тестовой среде.

Размышляя над этим, я подумал, что было бы здорово пометить действие как отключённое или игнорируемое. И знаете что? С помощью атрибутов это оказалось очень просто, и к тому же очень чисто.

Давайте начнём с создания атрибута. Я назову его Ignore, и у него будет одно свойство, называемое in:

<?php

namespace App\Attributes;

use Attribute;

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

Вот и все, вы только что создали атрибут. Вы также заметили, что мы ограничили область его действия классами и методами, что позволяет размещать этот атрибут исключительно на этих двух сущностях.

Теперь мы можем использовать его следующим образом:

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(),
]);
}
}

Вы можете видеть, что это уже хорошо читается, игнорируется в production и staging. Тем не менее нам нужно сделать это функциональным, и для этого есть несколько способов, самый простой из которых — использование middleware.

Давайте создадим middleware, я назову его IsRouteIgnored, вы можете выбрать любое имя, которое вам больше нравится

php artisan make:middleware IsRouteIgnored

Теперь мы можем реализовать логику, идея проста: мы перехватываем запросы маршрутов, использующих это middleware, затем проверяем, есть ли у экшена атрибут Ignore, если есть, то проверяем, разрешено ли текущему окружению иметь этот маршрут или нет.

Для этого мы воспользуемся магией Reflection API, давайте погрузимся в код:

<?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);
}
}

Мы создаём отражение метода, к которому ведёт маршрут, поэтому получаем атрибут Ignore. По умолчанию атрибуты не повторяются, то есть они могут использоваться только один раз для каждой сущности. Поскольку нас интересует только атрибут Ignore, в итоге мы получим одно-элементный массив.

Теперь мы можем инстанцировать атрибут, вызвав newInstance(), вернувшись в сферу обычных классов. Затем в свойстве in мы можем указать окружения, в которых этот маршрут должен игнорироваться. В данном случае маршрут будет возвращать ответ 404 для production и staging окружений, но будет работать в local и testing окружениях.

После этого вы можете зарегистрировать middleware глобально или в API маршрутах, как вы обычно делаете, и можете начать игнорировать маршруты, пометив их атрибутом.

Заключение

Всего в нескольких строках кода мы включили отключаемые маршруты. Хотя реализация была относительно простой, этот пример должен был продемонстрировать возможности атрибутов. Ведь это так здорово! Можно включать и выключать маршруты в определённых окружениях по нашему выбору, можно даже настроить атрибут Ignore, чтобы исключить маршрут из всех окружений, кроме тех, которые вы укажете, — варианты бесконечны.

В следующий раз, когда вы задумаетесь о том, чтобы обозначить класс как что-то специфичное, попробуйте использовать Атрибуты!

Дополнительные материалы

Предыдущая Статья

Как использовать файловую систему в Node.js

Следующая Статья

Будьте последовательны в использовании скриптов Composer в CI