DEV Community

Eivin Landa
Eivin Landa

Posted on

Laravel route binding for finite objects

Dependency injection in Laravel is a complicated topics and it's mostly used for 3rd party packages and some internals. You can utilise it in your own application too, but in my opinion it often complicates the code more than it's worth and makes debugging much harder.
There is one very good use for it, and one that you're probably already using. Route binding for eloquent models. Eg. if you have a model, Saw, the route could look something like this

<?php

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

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

And then in the controller you can directly access the saw model in the show method like this

<?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 ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is makes it super easy to create routes for eloquent models and gives you instant access to the model you want. In some cases you might have objects that has a finite set. In my case I have 2 saw machines and I don't want to store them in the database because they will always be the same and they each have some unique rules and code that runs for each individual machine. That means that making an eloquent model for the Saw model is a bad fit because I want to avoid tightly coupling code with the database, but I still want the ease of use for routing and be able to access the model directly based on it's id.
I took a look at how eloquent models get resolved during the routing process and found that all that's needed is to attach the UrlRoutable contract on our class and then Laravel will use reflection magic via middleware to inject the correct objects.

<?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)
    {
        // I'm not sure how this one works so I just return null
        // and it's not required for my application
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

UrlRoutable requires you to implement four methods, getRouteKey, getRouteKeyName, resolveRouteBinding and resolveChildRouteBinding.

  • getRouteKey should return the route key or id value of the object. This is usually the id or the slug of the object you have.
  • getRouteKeyName should return the name of the property that has the key value.
  • resolveRouteBinding takes a parameter, value, that is the id of the object to get and should return the object that gets injected.
  • resolveChildRouteBinding is related to child routing which I haven't looked into. If you know more then please comment and tell me.

Because I only have a couple of saws I made a quite basic repository class to house them.

<?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;
    }
}
Enter fullscreen mode Exit fullscreen mode

To populate the repository I used Laravels singleton method to bind it to the application in my SawProvider class.

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;
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

There are other ways to deal with finite objects in Laravel, but I quite like this route binding method as it lets me use my own custom objects and classes as part of the routes and have the correct objects injected into the controller methods.
I've seen many, many examples where developers have fallen for the temptation to put finite models that is directly connected with the code into the database as eloquent models and it has always resulted in headaches for several reasons. Testing becomes harder because you have to ensure that you seed the database before you can test. If you ever need to change the objects you have to first change it in the database and then you have to make sure that you also update the seeding code.
Whenever you have a finite set of objects that have specific rules or code that execute based on each individual object then I believe it's much better that these objects are solely governed by code and should not be coupled with the database at all.
Using a custom repository class and UrlRoutable for the model is a great way to achieve this.

Top comments (0)