diceline-chartmagnifierquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

How to use factories to create relationships which follow a sequence

Factories an extremely useful tool for testing. The more complex the use case, the more awesome you find out Laravel factories are.

For instance, we might need to create a few items, each one with a different measurement of its own:

 Item::factory()
    //...
    ->has(Measurement::factory()->state(new Sequence(
        ['length' => 50, "width" => 110],
        ['length' => 65, "width" => 200],
        ['length' => 190, "width" => 295],
    )))
  ->count(3)
  ->create()

This will result in 3 items, each item having one unique measurement assigned. The first item will have a measurement with values corresponding to the first array in the sequence and so on.

How to set default attribute values for Laravel models

The problem:

Given the following schema:

 Schema::create('posts', function (Blueprint $table) {
      $table->id();
      $table->unsignedBigInteger('number_of_hits')->default(0);
      $table->string('title');
  });

We when we create a new post:

$post = new Post(["title" => "test"]);

we might expect the 'number_of_hits' to be 0, but it is null.

The solution:

To fix this, we can easily tell Laravel what default values we want for the model attributes:

class Post extends Model
{
   ...
   protected $attributes = [
        'number_of_hits' => 0,
    ];
}

SQLite vs MySQL - foreign key checks

Sqlite does not check for foreign key integrity when creating tables, only when inserting records. However, MySQL does check in both cases.

To make this more clear, let's take a look at a simple example: given two tables - todos and users - created in this exact order:

 Schema::create('todos', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->unsignedBigInteger('user_id');
      $table->foreign('user_id')
          ->references('id')
          ->on('users');
  });
 Schema::create('users', function (Blueprint $table) {
      $table->id();
      $table->string('username');
      $table->string('password');
  });

If you run your migrations using the sqlite driver, everything works just fine.

However, if you run your migrations using the mysql driver, you get the following error:

SQLSTATE[HY000]: General error: 1005 Can't create table `[your-app]`.`todos` 
(errno: 150 "Foreign key constraint is incorrectly formed") 
(SQL: alter table `todos` add constraint `todos_user_id_foreign`
 foreign key (`user_id`) references `users` (`id`))

Extracting closures to their own classes

Laravel has a lot of functions that accept a closure as a callback. But sometimes the callback function is just too large to be done inline - so our becomes very unreadable.

Let's take a look at an example of using such a function:

class MyScope
{
    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            function (Builder $query) {
                //really long function
            }
        );
    }
}

If you are used to writing Javascript you might first attempt something like this in order to refactor:

class MyScope
{

    public function callback(Builder $query)
    {
       //really long method
    }

    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            $this->callback
        );
    }
}

Here we extracted the callback to a method and we attempt to pass it as a parameter instead of writing the closure inline.

Unfortunately, this would not work and we would get the following error:

Undefined property: App\Scopes\MyScope::$callback

One valid approach would be to have our method return the function we need instead:

class MyScope
{

    public function callback()
    {
       return function (Builder $query) {
          //really long function
       }

    }

    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            $this->callback()
        );
    }
}

This would do just fine, but we can take this one step further by making use of a powerful PHP feature - invokable classes:

class InvokableClass
{
    //this method will automatically be called and
    //passed the query parameter when the time comes
    public function __invoke($query)
    {
        //really long method
    }
}

This approach is very beneficial because it also allows us to break up the long method into smaller, more understandable pieces (because an invokable class is still a class and we can take full advantage of it) so we are not just sweeping the unreadable code under the rug.

We can now use our freshly defined invokable class in the following way:

class MyScope
{
    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            \Closure::fromCallable(new InvokableClass())
        );
    }
}

Please notice that we called Closure::fromCallable() first - this is necessary because we have to convert our class to a closure to avoid getting an error such as:

Argument 2 passed to Illuminate\Database\Eloquent\Builder::whereHas() must be an instance of Closure or null, instance of App\InvokableClass given