Eloquent Attributes and Database Defaults

There’s an important gotcha to remember when working with Eloquent and default database values.

Let’s say you have the following (partial) migration:

Schema::table('orders', function (Blueprint $table) {
    $table->enum('status', ['open', 'shipped', 'closed'])
        ->default('open');
});

And the corresponding Eloquent model:

class Order extends Model
{
}

If you create a new model instance, without overriding the default database value, the status attribute will be null.

$order = Order::create();

// Outputs `null`
echo($order->status);

What the hell, Eloquent?

The reason this happens is simple: Laravel can’t possibly know what happened at the database level when creating the record, without explicitly reloading the data.

It makes perfect sense when you phrase it like that, but if you’ve become accustomed to Eloquent just magically working, this behaviour can come as a bit of a surprise.

Solutions

There are three easy solutions to this problem. As you might expect, each comes with its own set of pros and cons.

Option one: specify the default value in your code

The simplest solution is to explicitly specify any default values when creating a new model instance.

// Assuming `status` is unguarded.
$order = Order::create(['status' => 'open']);

This works well if you regard your code as the single source of truth for all of your data, in which case you can remove the default call from the migration.

In practise, this rarely makes sense, particularly when you’re using the Active Record pattern, which means this solution has one very serious drawback: you now have two sources of “truth” in your application, which can easily get out of sync.

Option two: refresh the data for each model

The second solution is to manually call the refresh method on your new model instance1:

$order = Order::create();
$order->refresh();

This ensures that the database remains the single source of truth in your application. You can also choose to do this on an as-needed basis, which reduces unnecessary overhead.

On the downside, it’s easy to forget, not to mention rather ugly.

If you’re using the Repository Pattern2, this is less of an issue, as you can encapsulate the code in the repository’s create method:

public function create(array $attributes): Order
{
    $order = Order::create($attributes);
    $order->refresh();
    
    return $order;
}

That’s fine in theory, but in my experience, somebody is going to create a new model instance outside of the repository, at which point everything falls apart3.

Option three: hook into the Eloquent “created” event

The final option is to hook into the Eloquent “created” event for your Order model. This involves a few different parts, so let’s start with your model.

<?php

namespace App;

use App\Events\OrderCreated;
use Illuminate\Notifications\Notifiable;

class Order extends Model
{
    use Notifiable;
    
    // In Laravel 5.4 and earlier, use $events
    protected $dispatchesEvents = ['created' => OrderCreated::class];
}

Next up, you need an OrderCreated event class.

<?php

namespace App\Events;

use App\Order;
use Illuminate\Queue\SerializesModels;

class OrderCreated
{
    use SerializesModels;
    
    public $order;
    
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

An event isn’t much use without a listener, so you also need a class that will respond to the OrderCreated event, and refresh the model.

<?php

namespace App\Listeners;

use App\Events\OrderCreated;

class RefreshOrder
{
    public function handle(OrderCreated $event)
    {
        $event->order->refresh();
    }
}

And finally, you need to hook it all together in your EventServiceProvider class.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        'App\Events\OrderCreated' => ['App\Listeners\RefreshOrder'],
    ];
}

No muss, no fuss, and you now have a nice clean solution which runs automatically whenever anyone creates a new order.4.


  1. This requires an additional database query, but there’s really no getting around that. ↩︎

  2. Or possibly anti-pattern↩︎

  3. I’ll let you decide whether this an Eloquent problem, or a developer problem. ↩︎

  4. Unless somebody decides to do DB::table('orders')->insert(), in which case you’re on your own. ↩︎

Bend Craft to Your Will

Our newsletter helps you make the most of Craft. Join for free, leave any time.