Tell Don't Ask
Published on

Keep defaults in models, not migrations and databases

Authors

Arrange

I bet you've seen or done something like this where default value is declared in database migration:

class CreateUsersTable extends Migration
{
  public function up(): void
  {
    Schema::create('users', function (Blueprint $table): void {
      $table->id();
      $table->string('name');
      $table->string('email')->unique();
      $table->timestamp('email_verified_at')->nullable();
      $table->string('password');
      $table->rememberToken();
      $table->boolean('loves_pets')->default(1);
      $table->timestamps();
    });
  }

That piece of logic is important for the codebase and it is not available in it. So developers tend to let that leak out of the model and they compensate for it all over the codebase.

$user = new User;
$user->loves_pets = 1;

More experienced Laravel developers reach out for model accessors.

public function getLovesPetsAttribute($value)
{
  return $value ?? true;
}

You would think that is the end of your problems. Sorry to say that it is not. If you create a new User instance and cast it to array by calling $user->toArray(), loves_pets property would not be present.

Someone even more experienced would think that by adding loves_pets to $appends array on the model would solve the problem. And they are right. Now when you call $user->toArray() that loves_pets would be present. Nice job. You deserve a raise.

But there are more problems lurking in the codebase. If you call $user->save() that loves_pets property would be empty and you would not be able to persist it to the database since it is not nullable.

Act

Some might think that if all of the above didn't work that they would have to fork the framework and change some code in order to make this work. This is an overstatement, but luckily that is not how Laravel works. Here is where $attributes model property comes to the rescue.

By defining that on the model all of the issues above will be covered by it. Here is how it looks:

protected $attributes = [
  'loves_pets' => true,
];

This way new objects and models that are persisted get the default value. It is not hidden in some migration that no developer will ever check on first occurrence of mentioned problems. Your less experienced colleagues will not add code where their cursor is at that point in the controller.

Assert

So by adding loves_pets to $attributes model property you will probably solve all of your issues. I'm saying "probably" cause I still haven't encountered a scenario where that problem was not covered by this solution.

Just as a reminder, this will cover following cases:

  • default value is not defined in migration
  • default value is not defined on database level
  • default value is defined in your codebase where it should be
  • new instances will have default value when casted to array with $model->toArray()
  • default value will be present when saving to the database