PHP
Article

What Are Polymorphic Relations and How Do We Use Them with Eloquent?

By Younes Rafie

Working with PHP 7.1? Download our FREE PHP 7.1 Cheat Sheet!

This article was peer reviewed by Christopher Vundi and Christopher Pitt. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


While I was working on an application for a client, I had to implement a new module that entails the following:

  • Users ask for a budget quotation for a certain task.
  • Every task has a location.
  • Professionals can subscribe to different zones.
  • A zone can be a region or a city.

Now, let’s neglect the core application and try to implement this single module to see what we can achieve here.

laravel-l-slant

Scaffolding Application

I assume you have your development environment already set up. If not, you can check this Homestead Improved Quick Tip or just use the official Homestead Box.

Go ahead and create a new Laravel project using the Laravel installer or via Composer.

laravel new demo

Or

composer create-project --prefer-dist laravel/laravel demo

Edit your .env file with your database credentials.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead    
DB_PASSWORD=secret

Creating Migrations

Before creating our migrations, we should talk about Eloquent polymorphic relations and how they can benefit us in this case!
Polymorphism is often used when an object can have different forms (shapes). In our case, professional users subscribe to different zones and they get notified whenever a new job is posted in this area.

Lets start by creating the zones table.

php artisan make:model Zone --migration

This creates a migration file, but we do need to add a bit of code to it before it’s complete, as demonstrated by:

// database/migrations/2016_12_02_130436_create_zones_table.php

class CreateZonesTable extends Migration
{
    public function up()
    {
        Schema::create('zones', function (Blueprint $table) {
            $table->integer('user_id')->unsigned();

            $table->integer('zone_id')->unsigned();
            $table->string('zone_type');
        });
    }

    public function down()
    {
        Schema::dropIfExists('zones');
    }
}

Next, we create the cities and regions tables.

php artisan make:model Region --migration
// database/migrations/2016_12_02_130701_create_regions_table.php

class CreateRegionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('regions', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
        });

    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('regions');
    }
}
php artisan make:model City --migration
// database/migrations/2016_12_02_130709_create_cities_table.php

class CreateCitiesTable extends Migration
{
    public function up()
    {
        Schema::create('cities', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 255);
            $table->integer('postal_code')->unsigned();

            $table->integer('region_id')->unsigned()->nullable();
        });
    }

    public function down()
    {
        Schema::drop('cities');
    }
}

We could’ve achieved the same result by making a many to many relation with the cities and regions table. However, the zones table will act as an abstract class for the two other tables. The zone_id will hold an id for a city or a region and using the zone_type value Eloquent will decide which model instance to return.

Creating Models

Eloquent has the ability to guess the related table fields, but I decided not to use it just to explain how to map database fields to models. I will explain them as we go along!

// app/User.php

class User extends Authenticatable
{
    // ...

    public function cities()
    {
        return $this->morphedByMany(City::class, 'zone', 'zones', 'user_id', 'zone_id');
    }

    public function regions()
    {
        return $this->morphedByMany(Region::class, 'zone', 'zones', 'user_id', 'zone_id');
    }
}

The morphedByMany method is similar to the manyToMany one. We specify the related model, the mapping field name (used for zone_type and zone_id), related table name, current model foreign key and the morphed relation key.
We could’ve automated this by letting Eloquent guess field names, if you take a look at the documentation you’ll see that we can name the fields as zoneable_id and zoneable_type, then only specify the mapped field name (return $this->morphedByMany(City::class, 'zoneable').

// app/Region.php

class Region extends Model
{
    // ...

    public function cities()
    {
        return $this->hasMany(City::class);
    }

    public function users()
    {
        return $this->morphMany(User::class, 'zones', 'zone_type', 'zone_id');
    }
}

We can quickly guess the parameters definition from the above code. We specify the related table name, morphed relation type and ID.

// app/City.php

class City extends Model
{
    // ...

    public function users()
    {
        return $this->morphMany(User::class, 'zones', 'zone_type', 'zone_id');
    }
}

Now that everything is set up, we can start testing our relations and see if everything is working as expected. We should first seed our tables to save some time. You can grab the database seeder’s code from the GitHub repository.

Using Relations

We can attach cities and regions to users using syncing, attaching, and detaching Eloquent methods. Here’s an example:

$user = App\User::find(1);

$user->cities()->sync(App\City::limit(3)->get());

This will attach three cities to the selected user, and we can do the same for regions.

$user = App\User::find(1);

$user->regions()->sync(App\Region::limit(3)->get());

Eloquent Polymorphism relations

If we inspect our database now to see what was saved, we can see the following:

mysql> select * from zones;
+---------+---------+------------+
| user_id | zone_id | zone_type  |
+---------+---------+------------+
|       1 |       1 | App\City   |
|       1 |       2 | App\City   |
|       1 |       3 | App\City   |
|       1 |       1 | App\Region |
|       1 |       2 | App\Region |
|       1 |       3 | App\Region |
+---------+---------+------------+
6 rows in set (0.00 sec)

We can also detach regions if they exist.

$user = App\User::find(1);

$user->regions()->detach(App\Region::limit(3)->get());

Now, we can fetch user cities and regions as we would do on a normal many to many relationship:

$user = App\User::find(1);

dd($user->regions, $user->cities);

We could add a new method called zones which will merge cities from regions with individual selected cities.

class User extends Authenticatable
{
    // ...

    public function zones()
    {
        return $this->regions->pluck("cities")->flatten()->merge($this->cities);
    }
}

The pluck method will get a collection of cities from each region, which be then flattened (merge all the collections into one) and merged with the user selected cities. You can read more about collections in the documentation, and if you want to learn more, I recommend this Refactoring to Collections book from Adam Wathan.

Conclusion

Even though polymorphic relations are rarely used in applications, Eloquent makes it’s easy to deal with related tables and returning the correct data. If you have any questions or comments about Eloquent or Laravel in general, you can post them below and I’ll do my best to answer them!

Recommended
Sponsors
Get the latest in PHP, once a week, for free.