Маршрутизация в Laravel только с помощью контроллера через PHP-атрибуты

Атрибуты, новая фича PHP 8, также известные как аннотации, добавляют метаданные в классы, методы и переменные. Используя их, мы можем избежать создания дополнительных файлов маршрутизации и все необходимое сразу задать в одном контроллере, повысив читаемость кода. Нам не понадобятся никакие дополнительные пакеты, важна только версия языка.

Обычный код без атрибутов:

// on web.php 
Route::prefix('api')->delete('admin/auth', [MyController::class, 'delete'])->middleware('auth:admin');

// on Controller 
class MyController extends Controller
{
    public function logout(Request $request): Response
    {
        /** @var $admin Admin */
        $admin = Auth::guard('admin')->user();
        $admin->logout();
        return response(null, Response::HTTP_NO_CONTENT);
    }
}

После применения атрибутов

// Just on the Controller only
class MyController extends Controller
{
    #[Route (method: 'delete', path: 'api/admin/auth', middlewares: ['auth:admin'])] 
    public function logout(Request $request): Response
    {
        /** @var $admin Admin */
        $admin = Auth::quard('admin')->user();
        $admin->logout();
        return response(null, Response::HTTP_NO_CONTENT);
    }
}

Давайте разбираться как всё это работает. Прежде всего рекомендую прочесть официальную документацию по PHP атрибутам. Если вы уже разобрались, то идём дальше.

Зададим наш новый атрибут Route. Создаём новую папку Attributes в каталоге app. Затем создаём файл атрибута с названием Route.php.

namespace App\Attributes;

use Attribute;

#[Attribute]
class Route
{
    public array $args;
    public function __construct(string $method, string $path, array $middlewares = [])
    {
        $this->args = func_get_args();
    }
}

Теперь обновим логику загрузки маршрутов в RouteServiceProvider.php как показно ниже:

namespace App\Providers;

use App\Attributes\Route as RouteAttribute;

class RouteServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->routes(function () {
            $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(app_path('Http/Controllers')));

            foreach ($rii as $file) {
                // pass if the directory
                if ($file->isDir()) continue;

                // make our controller namespace
                $class = 'App\\Http\\Controllers\\' . $file->getBasename('.php');

                // create our virtual class
                $reflectionClass = new \ReflectionClass($class);

                // collect attributes of class if the invokable method is defined
                $classAttributes = collect($reflectionClass->getAttributes(RouteAttribute::class));

                // check if the class have attribute itself
                if ($classAttributes->isNotEmpty()) {
                    $arguments = collect($classAttributes->first()->getArguments());

                    // register our dynamic route
                    Route::match($arguments->get('method'), $arguments->get('path'), $class)
                        ->middleware($arguments->get('middleware', []));
                }

                // check each method individually
                foreach ($reflectionClass->getMethods() as $method) {
                    // collect all attributes of the method
                    $methodAttributes = collect($method->getAttributes(RouteAttribute::class));

                    // pass if the method doesn't have any defined attributes
                    if ($methodAttributes->isEmpty()) continue;

                    // get arguments of our defined attribute
                    $arguments = collect($methodAttributes->first()->getArguments());

                    // register our dynamic route
                    Route::match($arguments->get('method'), $arguments->get('path'), $class . '@' . $method->getName())
                        ->middleware($arguments->get('middleware', []));
                }
            }
        });
    }
}

Этот код в цикле прочитает ваши контроллеры и использует атрибуты из них для создания новых маршрутов. Больше нет необходимости отдельно прописывать их в файл маршрутов.

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

Если вы используете контроллеры одного действия, то вам необходимо добавить атрбут Route не в метод, а в класс:

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\User;

#[Route(method: 'post', path: 'api/account/{id}/server/provision')]
class ProvisionServer extends Controller
{
    /**
     * Provision a new web server.
     *
     * @return \Illuminate\Http\Response
     */
    public function __invoke()
    {
        //
    }
}

Необязательно всерьёз воспринимать это как замену дефолтной маршрутизации. Важно, что вы теперь знаете как можно использовать атрибуты в вашем коде!

Автор: Samir Mammadhasanov
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.