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 instance. This requires an additional database query, but there’s really no getting around that.
$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 Pattern (or possibly anti-pattern), 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 apart. I’ll let you decide whether this an Eloquent problem, or a developer problem.
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. Unless somebody decides to do DB::table('orders')->insert()
, in which case you’re on your own.
Sign up for my newsletter
A monthly round-up of blog posts, projects, and internet oddments.