Automatically Hash Laravel Model Values Using the "Hashed" Cast
Introduction
Hashing is an important security concept that every web developer should know about. It's what allows us to store passwords securely in our databases.
I won't be covering what hashing is in this article, so if you're not familiar with it, you might be interested in checking out my Guide to Encryption and Hashing in Laravel article.
In this short article, we're going to take a look at how to automatically hash model values in our Laravel projects before they're stored in the database.
Manually Hash Model Values
Typically, you may have been used to doing something like this in your Laravel code to manually hash a value:
use App\Models\User;
use Illuminate\Support\Facades\Hash;
$user = User::create([
'name' => 'Ash',
'email' => '[email protected]',
'password' => Hash::make('password'),
]);
As a result of running the above code, the password
field for the new user would be stored as something like so in the database:
$2y$10$Ugrfp6Myf9zVOo66KEuF9uQZ3hyg3T5GhJNgjOTy7o7AXCXSpwWpy
Automatically Hash Model Values
However, what if we wanted to automatically hash the password
field for our User
model without having to manually hash it each time?
To do this, we could use the hashed
model cast that Laravel provides and was added in Laravel v10.10.0. This will automatically hash the value of the field before it's stored in the database.
Let's imagine we wanted to update our code example from above and remove the manual hashing of the password
field. We'll first need to specify in our App\Models\User
class that we want to use the hashed
model cast for the password
field. We can do this by updating the casts
property on the model like so:
declare(strict_types=1);
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
final class User extends Authenticatable
{
// ...
protected $casts = [
'password'=> 'hashed',
];
}
Now, when we create a new user, we can remove the manual hashing of the password
field like so:
use App\Models\User;
$user = User::create([
'name' => 'Ash',
'email' => '[email protected]',
'password' => 'password',
]);
It's worth noting that the hashed
cast won't hash the value if it's already hashed. So if you're using Hash::make
to manually hash the value as well as the hashed
cast, you don't need to worry about the value being hashed twice.
Which Approach Should I Use?
A benefit of manually hashing the value is that it's more explicit and obvious that the value is being hashed. This means it can be easier for other developers to understand what's happening in the code at a glance. Whereas with the hashed
model cast, it's not as obvious that the value is being hashed without the developer knowing about the model cast.
On the other hand, a benefit of using the hashed
model cast is that it's less code to write. Although, I think this advantage is minimal though because you're not really removing that much code.
It's really important to remember that the casts are only applied when you're using the models to create or update records in the database. For example, if you're using something like the DB
facade to create the user, their password won't be hashed using the hashed
cast. So you'll need to remember to manually hash the value in this case.
In my opinion, I think both approaches are completely fine to use with their own pros and cons, and it's really down to personal preference. I'd just strongly recommend only using one approach in your project to avoid any confusion and be consistent. This will reduce the likelihood of someone making an assumption that a value is manually being hashed when it's not.
Testing That A Value Is Hashed
No matter which approach you choose, it's handy to have a test in place to ensure that the value is being hashed correctly.
This can be especially helpful in situations where you might accidentally assume that a value is being automatically hashed when it's not.
So let's take a quick look at a basic test that you could write to make sure that the value is being hashed when it's stored in the database.
We'll imagine that we have a basic App\Services\UserService
class that we're using to create new users. The UserService
class has a createUser
method that accepts an imaginary NewUserData
class. The NewUserData
class has a password
property that we want to ensure is hashed before it's stored in the database.
The UserService
class might look something like so:
declare(strict_types=1);
namespace App\Services;
use App\DataTransferObjects\User;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
final readonly class UserService
{
public function createUser(NewUserData $userData): User
{
return User::create([
'name' => $userData->name,
'email' => $userData->email,
'password' => $userData->password,
]);
}
}
We may want to write a test like so:
declare(strict_types=1);
namespace Tests\Feature\Services\UserService;
use App\DataObjects\NewUserData;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Hash;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
final class CreateUserTest extends TestCase
{
use LazilyRefreshDatabase;
#[Test]
public function user_can_be_stored(): void
{
$newUserData = new NewUserData(
name: 'Ash',
email: '[email protected]',
password: 'password',
);
$user = (new UserService())->createUser($newUserData);
// Assert the user was stored in the database.
$this->assertDatabaseHas(User::class, [
'id' => $user->id,
'name' => 'Ash',
'email' => '[email protected]',
]);
// Assert the password has been hashed correctly.
$this->assertTrue(
Hash::check('password', $user->password),
);
}
}
In the test above, we're calling the createUser
method on the UserService
class and passing in a NewUserData
object with some dummy data. We're then asserting that the user was stored in the database.
Seeing as we can't directly compare the hashed value of the password with the value that we passed in using the assertDatabaseHas
assertion, we can use the Hash::check
method to check that the value is hashed correctly.
In the event that the value isn't hashed, the Hash::check
method will return false
and the test will fail. As a result, this means that if someone accidentally removes the hashing from the password
field, we'll be able to spot it.
Conclusion
Hopefully, this short article has shown you how you can automatically hash your Laravel models fields.
If you enjoyed reading this post, I'd love to hear about it. Likewise, if you have any feedback to improve the future ones, I'd also love to hear that too.
You might also be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.
Or, you might want to check out my other 440+ ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.
Keep on building awesome stuff! 🚀
driesvints, ash-jc-allen, jerexbambex liked this article
Other articles you might like
Laravel Custom Query Builders Over Scopes
Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...
Access Laravel before and after running Pest tests
How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....
🍣 Sushi — Your Eloquent model driver for other data sources
In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...
The Laravel portal for problem solving, knowledge sharing and community building.
The community