Laravel Logo

I wanted to take a quick break in my barrage of Artisan posts to write about defining one-to-many and many-to-many relationships in Laravel’s Eloquent. Eloquent also supports one-to-one relationships but I felt like those are used so rarely that I could skip it.

One-to-Many

In this example, we’re going to work through setting up a very basic one-to-many relationship where a User must belong to a Subscriber.

The Migrations

We’re going to create a single migration for each step of this process. Our first step is to create our subscribers table.

$ php artisan make:migration create_subscribers_table
Created Migration: 2019_11_02_012306_create_subscribers_table

To keep it simple we’ll create the table with just a name plus the default columns.

public function up()
{
    Schema::create('subscribers', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->timestamps();
        $table->string('name');
    });
}

Now we’ll create a migration to add the subscriber_id column to the users table.

$ php artisan make:migration add_subscriber_id_to_users_table
Created Migration: 2019_11_02_012405_add_subscriber_id_to_users_table
public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->unsignedBigInteger('subscriber_id');

        $table
            ->foreign('subscriber_id')
            ->references('id')
            ->on('subscribers');
    });
}

The Models

We’re going to be using the User model that comes with Laravel but we do need to create a Subscriber model.

$ php artisan make:model Subscriber
Model created successfully.

Now that we have our models we’re going to create a test Subscriber and User.

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.3.9-1+ubuntu16.04.1+deb.sury.org+1  cli) by Justin Hileman
>>> $item = new \App\Subscriber();
=> App\Subscriber {#3007}
>>> $item->name = "Test";
=> "Test"
>>> $item->save();
=> true
>>> 
>>> $user = new \App\User();
=> App\User {#3017}
>>> $user->name = "Scott";
=> "Scott"
>>> $user->email = "Scott";
=> "Scott"
>>> $user->password = "junk";
=> "junk"
>>> $user->subscriber_id = 1;
=> 1
>>> $user->save();
=> true
>>> $user->id
=> 2

In our Subscriber model we’re going to create a function named users that uses the hasMany() helper function to define the relationship.

class Subscriber extends Model
{
    /**
     * Get the users for the subscriber
     */
    public function users()
    {
        return $this->hasMany('App\User');
    }
}

There’s a little bit of magic going on behind the scenes because the hasMany function call above has some “hidden” parameters so it’s equivalent to:

return $this->hasMany('App\User', 'subscriber_id', 'id');

The subscriber_id string is determined by taking the name of the class (“subscriber”) and appending “id”. The “id” string is assumed because it’s the default for Eloquent models’ auto incrementing column.

Let’s say we name our subscriber_id column subId then we could replace the function call above with:

return $this->hasMany('App\User', 'subId');

Personally, my preference is being able to only pass one parameter so it’s as clean as possible but I like the fact we can override what Eloquent is expecting.

Now that we’ve defined our users() function we can get an instance of the Subscriber model and then query for the Users associated with it using the users attribute.

>>> $subscriber = \App\Subscriber::find(1);
=> App\Subscriber {#3021
     id: 1,
     created_at: "2019-11-02 01:38:34",
     updated_at: "2019-11-02 01:38:34",
     name: "Test",
   }
>>> $subscriber->users;
=> Illuminate\Database\Eloquent\Collection {#3003
     all: [
       App\User {#3018
         id: 2,
         name: "Scott",
         email: "Scott",
         email_verified_at: null,
         created_at: "2019-11-02 01:41:34",
         updated_at: "2019-11-02 01:41:34",
         subscriber_id: 1,
       },
     ],
   }
>>> 

The other thing we need to do is define the inverse of this relationship. So in the User model we need to add a subscriber() function:

class User extends Authenticatable
{
    /**
     * Get the subscriber
     */
    public function subscriber()
    {
        return $this->belongsTo('App\Subscriber');
    }
}

Again, there are two default parameters that have sensible defaults and this above function is equivalent to:

return $this->belongsTo('App\Subscriber', 'subscriber_id', 'id');

We’ve now defined the inverse of the relationship we can access the Subscriber thorough any of it’s Users:

>>> $subscriber = \App\Subscriber::find(1);
=> App\Subscriber {#3014
     id: 1,
     created_at: "2019-11-02 01:38:34",
     updated_at: "2019-11-02 01:38:34",
     name: "Test",
   }
>>> $subscriber->users[0]->subscriber;
=> App\Subscriber {#3002
     id: 1,
     created_at: "2019-11-02 01:38:34",
     updated_at: "2019-11-02 01:38:34",
     name: "Test",
   }
>>> 

Many-to-Many

In this example, we’re going to work through setting up a very basic many-to-many relationship where a User might have multiple Accounts they’re responsible for.

The Migrations

I’m going to be lazy and create the model and migration all in one go.

$ php artisan make:model -m Account
Model created successfully.
Created Migration: 2019_11_02_015336_create_accounts_table

And the related migration is again super simple.

public function up()
{
    Schema::create('accounts', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->timestamps();
        $table->string('companyName');
    });
}

Now here is where the magic starts to come in. When Eloquent does it’s many-to-many functions it automatically looks for a table named [firstModel]_[secondModel] where the order is determined by alphabetically. In this example, we’re going to have accounts and users so our pivot table name will need to be account_user to minimize the amount of work we have to do later. Again, I recommend sticking to the process that generates the cleanest code.

$ php artisan make:migration create_account_user
Created Migration: 2019_11_02_015509_create_account_user

We also need to make sure we have a [firstModel]_id and a [secondModel]_id column in the table. I also setup the foreign key relationships but that’s not explicitly required.

public function up()
{
    Schema::create('account_user', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->timestamps();
        $table->unsignedBigInteger('user_id');
        $table->unsignedBigInteger('account_id');

        $table
            ->foreign('user_id')
            ->references('id')
            ->on('users');

        $table
            ->foreign('account_id')
            ->references('id')
            ->on('accounts');
    });
}

I also created three test Accounts for our testing:

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.3.9-1+ubuntu16.04.1+deb.sury.org+1  cli) by Justin Hileman
>>> \App\Account::create(['companyName'=>'The Big One']);
=> App\Account {#3003
     companyName: "The Big One",
     updated_at: "2019-11-02 02:00:57",
     created_at: "2019-11-02 02:00:57",
     id: 1,
   }
>>> \App\Account::create(['companyName'=>'Scott's Awesome Company']);
PHP Parse error: Syntax error, unexpected T_STRING, expecting ',' or ')' or ']' on line 1
>>> \App\Account::create(['companyName'=>'Scott\'s Awesome Company']);
=> App\Account {#3004
     companyName: "Scott's Awesome Company",
     updated_at: "2019-11-02 02:01:13",
     created_at: "2019-11-02 02:01:13",
     id: 2,
   }
>>> \App\Account::create(['companyName'=>'Awesome Company']);
=> App\Account {#3011
     companyName: "Awesome Company",
     updated_at: "2019-11-02 02:01:32",
     created_at: "2019-11-02 02:01:32",
     id: 3,
   }
>>> 

The Models

In our User model we’re going to create an accounts() function that calls the belongsToMany function with the other model (App\Account in this case) as the parameter.

class User extends Authenticatable
{
    /**
     * Get the accounts
     */
    public function accounts()
    {
        return $this->belongsToMany('App\Account');
    }
}

We also need to create a users() function in our Account model that also calls the belongsToMany function so we can reference these both ways.

class Account extends Model
{
    /**
     * Get the users
     */
    public function users()
    {
        return $this->belongsToMany('App\User');
    }
}

Again, this is shorthand for the following:

return $this->belongsToMany('App\User', 'account_user', 'user_id', 'account_id');

Where account_user is the pivot table and user_id and account_id are the columns that determine the relationship.

In case you’re wondering why we didn’t create an Account_User model it’s because we don’t need one. Eloquent is smart enough to handled this all on it’s own without an extra class mucking up our source code.

Now that we have all these functions setup to define the relationships between the models we can use the attach() function to link a user to an account:

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.3.9-1+ubuntu16.04.1+deb.sury.org+1  cli) by Justin Hileman
>>> $user = \App\User::find(2);
=> App\User {#3014
     id: 2,
     name: "Scott",
     email: "Scott",
     email_verified_at: null,
     created_at: "2019-11-02 01:41:34",
     updated_at: "2019-11-02 01:41:34",
     subscriber_id: 1,
   }
>>> $user->accounts()->attach(\App\Account::find(3));
=> null

And then we can use the accounts property to see all the Accounts tied to the User.

>>> $user->accounts
=> Illuminate\Database\Eloquent\Collection {#3023
     all: [
       App\Account {#3013
         id: 3,
         created_at: "2019-11-02 02:01:32",
         updated_at: "2019-11-02 02:01:32",
         companyName: "Awesome Company",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3021
           user_id: 2,
           account_id: 3,
         },
       },
     ],
   }
>>> 

And the opposite:

>>> $account = \App\Account::find(1);
=> App\Account {#3029
     id: 1,
     created_at: "2019-11-02 02:00:57",
     updated_at: "2019-11-02 02:00:57",
     companyName: "The Big One",
   }
>>> $account->users()->attach($user);
=> null
>>> $account->users
=> Illuminate\Database\Eloquent\Collection {#3032
     all: [
       App\User {#3025
         id: 2,
         name: "Scott",
         email: "Scott",
         email_verified_at: null,
         created_at: "2019-11-02 01:41:34",
         updated_at: "2019-11-02 01:41:34",
         subscriber_id: 1,
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3011
           account_id: 1,
           user_id: 2,
         },
       },
     ],
   }
>>> 

There is also a detach() function that’s the opposite of the attach() function:

>>> $account->users()->detach($user);
=> 1
>>> $account->users
=> Illuminate\Database\Eloquent\Collection {#3029
     all: [],
   }
>>>

attach() and detach() will also work with an id instead of an object:

$shop->products()->attach($product_id);

And you can also use the sync() function to specify a list of relationships and Eloquent will insert the missing values:

// adds missing values
$shop->products()->sync([1,2,3]);

Order By

As a quick note this process allows you to order the results in case you need them sorted:

>>> $user->accounts()->orderBy('companyName')->get()
=> Illuminate\Database\Eloquent\Collection {#3002
     all: [
       App\Account {#3023
         id: 3,
         created_at: "2019-11-02 02:01:32",
         updated_at: "2019-11-02 02:01:32",
         companyName: "Awesome Company",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3011
           user_id: 2,
           account_id: 3,
         },
       },
       App\Account {#3028
         id: 1,
         created_at: "2019-11-02 02:00:57",
         updated_at: "2019-11-02 02:00:57",
         companyName: "The Big One",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3004
           user_id: 2,
           account_id: 1,
         },
       },
     ],
   }
>>> 

Where

What if you have a slightly more complicated relationship with your Accounts? For example, you might have a sales representative who closed the Account and an account representative who helps them if they have problems. Eloquent has a solution for this.

First where going to add two columns to our account_user table.

$ php artisan make:migration add_fields_to_account_user
Created Migration: 2019_11_02_105825_add_fields_to_account_user
public function up()
{
    Schema::table('account_user', function (Blueprint $table) {
        $table->boolean('sales_rep')->default('0');
        $table->boolean('account_rep')->default('0');
    });
}

Now in our account table we can define these relationship in some new functions:

public function salesReps()
{
    return $this->belongsToMany('App\User')->wherePivot('sales_rep', 1);
}

public function accountReps()
{
    return $this->belongsToMany('App\User')->wherePivot('account_rep', 1);
}

Then when we attach the Users to the Accounts we need to specify the column values:

>>> $account->users()->attach($salesRep, ['sales_rep' => 1]);
=> null
>>> $account->users()->attach($accountRep, ['account_rep' => 1]);
=> null
>>> 

Then we can use the salesReps and accountReps attributes to access the Users just like we used the users before:

>>> $account->salesReps
=> Illuminate\Database\Eloquent\Collection {#3030
     all: [
       App\User {#3020
         id: 3,
         name: "Sales Rep",
         email: "Sales Rep",
         email_verified_at: null,
         created_at: "2019-11-02 11:04:16",
         updated_at: "2019-11-02 11:04:16",
         subscriber_id: 1,
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3021
           account_id: 1,
           user_id: 3,
         },
       },
     ],
   }
>>> $account->accountReps
=> Illuminate\Database\Eloquent\Collection {#3044
     all: [
       App\User {#3034
         id: 5,
         name: "Account Rep",
         email: "Account Rep",
         email_verified_at: null,
         created_at: "2019-11-02 11:05:05",
         updated_at: "2019-11-02 11:05:05",
         subscriber_id: 1,
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3038
           account_id: 1,
           user_id: 5,
         },
       },
     ],
   }
>>> 

This is really only scratching the surface of what can be done with relationship in Eloquent but I think it covers most of the important parts.