Add These to Every New Laravel Project

February 12th, 2025 • 7 minutes read time

Out of the box, Laravel is configured for you to be productive immediately without thinking about much. Let's take it a step further and add some opinionated configuration to improve developer experience.

Of course, like anything, these are optional and may not be suitable for all projects, teams or experience levels.

Adding Model::shouldBeStrict to your Laravel application does a few things. First, let's add it to the boot method of our AppServiceProvider.

use Illuminate\Database\Eloquent\Model;

class AppServiceProvider extends ServiceProvider
{
    //...

    public function boot(): void
    {
        Model::shouldBeStrict();
    }
}

Behind the scenes, here's what the shouldBeStrict method does:

public static function shouldBeStrict(bool $shouldBeStrict = true)
{
    static::preventLazyLoading($shouldBeStrict);
    static::preventSilentlyDiscardingAttributes($shouldBeStrict);
    static::preventAccessingMissingAttributes($shouldBeStrict);
}

Let's cover each one (which, by the way, can be used on their own, too, with more on that later).

The preventLazyLoading method will throw an error if you attempt to read model relationships without having eager loaded them.

For example, if you wrote this code:

$posts = Post::get();

And then attempted to loop anywhere (in this case, a Blade template):

@foreach($posts as $post)
    {{ $post->title }} by {{ $post->user->name }}
@endforeach

You'd see the following error:

Attempted to lazy load [user] on model [App\Models\Post] but lazy loading is disabled.

Essentially, preventLazyLoading stops you from causing an n+1 problem in your applications.

You'd have to update the original fetching of Postss to this to get rid of the error:

$posts = Post::with('user')->get();

This can cause an issue in production since executing too many queries in production is better than presenting an error to users. In this case, you'll want to disable this behaviour specifically:

Model::shouldBeStrict(!app()->isProduction());

This will disable preventLazyLoading in production, but you'll still benefit from it for local development. If you're using preventLazyLoading on its own, you can control whether it's enabled in the same way:

Model::preventLazyLoading(!app()->isProduction());

We'll talk a little more about fillable columns and mass assignment later, but preventSilentlyDiscardingAttributes will change Eloquent's default behaviour and will error if and when you attempt to fill a non-fillable column in a model.

As an example, let's say we create a new Post model and don't add column to a $fillable array:

class Post extends Model
{
    protected $fillable = [
		'title'
	];
}

Doing something like this, with preventSilentlyDiscardingAttributes disabled, won't error:

$post = Post::find(1);

$post->fill(['user_id' => 2]);
$post->save();

However, once preventSilentlyDiscardingAttributes is turned on via shouldBeStrict, performing the same action throws an error like this:

Add fillable property [user_id] to allow mass assignment on [App\Models\Post].

This is useful if you're not unguarding your models and would like to know if/when you're attempting to fill columns that have not been marked explicitly as fillable.

Most Laravel developers have been stuck figuring out why model data isn't being updated. This feature immediately flags the issue, saving time.

You can access a property of an Eloquent model, regardless of whether it exists. For example, the following property that doesn't exist on the Post model returns null.

$post = Post::find(1);

$post->price; // null

If you'd like to be alerted when you're trying to access a property on a model that doesn't exist, turning on preventAccessingMissingAttributes, either via shouldBeStrict or on its own, will alert you:

The attribute [price] either does not exist or was not retrieved for model [App\Models\Post].

In conclusion, shouldBeStrict is a great way to enable sensible behaviour in Eloquent that can save you some time and ensure you're eager loading your relationships. And because this is just a shortcut to enable the three toggles we've looked at, these can be toggled individually, too.

It's a popular approach within the Laravel community to unguard models. What does this mean?

No columns are fillable by default when we create a model in Laravel. This means that we have to specify (using a $fillable array) which columns can be mass assigned.

Here's an example of a freshly created Post model and the required $fillable properties:

class Post extends Model
{
    protected $fillable = [
        'title',
        'body'
    ];
}

With title and body added to $fillable, we can now create and update Post's titles and body.

Enter Model::unguard().

By adding Model::unguard() to your AppServiceProvider boot method, you unguard all models in your Laravel application by default.

public function boot(): void
{
    Model::unguard();
}

This means that for any models we create, we don't need to bother defining $fillable:

class Post extends Model
{
    //
}

This saves a lot of time and energy when you're building an application because you won't need to keep returning to your models to update them when new columns are added (or tidy them up when columns are removed).

Yes and no. You're good if you're correctly validating input and not blindly accepting any values when creating/updating models.

Here's an example of how using Model::unguard() could get you into trouble:

public function store(Request $request)
{
    Post::create($request->all());

    return back();
}

In this example, we're creating a post using the all method on the incoming request. This means that any data can be passed into the request and will get filled.

Instead, we should be doing this:

public function store(Request $request)
{
    $data = $request->validate([
        'title' => ['required', 'max:255'],
        'body' => ['required', 'max:5000'],
    ]);

    Post::create($data);

    return back();
}

Or something like this:

public function store(Request $request)
{
    // Validate here

    Post::create($request->only('title', 'body'));

    return back();
}

In conclusion, use Model::unguard() to improve developer experience, but don't neglect validation and be selective about the data you use to create and update models.

If in doubt, don't use unguard globally at all, or define it on a per-model basis, like this:

class Post extends Model
{
    protected $guarded = false;
}

This quick and easy one stops destructive artisan commands from being run. It's a great one to enable specifically for production since it's highly unlikely you want to roll back or entirely wipe your database in a production environment.

Here's how to enable it in your AppServiceProvider's boot method (for production only):

use Illuminate\Support\Facades\DB;

public function boot(): void
{
    DB::prohibitDestructiveCommands(app()->isProduction());
}

Let's look behind the scenes at the prohibitDestructiveCommands method:

public static function prohibitDestructiveCommands(bool $prohibit = true)
{
    FreshCommand::prohibit($prohibit);
    RefreshCommand::prohibit($prohibit);
    ResetCommand::prohibit($prohibit);
    RollbackCommand::prohibit($prohibit);
    WipeCommand::prohibit($prohibit);
}

So, this disables the following artisan commands:

  1. migrate:fresh, which drops all tables and re-runs all migrations
  2. migrate:refresh, which resets and re-runs all migrations
  3. migrate:reset, which rolls back all database migrations
  4. migrate:rollback, which rolls back the last database migration
  5. db:wipe, which drops all tables, views, and types

Basically, any command that has the potential to cause data loss.

So, if you run any of these commands in production, it won't warn you... it'll just stop you:

WARN  This command is prohibited from running in this environment.

Laravel uses the popular Carbon package to power date and time parsing, manipulation and formatting.

Here's an example of grabbing the current date and time with the handy now function. I've simplified the output so we can focus on the difference between having immutable dates turned on or off:

$date = now(); // 2025-02-12 08:33:23

From this $date, we could use a method like addHours to manipulate the date:

$date = now(); // 2025-02-12 08:33:23
$future = $date->addHours(2); // 2025-02-12 10:33:23

As you can see, this $future variable now has a Carbon date with the hour incremented by 2.

All seems normal until you realise that Carbon is mutable by default. That means that although we've created a new $future variable to hold the future date/time, we've also changed the original $date variable!

So, actually, the above code snippet where I've added the comments is wrong, and it would contain these values:

$date = now(); // 2025-02-12 10:33:23 (both are 10am)
$future = $date->addHours(2); // 2025-02-12 10:33:23 (both are 10am)

This can cause a lot of headaches when you're working with dates and times and can actually cause terrible side effects (imagine building a booking system and messing up appointment times due to not knowing about Carbon being immutable by default).

The solution is to use copy liberally throughout your code or set Carbon to be immutable by default.

Here's what copy looks like in the example above:

$date = now(); // 2025-02-12 8:33:23 (still 8am!)
$future = $date->copy()->addHours(2); // 2025-02-12 10:33:23 (this is 10am)

If you don't want to rely on remembering to use copy where it's required, head over to your AppServiceProvider and add this under your boot method:

use Illuminate\Support\Facades\Date;

public function boot(): void
{
    Date::use(CarbonImmutable::class);
}

Once you've done that, all dates are now immutable by default. So, the code we wrote earlier that unintentionally modified the original date will now work, with no need for copy:

$date = now(); // 2025-02-12 08:33:23 (still 8am)
$future = $date->addHours(2); // 2025-02-12 10:33:23 (changed 10am)

If whatever you're building relies heavily on manipulating dates and times, making Carbon dates and times in Laravel immutable by default might save you a lot of issues later on.

If you found this article helpful, you'll love our practical screencasts.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first!