Привязка Laravel маршрутов для конечных объектов

Источник: «Laravel route binding for finite objects»
Инъекция зависимостей в Laravel — сложная тема, и в основном она используется для сторонних пакетов и некоторых внутренних компонентов. Вы можете использовать её и в своём приложении, но, на мой взгляд, она часто усложняет код больше, чем стоит, и значительно затрудняет отладку.

Инъекция зависимостей в Laravel — сложная тема, и в основном она используется для сторонних пакетов и некоторых внутренних компонентов. Вы можете использовать её в своём приложении, но, на мой взгляд, она часто усложняет код больше, чем стоит, и значительно затрудняет отладку.

Для неё есть одно очень хорошее применение, которое вы, вероятно, уже используете. Связывание маршрутов для Eloquent моделей. Например, если у вас есть модель Saw, маршрут может выглядеть примерно так:

<?php

use Illuminate\Support\Facades\Route;
use Saw\Http\Controllers\SawController;

Route::get(
'/saw/{saw}',
[SawController::class, 'show']
)->name('saw.show');

А затем в контроллере вы можете напрямую обратиться к модели Saw в методе show следующим образом:

<?php
namespace Saw\Http\Controllers;

use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Saw\Models\Saw;

class SawController extends Controller
{
public function show(Request $request, Saw $saw): View
{
return view('saw.show', [ 'saw' => $saw ]);
}
}

Это позволяет очень легко создавать маршруты для Eloquent моделей и даёт мгновенный доступ к нужной модели. В некоторых случаях у вас могут быть объекты, имеющие конечный набор. В моем случае у меня есть 2 распиловочных станка, и я не хочу хранить их в базе данных. Потому что они всегда будут одинаковыми, и у каждого из них есть уникальные правила и код, который выполняется для каждого отдельного станка. Это означает, что создание Eloquent модели для модели Saw плохо подходит, потому что я хочу избежать тесной связи кода с базой данных. Но я всё ещё хочу иметь простоту использования для маршрутизации и возможность прямого доступа к модели на основе её идентификатора.

Я посмотрел, как Eloquent модели разрешаются в процессе маршрутизации, и обнаружил, что всё, что нужно, — это прикрепить контракт UrlRoutable к классу, а затем Laravel использует магию отражения через middleware для внедрения нужных объектов.

<?php

namespace Saw;

use Illuminate\Contracts\Routing\UrlRoutable;
use Saw\Enums\SawTypeEnum;
use Saw\Repositories\SawRepository;

class Saw implements UrlRoutable
{
public function __construct(
public ?int $id = null,
public ?SawTypeEnum $sawTypeEnum = null,
)
{
}

public function getRouteKey(): ?int
{
return $this->id;
}

public function getRouteKeyName(): string
{
return 'id';
}

public function resolveRouteBinding($value, $field = null): ?Saw
{
/** @var SawRepository $repository */
$repository = app(SawRepository::class);
return $repository->getById($value);
}

public function resolveChildRouteBinding($childType, $value, $field)
{
// Я не уверен, как это работает, так что я просто возвращаю null,
// и это не требуется моему приложению
return null;
}
}

UrlRoutable требует реализации четырёх методов: getRouteKey, getRouteKeyName, resolveRouteBinding и resolveChildRouteBinding.

Поскольку у меня есть только пара пил, я сделал довольно простой класс хранилища для их размещения.

<?php

namespace Saw\Repositories;

use Saw\Saw;

class SawRepository
{
private array $saws;

public function addSaw(Saw $saw): SawRepository
{
$this->saws[$saw->id] = $saw;
return $this;
}

public function getById(int $id): ?Saw
{
return $this->saws[$id] ?? null;
}
}

Для наполнения хранилища я использовал Laravel метод singleton, чтобы привязать его к приложению в классе SawProvider.

public function register(): void
{
$this->app->singleton(
SawRepository::class,
function () {
$sawRepository = new SawRepository();
$sawRepository->addSaw(
new Saw(
id: 1,
sawTypeEnum: SawTypeEnum::CASING
)
);
$sawRepository->addSaw(
new Saw(
id: 2,
sawTypeEnum: SawTypeEnum::MOULDING
)
);
return $sawRepository;
}
);
}

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

Я видел множество примеров, когда разработчики поддавались искушению поместить конечные модели, напрямую связанные с кодом, в базу данных в виде Eloquent моделей, и это всегда приводило к головной боли по нескольким причинам. Тестирование становится сложнее, потому что перед тестированием необходимо убедиться в том, что вы засеяли базу данных. Если вам когда-нибудь понадобится изменить объекты, вам придётся сначала изменить их в базе данных, а затем убедиться, что вы также обновили код засева.

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

Использование пользовательского класса репозитория и UrlRoutable для модели — отличный способ добиться этого.

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

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

Ошибки доступности, встречающиеся при проведении аудита

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

Тройное C: Currying, Closure и Callback в JavaScript