Laravel Relationships Unlocked: Using belongsToMany, attach, and detach
The many-to-many relationship is slightly more complicated than the relationships that we’ve looked at so far. For our example, we have a PersonalCar
and that PersonalCar
has images. If we looked at the inverse of it, the Image
can have one or more PersonalCars
. How do you store the relationship? Does PersonalCar
need to have an image_id
(foreign id) inside it and does Image
also need a personal_car_id
inside it? This is where the concept of pivot tables come in. It’s a table that stores the foreign id of each table.
The Pivot Table Migration
We have already looked at the pivot table migration when we were creating our database structure, but what does it actually mean?
https://medium.com/geekculture/laravel-p35-database-setup-cmp-e9ab2aba05ae
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('image_personal_car', function (Blueprint $table) {
$table->foreignId('personal_car_id');
$table->foreignId('image_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('image_personal_car');
}
};
Our pivot tables are named uniquely: they merge the names of the two tables that we’re trying to link. In this case, images
and personal_cars
. Each table’s plural form is reduced to the singular form, the tables are sorted in alphabetical order, and are linked with underscores. Singular forms of these two tables are image
and personal_car
. They’re already sorted alphabetically since image
comes before personal_car
(i
before p
). We simply need to link them with underscores: image_personal_car
. And your pivot table is created with the personal_car_id
and image_id
foreign id fields inside it.
Relationship Setup (Many to Many)
The many-to-many relationship is defined with the belongsToMany
method. Let’s create an images
method inside of our PersonalCar
mo
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PersonalCar extends Model
{
use HasFactory;
protected $fillable = [
'year', 'is_manual', 'exterior_color', 'purchase_amount', 'current_value', 'sales_amount', 'date_purchased', 'date_sold'
];
public function brand()
{
return $this->belongsTo(PersonalCarBrand::class, 'personal_car_brand_id', 'id');
}
public function model()
{
return $this->belongsTo(PersonalCarModel::class, 'personal_car_model_id', 'id');
}
public function images()
{
return $this->belongsToMany(Image::class);
}
}
belongsToMany(Image::class)
code to your images
method. Now what? Well, let’s try to retrieve it.> PersonalCar::with('images')->first();
[!] Aliasing 'PersonalCar' to 'App\Models\PersonalCar' for this Tinker session.
= App\Models\PersonalCar {#4718
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
images: Illuminate\Database\Eloquent\Collection {#4716
all: [],
},
}
By running the PersonalCar::with(‘images’)->first()
in tinker
, we can see that images
is being pulled, but there are no images attached. That’s to be expected. We never populated our images
table nor our image_personal_car
pivot table. How do we do that?
attach()
To add an entry into our image_personal_car
pivot table, we’ll need to have values in both our images
table and our personal_cars
table. Once they’re both in there, we’ll use the attach()
method to link the two.
Let’s create a new Image
in tinker
.
> Image::create(['url' => '2007-chevy-corvette.jpg', 'alt' => '2007 Chevy Corvette']);
[!] Aliasing 'Image' to 'App\Models\Image' for this Tinker session.
= App\Models\Image {#4728
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
updated_at: "2023-01-22 13:15:11",
created_at: "2023-01-22 13:15:11",
id: 1,
}
The image is now inserted into the images
table. We can verify that by extracting all of the images in the images
table.
> Image::get();
= Illuminate\Database\Eloquent\Collection {#4723
all: [
App\Models\Image {#3760
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
},
],
}
We’ll need to now get and assign the PersonalCar
to a variable and then the image that we just created to a variable of its own.
> $car = PersonalCar::first();
= App\Models\PersonalCar {#3768
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
}
The $car
variable contains the first record from our personal_cars
table.
> $image = Image::first();
= App\Models\Image {#4716
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
}
The $image
variable contains the first record from our images
table.
Time to link them together. Let’s use attach()
. But where? This will need to be done through our relationship that we have defined inside of our PersonalCar
model, specifically the images()
relationship.
> $car->images()->attach($image);
= null
Make sure that you use images()
with the parentheses since you need to eloquent relationship, not the result. Since we’re starting off in the $car
we need to attach($image)
. Let’s see if our $car
has the images
relationship associated with it.
> $car->images;
= Illuminate\Database\Eloquent\Collection {#4730
all: [
App\Models\Image {#4733
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4101
personal_car_id: 1,
image_id: 1,
},
},
],
}
Lets try using our with()
method and see if it returns the images associated with the PersonalCar
.
> PersonalCar::with('images')->first();
= App\Models\PersonalCar {#4736
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
images: Illuminate\Database\Eloquent\Collection {#4743
all: [
App\Models\Image {#4746
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4744
personal_car_id: 1,
image_id: 1,
},
},
],
},
}
>
It does. Success. Not too bad of a concept when you break it down piece by piece. Can we do the same with our Image
model? Of course.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Image extends Model
{
use HasFactory;
protected $fillable = [
'url', 'alt',
];
public function cars()
{
return $this->belongsToMany(PersonalCar::class);
}
}
Let’s try creating a new image and associating it through our Image
model. Make sure to restart tinker
.
> $image_two = Image::create(['url' => '2003-chevy-corvette.jpg', 'alt' => '2003 Chevy Corvette']);
= App\Models\Image {#4748
url: "2003-chevy-corvette.jpg",
alt: "2003 Chevy Corvette",
updated_at: "2023-01-22 13:29:50",
created_at: "2023-01-22 13:29:50",
id: 2,
}
> $car = PersonalCar::first();
[!] Aliasing 'PersonalCar' to 'App\Models\PersonalCar' for this Tinker session.
= App\Models\PersonalCar {#4719
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
}
Image
model, we can use it to attach $car
as well. It really doesn’t matter which way you go.> $image_two->cars()->attach($car);
= null
Now to test whether we have two images associated with our first car.
> $car->images;
= Illuminate\Database\Eloquent\Collection {#4503
all: [
App\Models\Image {#3760
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4717
personal_car_id: 1,
image_id: 1,
},
},
App\Models\Image {#3776
id: 3,
url: "2003-chevy-corvette.jpg",
alt: "2003 Chevy Corvette",
created_at: "2023-01-22 13:30:08",
updated_at: "2023-01-22 13:30:08",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4504
personal_car_id: 1,
image_id: 3,
},
},
],
}
There they are.
detach()
What if we wanted to remove the relationship? In other words, we no longer wanted to have an Image
linked to a PersonalCar
. We need to use the detach()
method. Let’s create a third image, attach it, and then detach it.
> $image_three = Image::create(['url' => 'test.jpg', 'alt' => 'Test Image']);
= App\Models\Image {#4734
url: "test.jpg",
alt: "Test Image",
updated_at: "2023-01-22 13:36:52",
created_at: "2023-01-22 13:36:52",
id: 4,
}
> $car = PersonalCar::first();
= App\Models\PersonalCar {#4725
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
}
> $car->images()->attach($image_three);
= null
$car
.> PersonalCar::with('images')->first();
= App\Models\PersonalCar {#4728
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
images: Illuminate\Database\Eloquent\Collection {#4740
all: [
App\Models\Image {#4745
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4743
personal_car_id: 1,
image_id: 1,
},
},
App\Models\Image {#4746
id: 3,
url: "2003-chevy-corvette.jpg",
alt: "2003 Chevy Corvette",
created_at: "2023-01-22 13:30:08",
updated_at: "2023-01-22 13:30:08",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4742
personal_car_id: 1,
image_id: 3,
},
},
App\Models\Image {#4748
id: 4,
url: "test.jpg",
alt: "Test Image",
created_at: "2023-01-22 13:36:52",
updated_at: "2023-01-22 13:36:52",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4741
personal_car_id: 1,
image_id: 4,
},
},
],
},
}
Before we delete it, lets see what our pivot table looks like in mysql
.
mysql> select * from image_personal_car;
+-----------------+----------+
| personal_car_id | image_id |
+-----------------+----------+
| 1 | 1 |
| 1 | 3 |
| 1 | 4 |
+-----------------+----------+
3 rows in set (0.00 sec)
mysql>
Pretty cool. I want to delete the last relationship 1
and 4
. This is the one that has test.jpg
as our image.
First, we need to pull up that image.
> $image = Image::find(4);
= App\Models\Image {#4729
id: 4,
url: "test.jpg",
alt: "Test Image",
created_at: "2023-01-22 13:36:52",
updated_at: "2023-01-22 13:36:52",
}
PersonalCar
with the id
of 1
.> $car = PersonalCar::find(1);
= App\Models\PersonalCar {#4745
id: 1,
year: "2022",
personal_car_brand_id: 6,
personal_car_model_id: 6,
is_manual: 1,
exterior_color: "itaque",
purchase_amount: 21846038,
current_value: 635120796,
sales_amount: 0,
date_purchased: "2014-01-18",
date_sold: null,
created_at: "2023-01-20 20:37:11",
updated_at: "2023-01-20 20:37:11",
}
>
Now to detach. We can detach from Image
or from PersonalCar
. In other words, both of these are valid:
$car->images()->detach($image);
$image->cars()->detach($car);
I’ll execute the first one.
> $car->images()->detach($image);
= 1
PersonalCar
and that image will be removed.> $car->images;
= Illuminate\Database\Eloquent\Collection {#4740
all: [
App\Models\Image {#4749
id: 1,
url: "2007-chevy-corvette.jpg",
alt: "2007 Chevy Corvette",
created_at: "2023-01-22 13:15:11",
updated_at: "2023-01-22 13:15:11",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4748
personal_car_id: 1,
image_id: 1,
},
},
App\Models\Image {#4751
id: 3,
url: "2003-chevy-corvette.jpg",
alt: "2003 Chevy Corvette",
created_at: "2023-01-22 13:30:08",
updated_at: "2023-01-22 13:30:08",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4739
personal_car_id: 1,
image_id: 3,
},
},
],
}
I hope that this article has helped you demystify many-to-many relationships. It seems like this topic is sometimes difficult for developers to grasp, which is why I dedicated an entire article on it. We’ll come back to this concept, and many others revolving relationships, when we do a more thorough deep dive later.
Laravel Series
Continue your Laravel Learning.
Mastering Models in Laravel: $fillable, belongsTo, and hasMany
Laravel – P37: CMP – Models $Fillable,Belongsto and Hasmany
Explore Laravel models with a focus on $fillable, belongsTo, and hasMany. Understand how to efficiently manage relationships and data handling in your projects.
Laravel Relationships Unlocked: Using belongsToMany, attach, and detach
Laravel – P38: CMP – Many To Many Relationship belongsToMany, attach, detach
Learn how to manage many-to-many relationships in Laravel using belongsToMany, attach, and detach. Simplify complex data links in your applications.
Streamlining Laravel: Routes and Controllers High-Level Overview.
Laravel – P39: CMP – Routes And Controllers High-Level Setup
Discover how to efficiently set up routes and controllers in Laravel. A high-level guide to streamline your application’s foundational structure.