Hiding Model ID's in Application URL's with Laravel

Jethro Solomon • October 8, 2022

While working on the rebuild of payPod.co.za recently, I wanted to hide the integer primary ID from the URL, and use a UUID instead. I did not want to use UUID's as a replacement for my primary ID due to performance concerns when performing more complex queries on tables with millions of rows of data.

So, with my routes looking as simple as can get:

Route::get('/document/{document}', [DocumentController::class, 'show'])->name('document.show');
Route::get('/client/{client}', [ClientController::class, 'show'])->name('client.show');

I created a base model which all of my other models would extend.

I needed to only add the method getRouteKeyName() to specify the string attribute on my model would be used for routing, and then in the boot() method, set a default value for that column ("slug") when the model is first created.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class BaseModel extends Model
{
    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName()
    {
        return 'slug';
    }

    public static function boot()
    {
        parent::boot();

        // Register a callback to be executed upon the creation of our model.
        static::creating(function ($model) {

            // Add our UUID slug. You may also want to check for duplicates.
            $model->slug = Str::uuid();

        });

    }
}

That's it! I now had this: /document/a23381c8-9c06-4ffe-b313-d8a6d2f8aabc

Instead of this: /document/483546

This may seem obvious to any seasoned Laravel dev (which I am not), but I came across way too many examples doing horrible things such as this in the routing file:

// Gross
Route::get('document/{slug}', function($slug){
    $result =  DB::table('documents')->where('slug', $slug)->get(); 
    // .... call controller etc...
});

Sure, it works, but I'd have to modify every single route. And then still the below code would not output the actual path we want to display to the user:

{{ route('document.show', ['document' => $document]) }}

Handling this all in our model means we only need to change it in one place.