Obfuscating IDs with Laravel

I saw a pretty common question from Tyris on the Laracast forums this morning about hiding the ID from the URL of their laravel application.

I've done this on a lot of projects, and figured i'd show how i approach the subject using the Hashids library.

Install Hashids:

This part is simple, add it to your project with:


_10
composer require hashids/hashids

Create a provider to handle generating HashIDs automatically for your models.

(Psst...My way of adding event listeners is a little "old-school" now, you might want to upgrade to an observable class, or adding event listeners through the model)


_10
php artisan make:provider HashIdModelProvider

Then make sure you add this to your providers array in config/app.php:


_10
'providers' => [
_10
//
_10
//
_10
//
_10
_10
\App\Providers\HashIdModelProvider::class
_10
]

Set up a column in your database.

In order to generate a HashId that's unique to the model we're creating it for, we need the model to have an id - these are assigned by the database, so we can only add a HashID after the model has been initially saved.

That means that the column that stores the HashID value will not have a value when saved initially, so it must be nullable(), for example:


_10
Schema::create('posts', function($table) {
_10
$table->increments('id');
_10
$table->string('url_string')->nullable(); // So it won't throw errors when saving posts.
_10
$table->text('post_content');
_10
$table->timestamps();
_10
});

Listen for the model created event.

When a new post is created, we want to create and save a HashID in the url_string column of the table!

So, add an event listener for the model created event in your service provider you created:


_10
use App\Post;
_10
_10
public function boot()
_10
{
_10
Post::created(function($model) {
_10
// Create and save the hashid here.
_10
});
_10
}

Now, all we have to do is create the actual HashID, assign it to the url_string column of the model, and that's it!


_11
use App\Post;
_11
use Hashids\Hashids;
_11
_11
public function boot()
_11
{
_11
Post::created(function($model) {
_11
$generator = new Hashids(Post::class, 10);
_11
$model->url_string = $generator->encode($model->id);
_11
$model->save();
_11
});
_11
}

Notice how in the call to new Hashids, we're passing two arguments:


_10
new Hashids(Post::class, 10);

The first, is a string, which will be something like \App\Post - this acts as a salt of sorts, making sure that the hash it generates is unique to this class.

If you didn't pass this, then the Post with an ID of 1, and a User with an id of 1 may end up with the same hash!

The second argument is a minimum length - by default Hashids will spit out the shortest length string possible - usually one or two characters. We want at least 10 here to make it not look silly in a URL.

(This minimum length stuff is totally optional but i prefer a longer hash by default)

Bonus round!

Do you use route model binding? that is, say you have a route:


_10
use App\Post;
_10
_10
// www.myapp.com/posts/24
_10
Route::get('posts/{post}', function(Post $post) {
_10
// $post is a fully fledged App\Post instance thanks to model binding!
_10
});

You can update Laravel's automatic resolution for model binding by importing the Route facade and adding this code to your service provider:


_15
use App\Post;
_15
use Hashids\Hashids;
_15
use Illuminate\Support\Facades\Route;
_15
_15
public function boot()
_15
{
_15
_15
_15
// Your other code above here
_15
_15
Route::bind('post', function ($value) {
_15
return Post::where('url_string', $value)->first();
_15
});
_15
_15
}

Update: Bonus round two:

You can quickly set the column name that route model binding should use to search for by overriding the getRouteKeyName() method on your model:


_10
// Post.php
_10
_10
public function getRouteKeyName()
_10
{
_10
return 'url_string';
_10
}

Thanks to Lee Willis and Ben Sampson for that one - i didn't know about it! #timesaver

That should be it! If you have any feedback on this article, Tweet me!