How and Why to use shouldBeStrict() in Laravel
When working with Eloquent for local development, one can easily overlook database-related issues. A missing foreign key in an eager loading query can cause 50 additional queries to be made in the background. A typo in a field name can cause Laravel to silently ignore data passed to create(). Accessing a field that wasn't included in the select() call will return null.
The problems occur because Eloquent is not too strict. There is no immediate feedback about what is wrong. You only know right when you ship your code to production and it's already too late.
Laravel has Model::shouldBeStrict() to enforce strict Eloquent mode. It disables 3 of the most common "forgiving" features of Eloquent so your application crashes right then and there when you write wrong database queries during development.
What Model::shouldBeStrict() Actually Does
Calling Model::shouldBeStrict() toggles off 3 internal Eloquent switches with a single call:
- Disables lazy loading: Prevents models from loading relationships that were not eager loaded.
- Disables discarding of unfillable attributes: Prevents Eloquent from silently dropping array keys passed to create() or update() that do not appear in $fillable.
- Disables reading of unretrieved attributes: Throws an exception if you attempt to read a database column that was excluded from a custom select() statement.
How to Turn It On
You turn this on once inside your AppServiceProvider. Pass a boolean condition so the checks run on local machines and in test suites, but stay off in production:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::shouldBeStrict(!$this->app->isProduction());
}
}
With ! $this->app->isProduction(), that means it'll evaluate to true in your local environment, staging environments, and any PHPUnit/Pest tests you have. It'll be false in production, so an unexpected edge case won't bring your whole application down for a customer.
1. Catching Unintended Lazy Loading
In standard Laravel, accessing a relationship you forgot to eager-load triggers a separate SQL query automatically:
$articles = Article::latest()->take(30)->get();
foreach ($articles as $article) {
// Queries the users table 30 times
echo $article->author->name;
}
Because the page renders correctly, developers tends to ship this code. But on production with thousands of records, it will cause high CPU usage on your database.
When you have strict mode enabled, the first time you try to access $article->author, Laravel will throw an exception.
This way you can optimize your code knowing that it will cause issues in production.
Illuminate\Database\LazyLoadingViolationException
Attempted to lazy load [author] on model [App\Models\Article] but lazy loading is disabled.
The fix is direct. Eager load the relationship with with():
$articles = Article::with('author')->latest()->take(30)->get();
Now Laravel runs two queries total, regardless of how many articles you fetch.
2. Catching Silently Dropped Mass Assignment Attributes
Eloquent uses $fillable to protect against mass assignment. But when an attribute is missing from $fillable, Eloquent normally ignores that attribute and saves everything else without telling you.
Let's suppose your User model is as follows:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
];
}
Later, you add a phone_number column in a migration, build a form for it, and run:
User::create([
'name' => 'Kishan Kumar',
'email' => 'kishan@example.com',
'password' => bcrypt('password123'),
'phone_number' => '+919876543210',
]);
WITHOUT STRICT MODE, Laravel will silently save the row without the phone_number, leaving it blank in the database. You won't see any error in your log file, and it may take hours/days to realize that SMS notifications aren't being sent due to this missing data
WITH STRICT MODE enabled, Laravel will throw a MassAssignmentException as soon as this code runs:
Illuminate\Database\Eloquent\MassAssignmentException
Add [phone_number] to fillable property to allow mass assignment on [App\Models\User].
You add 'phone_number' to $fillable, and the problem is resolved before the code is committed.
3. Catching Reads on Columns That Were Never Queried
When optimizing queries, you often choose specific columns with select() instead of loading every column with *:
$user = User::select('id', 'name')->first();
If another function or Blade view later attempts to read $user->email, default Eloquent returns null. This creates bugs where code checks if ($user->email) and assume that the user has no email address on file, when in fact it was just not selected in the SQL query then Strict mode will throw an exception instead of returning a false positive.
Strict mode replaces the fabricated NULL value with an immediate exception.
Illuminate\Database\Eloquent\MissingAttributeException
The attribute [email] either does not exist or was not retrieved for model [App\Models\User].
You can see right away that you need to add 'email' to your select() array.
Using the Individual Methods
If you have an older project with existing code that makes full strict mode impractical to adopt in one go, you can turn these behaviors on one at a time:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$enabled = ! $this->app->isProduction();
Model::preventLazyLoading($enabled);
Model::preventSilentlyDiscardingAttributes($enabled);
Model::preventAccessingMissingAttributes($enabled);
}
}
This lets you fix one category of problems—such as lazy loading—before addressing missing fillable fields or partial query reads.
Logging Violations in Production
While you don't want your production site to give away 500 errors on lazy loading you still want to know about it so that you can fix the issue. Laravel can be instructed to log the error instead of displaying it:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
if ($this->app->isProduction()) {
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
Log::warning("Lazy load detected on relation [{$relation}] on model [" . get_class($model) . "].");
});
} else {
Model::shouldBeStrict();
}
}
}
Your team gets a trace in your logs pointing directly to the controller or view causing the extra queries, while users see no disruption.
Bypassing Strict Mode for Specific Blocks
Sometimes you need to run code from a third-party package or some internally developed script that uses lazy loading, but you can't change it right now; you can temporarily disable the check for this particular block using withoutLazyLoading():
$authorName = Model::withoutLazyLoading(function () use ($article) {
return $article->author->name;
});
The check applies again as soon as execution leaves the closure.
Summary
Adding Model::shouldBeStrict(! $this->app->isProduction()) to your application will alert you every time your code tries to create invalid loops of relationships, omitted fields in forms etc., saving you hours of trying to figure out why something doesn't work in production. It takes just one line to set up in AppServiceProvider and protects you from making common mistakes with databases.
Thank you for reading this article 😊
For any query, do not hesitate to comment 💬
-compressed.jpg)