Handling Optional Relationships Gracefully with Laravel withDefault

how to use withDefault() method in Laravel

Handling Optional Relationships Gracefully with Laravel withDefault


Every Laravel developer eventually comes across such an error: 'Trying to get property 'name' of non-object' (or 'Call to a member function on null' for PHP versions before 8). It typically happens in a Blade view or during API transformation when trying to access a relationship that may not always be present. For example, in an API, there could be posts with author information and those without, orders with discount codes and those without, or users who have not filled out their profile.

To prevent PHP from throwing an error, one would have to use something like optional($post->author)->name, the null-safe operator $post->author?->name ?? 'Guest', or write a bunch of @if($post->author) blocks.

Luckily, Eloquent provides a neat solution to this common scenario through default models, or withDefault(). In this article, we'll explore how to use withDefault(), provide default attributes, and eliminate those pesky null property exceptions in PHP.

What is withDefault() in Eloquent?

The withDefault() method is available on belongsTo, hasOne, hasOneThrough, and morphOne relationships. When you query a relationship that does not find a matching record in the database, Eloquent will return a blank, hydrated instance of the related model instead of returning null.

Because it returns an actual model instance instead of null, calling properties or helper methods on the relation will never throw a null pointer exception.

The Problem: The Null Trap

Let's take a common example: a Post model that belongs to an Author (or User). In many applications, posts can be published by guests or system feeds where user_id is nullable.

Without default models, consider what happens in your Blade template:

<!-- If user_id is null, this throws: Attempt to read property 'name' on null -->
<span>Written by: {{ $post->author->name }}</span>


To prevent this, you would typically write defensive code like:

<span>Written by: {{ $post->author?->name ?? 'Guest Author' }}</span>


While the null-safe operator works, repeating it in every template, mailer, and controller across your application creates clutter and is prone to human error when someone forgets the ?->.

The Solution: Adding withDefault() to Your Relationship

Instead of patching the symptom in every view, you can solve the problem once at the model definition level using withDefault().

1. Returning a Blank Model Instance

If you call withDefault() without any arguments, Eloquent will return an empty instance of the related model when no record exists:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    protected $fillable = ['title', 'content', 'user_id'];

    /**
     * Get the author of the post.
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id')->withDefault();
    }
}


Now, if a post has no author:

$post = Post::whereNull('user_id')->first();

// Returns an empty App\Models\User model instance (not null!)
$author = $post->author;

// Outputs nothing, but does NOT throw an exception!
echo $post->author->name;


2. Providing Default Fallback Attributes (Array Syntax)

Usually, you don't just want an empty model—you want sensible fallback values. You can pass an array of default attributes to withDefault():

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id')->withDefault([
            'name' => 'Guest Author',
            'email' => 'no-reply@example.com',
            'avatar_url' => '/images/default-avatar.png',
        ]);
    }
}


Now, if the post does not have an author linked in the database, accessing $post->author->name smoothly renders "Guest Author" without any extra conditional logic in your views.

3. Dynamic Defaults Using a Closure

Sometimes your default values need to be dynamic or depend on the parent model. You can pass a closure to withDefault() that receives the newly instantiated default model and the parent model:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Order extends Model
{
    public function discount(): BelongsTo
    {
        return $this->belongsTo(Discount::class)->withDefault(function (Discount $discount, Order $order) {
            $discount->code = 'NONE';
            $discount->percentage = 0;
            $discount->description = "No discount applied to Order #{$order->id}";
        });
    }
}


Real-World Practical Example: Optional User Profiles

Consider an application where users have a 1:1 profile relationship (User hasOne Profile), but new users don't fill out profile information right away.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;

class User extends Model
{
    /**
     * Get the user's profile settings.
     */
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class)->withDefault([
            'bio' => 'No bio written yet.',
            'theme' => 'light',
            'timezone' => 'UTC',
            'notifications_enabled' => true,
        ]);
    }
}


Now your controllers and views can safely access profile settings immediately after user registration without worrying if the profile row has been inserted yet:

// Works well for both new users without a profile and established users!
$userTimezone = $user->profile->timezone;


Checking if the Model is Actually in the Database

A common question is: "Given that $post->author always returns a model instance, how does one know if it is really a database record or just a default fallback?"

As the default model returned by withDefault() is only hydrated in memory and has not been persisted, you may check the exists property:

if ($post->author->exists) {
    // This is a real User record stored in the database
    echo "Registered member since: " . $post->author->created_at->format('Y');
} else {
    // This is a dummy fallback model created by withDefault()
    echo "Guest submission";
}


Supported Relationships Comparison

Relationship Type Supports withDefault()? Reason
belongsTo() ✅ Yes Singular relationship that can be null.
hasOne() / morphOne() ✅ Yes Singular relationship that can be null.
hasOneThrough() ✅ Yes Singular relationship that can be null.
hasMany() / belongsToMany() ❌ No Plural relations return an empty Eloquent Collection, never null.

Important Things to Remember

  • It Does NOT Persist to Database: Calling withDefault() will never save the fallback model to the database on reads. It strictly exists in memory for that request lifecycle.
  • Check Primary Key: Since default models are not saved, $model->relation->id will be null unless you explicitly define an ID in the default array.
  • Null Foreign Keys in Database: Ensure that the foreign key column on your table is set to nullable() in the migration if a relationship is optional (e.g., $table->foreignId('user_id')->nullable()->constrained()).

Conclusion

Laravel’s withDefault() implementation is actually a part of the Null Object Pattern which helps you to avoid having messy conditional checks in your views and API resource response arrays. By utilizing default models on your relationships, you can keep your code clean while also making sure that your application doesn’t throw any errors.

Thank you for reading this article 😊

For any query, do not hesitate to comment 💬


Previous Post Next Post

Contact Form