Adding user-specific settings in Laravel

Geni Jaho

5 min read

The latest feature we added to OLM is the ability to make yourself known to the world using your social media accounts. The links to your accounts appear on the photos you upload, in the Global Map. See the end result in this photo on the map. This is the approach we're following in our back-end.

There are many ways to handle this, but after some googling, we came to the conclusion that the best solution in our case was to use a settings column in our User model. Database JSON columns come to the rescue. We copied a good chunk of code from this awesome post 🤣.

Storing the settings in the database

Starting off with adding the new column to the users table, this simple migration does the trick:

We want it to be nullable since existing users of the platform won't have any settings whatsoever, and it's easier than updating everyone with a default value. We could also provide a default value to this JSON column, like {}, but it's only supported from MySQL version 8.0.13 onwards.

To save the user's settings, we have a SettingsController and a method we called update():

Notice that we're using the $request->validated() method instead of $request->all(). That's because we don't want any setting that we don't have validation for to be stored in our database. The settings() method on the User model is a nice helper that does the actual heavy lifting.

It simply merges the new settings we add with the existing ones, overriding the old values where the keys match. Again, inspired by this awesome post.

Data validation is handled in a FormRequest:

The UpdateSettingsRequest does just some simple validation, it requires that the links provided be valid URLs. Through the messages() method we can override the default error messages to be more user-friendly.

Sprinkle some getters on the User

Now that we have a simple way to set a user's settings, we need a simple way to get them as well.

From top to bottom:

  1. It feels very comfortable when using the @property doc blocks. Through them, we notify our IDE that, on every $user instance, we can access properties like $user->settings and $user->social_links, and they will return an array. The more you have of these doc blocks, the merrier.
  2. We add the 'settings' => 'array' pair to the $casts array so that we don't have to json_encode() and json_decode() all the time to access the settings. You can also use 'json' instead of 'array'.
  3. The setting() method is straightforward, it simply checks if we have a setting with a given name and returns it, otherwise, it returns the default value provided. One gotcha here is that if the setting exists, but has a null-ish value, this method will return that instead of the default fallback. We want that by the way, at least for now. Also, notice $this->settings ?? [], we need this check since the column could be null.
  4. The getSocialLinksAttribute() method will allow us to access it like $user->social_links, and it will return only the settings that are not null-ish. This is for easier access in the front-end or other places in code. From Laravel 9 and above, there's a better way to define Accessors and Mutators on your models, you should check it out, it's the new kid in the block.

Does this work with nested settings?

In closing, we didn't go with a nested settings approach, something like this:

The reason is the complexity of dealing with it in the front end. Saving and updating nested settings would require some deep watching of properties and suspicious object merging operations. This solution does allow us to do that as it is implemented here, but we're keeping it simple for now.

See you in the next post, and thank you for making it here.

The code used for illustration is taken from the OpenLitterMap project. They're doing a great job at creating the world's most advanced open database on litter, brands & plastic pollution. The project is open-sourced and would love your contributions, both as users and developers.