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:
_10composer 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)
_10php 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:
_10Schema::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:
_10use App\Post;_10_10public 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!
_11use App\Post;_11use Hashids\Hashids;_11_11public 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:
_10new 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:
_10use App\Post;_10_10// www.myapp.com/posts/24_10Route::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:
_15use App\Post;_15use Hashids\Hashids;_15use Illuminate\Support\Facades\Route;_15_15public function boot()_15{_15 _15 _15// Your other code above here_15_15Route::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_10public 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!