I’ve recently seen a Twitter discussion about what primary key should your tables use. The conversion tends to focus on default auto-incrementing integer ids vs UUIDs. I always leaned towards incremental Integer IDs because I find it so simple.
The biggest drawback is that you rarely want to show the IDs to the enduser, in the URL for instance. You don’t want to show your customer they’re number 5, working or the 8th thing of your entire product. Not that it matters but nobody likes it.
I have built a small demo app to show what I’m discussing in this article.
One common solution is to keep the auto-increment int ID as the primary key and add store a new column with a UUID. You’ll still need to retrieve entries via their UUID.
Modern database systems handle UUID pretty well now. UUID aren't stored as string but still, I have some reserve.
- What version should you use? V4? V6?
- How do you order? Should you use ULID then?
- Is it as good as integers for complex queries?
If you even want to get rid of the primary ID, is it really as performant to have UUID as primary and foreign keys?
Maybe! 🤷♂️ I don’t know much about UUIDs, and I’m not interested because I have a much better solution.
EDIT: Now Laravel ships with a new feature to use UUID by default 🚀
Hashid is a way to generate short, unique, non-sequential strings from numbers. Despite the name, hashids encode the numbers in the string so you can decode it.
It's ideal to store primary key. Your database only knows about auto-incrementing integer and only the app manage hashids. Publicly, the enduser only see string ids like
Even better, you can prefix the hashids since they are strings. It makes all hashes unique across the whole system and easily recognisable.
This is exactly how Stripe does it.
Simple example of how hashid work
The following snippet is a simple example of how hashids work. Note that the decode functions always returns an array. You can store multiple number a single hashid.
Hashids are encoded with a salt so you'll need this salt to decode every hashid.
I find that very short hashids don't look very good. Fortunately, you can specify a minimum length when instantiating the
Implementing Stripe-like Hashids with Laravel
In the rest of this article, I'll show you how to use hashids in a Laravel application. This includes:
- Generating hashids per Models
- Retrieving Model instance from hashids
- Handling hashids in Route resolutions
I have built a simple demo app to showcase how this works. You can view it live and checkout the repository on GitHub.
Why not use a package?
There are packages available (like deligoez/laravel-model-hashid) but I recommend implementing Hashid yourself, following this guide.
I strongly believe we should keep the number of dependencies as low as possible. Something as critical (and simple, you'll see) as this should be done in the app code.
To be clear, we'll use a package to encode and decode the hashids but I don't want another one for the Laravel glue.
Adding Hashid package and creating a Facade
Vincent Klaiber implemented the hashid algorithm in PHP and maintains the package vinkla/hashids.
As mentioned earlier at example above, you need to always instantiate
Hashids\Hashids with the same salt. We'll need to add the class definition to the container.
Personally, I don't like injecting this type of class in my objects. I feel like this function could (should?) be part of PHP like
I wouldn't inject an object to manipulate JSON, I'd call it directly. I want a Facade for this, it's easy to use, and it feels more Laravel-y.
app/Provider/AppServiceProvider.php file, we'll register the function definition.
Next, let's create a Facade using this newly created
hashid singleton. I typically create them in a Support folder:
HasHashid trait for our models
Now, we want to add the ability to our model to encode their primary key in a hashid. We'll create a trait in our Models folder add two new dynamic attributes. One is the actual hashid and the other one prepends it with the model prefix.
To add hashid to a model, we'll use this new trait and define the prefix. A prefix is defined in every model using this trait.
Retrieve Models by Hashids
Next, we want to retrieve models directly via the hashid. The hashid is not stored in the database so we need decode the string to get the primary key.
Following conventions, we'll call this method
findOrFailByHashid and add it to our
The Laravel router will resolve your model based on the primary key. You can customise the key but it won't work here. The custom key can only map to a column that exists in the database. Remember that we're not storing the hash in the database.
We will introduce a new parameter name in our route file and tell Laravel how to resolve it.
In this case, you'll have to add the route binding for each model using hashids. You can make it more generic and retrieve the model directly from the prefix if you start having a lot of models, but I prefer to keep it this way.
I personally think you get both a both worlds with this hashid thing. Integer as primary/foreign keys are super simple AND you get cool URLs like Stripe.