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:
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)
php artisan make:provider HashIdModelProvider
Then make sure you add this to your providers
array in config/app.php
:
'providers' => [
//
//
//
\App\Providers\HashIdModelProvider::class
]
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:
Schema::create('posts', function($table) {
$table->increments('id');
$table->string('url_string')->nullable(); // So it won't throw errors when saving posts.
$table->text('post_content');
$table->timestamps();
});
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:
use App\Post;
public function boot()
{
Post::created(function($model) {
// Create and save the hashid here.
});
}
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!
use App\Post;
use Hashids\Hashids;
public function boot()
{
Post::created(function($model) {
$generator = new Hashids(Post::class, 10);
$model->url_string = $generator->encode($model->id);
$model->save();
});
}
Notice how in the call to new Hashids
, we're passing two arguments:
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:
use App\Post;
// www.myapp.com/posts/24
Route::get('posts/{post}', function(Post $post) {
// $post is a fully fledged App\Post instance thanks to model binding!
});
You can update Laravel's automatic resolution for model binding by importing the Route facade and adding this code to your service provider:
use App\Post;
use Hashids\Hashids;
use Illuminate\Support\Facades\Route;
public function boot()
{
// Your other code above here
Route::bind('post', function ($value) {
return Post::where('url_string', $value)->first();
});
}
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:
// Post.php
public function getRouteKeyName()
{
return 'url_string';
}
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!