Why you should use spatie/laravel-data to store "settings" in your Laravel app
For a long time, I struggled with finding a good way to store settings for a user, team, or any other model in my Laravel app. Over the last 10 years, I've used different approaches to solve this problem.
I either added separate columns for each setting to the respective database table (e.g., a timezone
or date_format
column to the users
table), created separate database tables to hold all the settings (e.g., a user_settings
table with user_id
, timezone
, and date_format
columns), or added a generic settings
JSON column to my users
table to store the settings.
Each method has its advantages and disadvantages. Adding separate columns is great if you need to query by a specific setting (e.g., "which users have selected a custom date format like YYYY-MM-DD
?"). On the other hand, it can be excessive to add a new column just for a small setting, especially if your table has millions or billions of rows.
Separating the settings columns from the core table can be better for database performance. Your database doesn't have to load all settings columns all the time, which is useful when you need a list of users or projects in your database.
Adding a catch-all settings
column is the easiest approach but can lead to performance issues and different structures between rows. It raises questions about how your app should react if an expected setting does not exist on a user. Do you have to check for the existence of the JSON key everywhere you check for the setting (e.g., $user->settings?->my_custom_setting
)?
My solution to all of these problems is the spatie/laravel-data
package developed by the Spatie team.
The spatie/laravel-data Approach #
The primary use case of the laravel-data
package is to create strongly typed data objects in your Laravel projects. Here's an example of such a data object:
use Spatie\LaravelData\Data;
class SongData extends Data
{
public function __construct(
public string $title,
public Artist $artist,
) {
}
}
The package also supports Eloquent Casting, which means a data object can be saved to your database and, when retrieved, cast back into a strongly typed data object instance.
The combination of strong types and Eloquent casting inspired me to use this package to store settings in my apps.
An Example #
Here is a basic example of a UserSettings
object I might add to any of my projects.
<?php
namespace App\Data;
use App\Domain\Support\Enums\ThemeApperance;
use Spatie\LaravelData\Data;
class UserSettings extends Data
{
public function __construct(
public string $timezone = 'UTC',
public string $locale = 'en',
public string $date_format = 'YYYY-MM-DD',
public ThemeApperance $apperance = ThemeApperance::AUTO,
) {
//
}
}
In these settings we store the timezone, locale and preferred date format and theme apperance of a user.
After creating a migration that adds a settings
column to my users
-table, I update the User
-model so that settings
is cast to a UserSettings
instance.
use App\Data\UserSettings;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'settings' => UserSettings::class . ':default',
];
Now, thanks to laravel-data
, whenever I access $user->settings
I always get an instance of UserSettings
with all properties that I've declared in the PHP class.
If a user signed up two years ago and the app didn't support $date_format
, the value on that user would just fall back to the default value I declared in the UserSettings
-class.
The next time that user updates their settings, their outdated database state gets updated. No longer supported settings will be removed as well.
If you can't wait until your users update their settings, you can create an Artisan command that does this for you.
use App\Data\UserSettings;
use App\Models\User;
Artisan::command('app:update-user-settings', function () {
// Get all Users and update their settings
User::query()
->each(function (User $user) {
// Update settings to the newest format
$user->settings = UserSettings::from($user->settings);
$user->save();
});
});
The command loops over all users in your database and updates the settings
-column with a fresh version. The current settings of a user (eg., their selection for $date_format
) will be migrated.
Why This Is Great #
At the beginning, I mentioned that adding a catch-all settings
column might be a bad idea if you don't have a structured way of storing settings. If different parts of your app add new keys to settings, it can become messy quickly. By using laravel-data, this problem goes away. There is one single source of truth for settings.
You can make settings strongly typed by using type hints and Enums. You could even create a nested structure of settings.
Imagine a UserSettings
-class that includes a UserGeneralSettings
, UserNotificationSettings
and UserApperanceSettings
.
<?php
namespace App\Data;
use App\Domain\Support\Enums\ThemeApperance;
use Spatie\LaravelData\Data;
class UserSettings extends Data
{
public function __construct(
public UserGeneralSettings $general,
public UserNotificationSettings $notification,
public UserApperanceSettings $apperance,
) {
//
}
}
One thing to keep in mind is that querying for specific settings can lead to performance issues and should probably be avoided.
If your app regularly needs to query for users who have selected a particular date_format
, it's better to promote this setting to its own column. This makes the work of your database and possible indexing much easier. [1]
Closing Thought #
Going forward, I will use this approach for all cases where I need the concept of settings in new and existing apps.
I believe that using spatie/laravel-data
and Eloquent casting is better than just putting your settings into a generic $settings
array. I encourage you to give it a try in your next project.
Do you like this approach? Do you think this is a bad idea? Let me know!
MySQL has support to index values inside a JSON column, but I would still promote the setting to its own column if I regularly query by it. ↩︎