Привязка 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
.
getRouteKey
должен возвращать ключ маршрута или значениеid
объекта. Обычно этоid
илиslug
имеющегося у вас объекта.getRouteKeyName
должен возвращать имя свойства, содержащего значение ключа.resolveRouteBinding
принимает параметрvalue
, являющийся идентификатором объекта, который нужно получить. И должен вернуть объект, который будет внедрён.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
для модели — отличный способ добиться этого.