Laravel: Adding preferences for each user

Below each “config” panel there is a whole odyssey behind it

TL;DR: Go and get Laraconfig to ease your pain.

In these days, users of an application take for granted a panel or dashboard with some settings, especially in the age of notifications preferences, dynamic themes, and even multiple billing addresses.

Wordpress General Settings panel

While the user shouldn’t be concerned about how it’s implemented, behind the scenes, the management of a key-value list of settings per-user is not just a “plug-and-play” thing. Well, you can say it is, but usually unthoughtful decisions tend to bite back after a while.

My point? There is no official and standard way to create “per-user settings”. You can do whatever you want to solve your problem, which is part of the magic of programming itself. If you land with an easy solution for your app, you end up trading performance, upgradability or query capabilities just for the convenience. At first glance the trade may not seem important, but down the road you don’t want to waste time “fixing” the settings themselves.

It’s not the problem of the configuration nature, but where and how to store it, and if you are aware of each drawback.

Can we just store JSON and call it a day?

Well, JSON does most of the job we want. Since JSON can become a list or a tree, we can just store it in any database column and then retrieve it as a Collection, or better, as an instance of Repository — the same object used by Laravel to manage the internal configuration of your application.

I would save this JSON into a settings column in the same users table, which would render no query overhead more than the memory used to store the values while the request is being handled.

Creating a new repository from a JSON tree.

Problem solved? If you won’t need to use this data at query time, ever, then yes. You can stop reading here and go away.

Now, when you need to query this data you will note that performance it’s not good. Some databases support querying JSON for filtering and updating the values, but again, there is a hit in performance since any query mostly translates in a full-table sweep since it needs to dive into the JSON itself for the correct value.

Imagine that we want to filter users that have “SMS Notifications” set to off. Worse, imagine we want to update the settings to a new layout, or move one value to another key. If you don’t play nice, you’ll end up swiping the whole table, or querying each bag of settings lazily and waiting some minutes or hours to update everyone.

Still, not the thing I’m personally looking into as it would become a pain to migrate from. JSON makes sense when dealing with presentational or other types of non-critical settings; values you are 200% sure you will never need to query at all.

What about the old abominable “horizontal table”?

The problem with a JSON column is having to sweep the whole table for querying one value. This is easily fixed by using a table with each column representing the keys. It feels like a braindead solution if you think about it.

| user_id | dark_mode | notify | notify_sms | notify_email |
| ------- | --------- | ------ | ---------- | ------------ |
| 1 | true | true | false | true |
| 2 | false | false | false | false |
| 3 | false | true | true | false |
| ... |

Imagine having a row with dark_mode, notify and so on. This is usually a very bad idea because it marries the settings layout to the table schema. Any small change will require migrating a whole table to another, and in some scenarios that may break things, or take the time to plan the migration on thousands or millions of rows.

On the other hand, the performance will be over the roof, as you may easily query settings by their column and value, or even get a single column. You may even create indexes for columns you may hit constantly.

Since columns can accept one single kind of data, you don’t need to worry about constraining a value in multiple applications as this will be mostly done by the database itself.

For example, consider one single row associated to a user by a user_id column. This is just one query to execute, usually indexed over the settings column that links both tables, and that’s all.

SELECT * FROM 'settings' WHERE 'user_id' = 14

Way better, just join the columns to the user into the retrieval query, like with the help of a Global Scope, to reproduce a query like this:

SELECT * FROM 'users' 
WHERE 'users'.'id' = 14
JOIN 'settings'
ON 'users.id' = 'settings.user_id'

But, this is not what I’m looking for. What about both?

JSON and horizontal table?

Well, you may make a point by saying “Why not both?”. If you know which settings are presentational, and which could be part of a query in the future, then you could perfectly separate them into their own columns:

| user_id | notifications | json |
| ------- | ------------- | ---- |
| 1 | true | ... |
| 2 | false | ... |
| 3 | false | ... |

How settings should be done

After some hours digging for the best way to manage a per-user settings, I landed with this timeless answer in StackOverflow.

Entity–relationship model of a settings storage approach.

The answer states that one table should hold the setting data itself, like the name and type. Another table would hold the allowed values the setting can have. A third final table stores the values themselves, per user.

This sounds very complicated, but necessary on applications which databases are hit by other languages or even directly. This approach avoids the problem of using values not allowed by the setting themselves.

We’re not that mile high yet. Instead, we can use the basic principle of the answer: one table holds the value, which is what changes per user, while other holds the setting data itself: name, type, constraints, whatever. Then, we let validation to the application itself.

Using that idea, I decided to make a package to handle that type of approach as a drop in, fire-and-forget software.

A drop-in solution: Laraconfig

I created Laraconfig to create per-user settings under the power of the database through a One-to-Many relation.

+------+   +-------+   +---------+
| User +---> Value |---> Setting |
+------+ +-------+ +---------+

What you want from from a bag of settings is to have no compromises when managing them, and you shouldn’t need a PhD in Computer Science to do so.

  • It should be easy to get and set from the get go,
  • let the database free from the settings themselves,
  • keep querying capabilities of the settings values themselves, and
  • keep performance up.

Laraconfig tackles these things with a simple one-to-many relation and a cache for those cases where you don’t want to hit the database every god damn time.

This is what you can do:

The layout of the package is relatively simple: we use a “parent” table that contains the settings data, like the name, default value, and the native type. Meanwhile, the “child” table stores the values, and links each of these rows to both the metadata and the user it belongs to.

When we retrieve the settings, behind the scenes, a Global Scope will make a JOIN to add parent metadata to each.

Then, instead of accessing to each setting and changing it manually, Laraconfig lets you use the convenient settings property to transform everything into oneliners.

Laraconfig uses the database, so you’re free to use the data itself directly from the tables — not encouraged but sometimes you have no other option.

Talking about databases, the included withConfig() query helper translates the cascade of query callbacks to filter users by their config value into another oneliner.

Cache for the speed

One of the caveats of this approach is the retrieval. Usually, having a few settings and querying — by the table index created automatically, mind you —is quick, but in some edge cases this can become slow.

To fix an eventual retrieval performance, the settings property on the User model interacts with the cache before retrieving the settings from the database — it’s disabled by default, but you’re one environment variable away to enable it.

The cache implementation is not just a dumb “save in cache and call it a day”. Using the __destruct() magic, we will regenerate the settings in the cache only if a change was detected.

To avoid race conditions, only the process who holds the fresher settings will save them into the cache.

Migrating settings like there is no tomorrow

Another big feature of Laraconfig is the migration system. You can use a convenient “manifest” of sorts to create, update, delete and migrate settings from the database.

That is part of the magic to manage settings in a simple way. Much like you declare your application routes, you can just “declare” a setting instead of injecting raw data into the database.

The migrations not only create settings. It will also detect the models using the HasConfig trait and fill each of these present in their respective tables, each with their own settings.

Then, if in the future we want to take the dark_mode setting and migrate to another new setting like theme, we can just change our manifest. To manage the values the users may have, we can use a callback to change that in a per-user basis.

If you accidentally run this migration twice, nothing will happen. Laraconfig will see both the manifest and the settings metadata in the database are the same. In other words, migrations only run when something changes.

While this magic is relatively safe on production, some applications with complex tables or millions of records may feel more comfortable doing an SQL statement, directly migrating settings instead of using a PHP Console command to do it. Executing a query is usually quicker than feeding an application with each row and then write back the result.

Creating settings automatically

You may think that each time you create a User in your database, you will need to also create the settings. Laraconfig does this automatically thanks to Eloquent ORM Events — you can always disable it and do it manually, but the “hands-off” way is preferred to save you headaches.

About the memory overhead, settings are never retrieved unless you need to work with them, much like any Eloquent relationship. You can even eager-load them in your model if you need to constantly work with them.

That’s most of the heavy lifting that Laraconfig does. I’m already happy with the results, as it works for me, and may work for you to avoid the hindrances and shenanigans of creating and maintaining per-user settings.

PS: Check out Carbon to create amazin’ code-snippets as images or smart embeds.

--

--

Italo Baeza Cabrera

Italo Baeza Cabrera

2.2K Followers

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com