Logging model changes and 11 other ways to improve your Laravel projects

I first started using Laravel with version 4. I started developing my first big application with Laravel 5: a business tool for process modelling. This tool is now in it’s third iteration and got some companions: a second tool for managing budgets and invoices as well as a company website that includes a blog, a self-service customer backend and an online shop.

I developed these tools all by myself and obviously it took me several years and they are still far from finished. So you might be wondering: okay, if you have been that knee-deep into stuff, are there any tips or recommendations for starting a new Laravel project?

Oh yes. And I am gladly sharing them with you for the May Mayhem Laravel Blog Contest :)


1. Read the Collection guide and crucial API pages often

Collections and Eloquent are powerful tools to achieve a clean and optimized codebase. The only problem is: their featureset changes a lot. New Eloquent functions are often introduced in minor Laravel versions and the offical docs for Eloquent are not always that elegant to read if you just want to get to the details.

So if your last Laravel project was on 5.5 and now you’re starting with 5.6, chances are that you’ve missed some cool new features. So here’s my tip: before diving into your next project, carefully review these two pages:

Rule of thumb: take 1-2 days off and review the API pages of the most frequently used features before diving into a new project.

2. Use a helper file

As your application grows, you might need a space where you can add small functions that need to be available everywhere. I’m always setting up a file for this when I start a new project, so I can directly add these functions in the right place when I need them.

I always like to create a folder “Foundation” where I keep all my business logic stuff, so a possible space would be app/Foundation/helpers.php. Now we only need to make this file available:

// in your composer.json add...
"autoload": {
"files": [
"app/Foundation/helpers.php"
]
}

Now we only need to run composer dump-autoload and we’re all set!

3. Use Migrations to adjust your data

We all use migrations to create or modify our database tables for a Laravel project. But sometimes small changes in our code can affect our data. For example, after careful consideration you decided that all order numbers for purchases should now be 10 digits long instead of 8. So where do you fix your data? Just use a migration:

php artisan make:migration change_order_number_format

// in your new migration file...
public function up()
{
foreach(Order::all() as $order){
// do stuff
}
}

So migrations can be useful, even if you do not need to actually change your database layout.

4. Storing Business Logic

Somewhere in your code you have the unique bits. That’s the stuff that separates your app from the others. Many people call this “Business Logic” and you should find a distinct place for it. As I said before I like to have a folder called “Foundation” in every Project. I always keep a subfolder “Managers” in there where I store all my unique classes. For example:

app/Foundation/Managers/OrderManager.php

Now I only need to register this class in my AppServiceProvider:

use App\Foundation\Managers\OrderManager;
class AppServiceProvider extends ServiceProvider {
  public function register(){
$this->app->singleton('OrderManager', function(){
return new OrderManager();
});
}
}

Now we can access the class almost everywhere in our application by calling resolve('OrderManager')or app('OrderManager')or by using dependency injection in a controller function like I did in this example:

// in app/Http/Controllers/ProcessController.php
/**
* Abort a process.
*
* @param Process $process
* @param ProcessManager $processManager
* @return \Illuminate\Http\Response
*/
public function abort(Process $process, ProcessManager $processManager){
$processManager->abort($process);
return redirect()->route('processes.show', [$process]);
}

Notice how this approach keeps my controller nice and clean.

If a controller method goes beyond 5 lines of code, I have a strong candidate for a little bit of refactoring into one of my manager classes.

5. Remember to check $fillable or use $guarded

Laravel uses the attribute $fillableon every model to ensure that only the right attributes are inserted into the database when a model changes. Its basically a whitelist for model attributes.

Now I am betting 5 bucks that there was a time where you changed something about your model (like adding an attribute) and then your application stopped working and you searched two hours for the error until you discovered that you forgot to add the attribute name to the$fillable array.

Well, you’re not alone. Which is why I now keep a note on my desk that just states “FILLABLE!”.

If you’re not into desk notes, you could switch $fillable with $guarded. This attribute does the exact opposite and serves as an attribute blacklist.

While $fillable will discard attribute values that are not present in his array,$guarded will pass everything to the model, except for the attributes listed in its array.

But: you should be extremely cautious about this and never switch from $fillable to $guarded in the middle of development! You will most likely break your app in 100 different places.

6. Prepare a global JavaScript object

If you are using Vue.js ore something else, you’ll frequently pass values to your JavaScript. This starts with the CSRF-Token and other values like a Stripe key or global page settings will add up on that over time. So you might want to plan ahead when starting a new project and add something like this to the header section of your main layout file:

<script>
window.myApp = {
'token': '{{ csrf_token() }}',
'settings': {!! collect(config('settings'))->toJson() !!},
'messages': {!! session('flash_message', collect())->toJson() !!}
};
</script>

7. Append values to your models

Sometimes you want to have certain attributes available on your model, but without actually storing them in your database because of redundancies. For example you might want to check, if a task is active depending on its start date and end date. Simply add this to your model:

class Task extends Model{
  protected $appends = ['active'];
  public function getActiveAttribute($value){
return now()->between($this->started_at, $this->ended_at);
}
}

By also specifying the new attribute in the $appends array you’re ensuring that the value will be preserved when converting the model to JSON.

However, use this approach carefully!

Why you ask? Well, you’ve just added a date operation to every attribute access for active. And you could go even further by querying relations in that function. This will most likely result in a performance bottleneck similar to the N+1 query problem when calling toJson() on a Collection of tasks.

8. Automatic ordering

If you want a model sorted in a specific way, you often end up ordering your queries and sorting Collections. Or you could configure a sensible ordering default directly in your model class:

use Illuminate\Database\Eloquent\Model
use Illuminate\Database\Eloquent\Builder;
class Task extends Model{
protected static function boot(){
parent::boot();
static::addGlobalScope('order', function (Builder $builder){
$builder->orderBy('name', 'asc');
});
}
}

9. Consider global settings

If your application grows you might need to store some global settings. I started by adding additional properties to my .env-file and actually, that’s fine as long as you also maintain the .env.example file in your project.

But as soon as I needed an admin panel for more than one user this approach just didn’t cut it anymore. So I created a model for settings and since then I throw it into every application right from the start.

First, let’s set up the model:

php artisan make:model Setting --migration

// in your migration file...
Schema::create('settings', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
$table->string('value')->nullable();
});

// in your model class...
class Setting extends Model {
  protected $guarded = [];

public $timestamps = false;
}

Now we just need a ServiceProvider:

php artisan make:provider SettingsServiceProvider

<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;
use App\Setting;
class SettingsServiceProvider extends ServiceProvider{
  public function boot(){
if(Schema::hasTable('settings')){
$settings = Setting::all()->pluck('value', 'name')->all();
config()->set('settings', $settings);
}
}
  public function register(){
//
}
}

Now we can register the new provider in config/app.php and we are done!

10. Embrace FormRequest classes

FormRequest classes are great, but are you really embracing them? Let me show you one of mine:

What this class does is actually not very important. The interesting parts are the three functions that I am overriding. They can help you making a FormRequest class really powerful, so let’s go through them:

  • prepareForValidation(): this function gets called before the validation of your request data. You can use it to aggregate or format a request. For example, if you send your request from your JavaScript, you might have a date value that you want to format correctly. This method would be the ideal place to adjust your date values with Carbon before validating everything.
  • getValidatorInstance(): You can override this function to get access to the validator and register additional validation logic via the after() validation hook. However, you should only put unique bits of code in here. If you need a certain part of validation logic more than once, you may want to create a custom validation rule instead.
  • failedValidation(): This function is getting called when the validation fails. It gives you a space for adding additional logic like putting some response values into your session.

These are just three examples that I frequently use. Make sure to treat yourself with a good read of the API docs to get an overview of all available options:

https://laravel.com/api/master/Illuminate/Foundation/Http/FormRequest.html

11. Focus on Policies and Gates

Authorization is something that you’ll likely ignore during prototyping. Of course you want to figure out the exciting bits first. But when everything starts to grow more quickly you may remember that you’ll need some authorization and you start to throw in some bits of code. For example, this may be tempting:

// in your App/User.php
public function isAdmin(){
return $this->group == 'admin';
}

If you go down this road, you’ll end up with messy code. To avoid that, focus on policies and gates from day 1. Let’s call it…

…Authorize-Driven-Development!

But first of all, let’s fix the above code in the AuthServiceProvider:

// in your AuthServiceProvider...
public function boot(){
$this->registerPolicies();
  Gate::define('administrate', function ($user) {
return $user->group == 'admin';
});
}

Defining this kind of authorization logic with the help of the Gate facade makes it available in all parts of your application via the various can()and authorize() helper functions. You should read the Authorization Docs at least two times to have all possibilites on your radar.

Always create and maintain policies as soon as you create a new model!

php artisan make:model Customer --migration
php artisan make:policy CustomerPolicy --Model=Customer

The — Model=Customer parameter will already add all neccessary stubs into your CustomerPolicy class, so you just need to fill in the blanks and add the new Policy class to your $policies array in your AuthServiceProvider.

The obvious benefit: now you have a dedicated space for everything regarding authorization. If you come back to your codebase later, you instantly know where to look for fixes and adjustments.

If you are working on an existing project without policies, I am betting another 5 bucks that you will find at least one Middleware class that could be easily refactored into a policy!

To finish off, let me tell you another neat thing about policies: Even though the documentation states that each policy function accepts only two parameters (the authenticated user and a model instance), you can add more, if you have your routes set up correctly!

Let’s say we have this route defined:

Route::post(articles/{article}/comment, CommentController@store);

You can actually grab an instance of the corresponding article when checking if a user is allowed to create a comment:

// in CommentController.php
public function __construct(){
$this->middleware('can:create,App\Comment',article)
->only(['create', 'store']);
}
// in CommentPolicy.php
public function create(User $user, Article $article){
return $article->allowsComments();
}

You can access corresponding models in your policies. But remember: this will only work, if you have your routes configured correctly.

This is really cool but being aware of this functionality is crucial for actually getting there. This is why you should focus on policies and gates from day one. Make it part of your routine when introducing new entities to your application. It will save you a lot of trouble later on and also helps you to maintain a good separation of concerns.

12. Use Traits to extend model behaviour

For my last recommendation I’d like to show you how you can leverage Traits to extend the behaviours of your models.

I’ve written a Trait to make all my models “loggable”. Each time a user changes an attribute, it will automatically get logged to the database:

The most important part is the implementation of bootLoggable(). Laravel will look for a function called “boot” plus the Trait name and call it automatically. Inside bootLoggable() I am registering callbacks for some Eloquent model events.

When a model is created or deleted, I simply add a log with a message. For that I need to implement the abstract method getModelName() for each model I want to use this Trait on. A user model could return its name, while an order model might respond with the id.

The update function is a little bit different. I am re-fetching the updated model so I have all attributes with their values before and after the update available. I then filter the uneccessary date fields and iterate over the remaining attributes. I then create a log message for each attribute with a changed value.

To complete my example, here are the bits of code for the migration:

// in your up() function in the migration file...
Schema::create('logs', function (Blueprint $table) {
$table->increments('id');
$table->text('text');
$table->integer('user_id')->nullable()->unsigned();
$table->integer('loggable_id')->unsigned();
$table->string('loggable_type');
$table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});

And here is my code for the model:

class Log extends Model {
  protected $fillable = [
'text',
'user_id',
'loggable_id',
'loggable_type'
];
  protected $dates = ['created_at'];
  public $timestamps = false;
  public function user(){
return $this->belongsTo('App\User');
}
  public function loggable(){
return $this->morphTo()->withTrashed();
}
}

And that’s it! Now you have a decent, automatic logging implemented!


Wow! This post turned out to get longer than expected! I hope you found a few helpful things in here. Let me know if something isn’t perfect. I’m very much open to polish and expand this in the future!

Like what you read? Give Lars Peterke a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.