Many to many / Belongs To many
A many-to-many relationship between two models using an intermediate pivot table to link records.
When to Use
Use BelongsToMany when both models can have multiple related records. Common examples:
- Students belong to many Courses (and Courses have many Students)
- Users belong to many Roles (and Roles have many Users)
- Products belong to many Tags (and Tags have many Products)
Database Structure
students
--------
id (primary key)
name
email
enrollment_date
courses
-------
id (primary key)
title
code
credits
course_student (pivot)
----------------------
id (primary key)
course_id (foreign key to courses)
student_id (foreign key to students)
grade (optional pivot data)
created_at
updated_at
Pivot Table Naming Convention
The pivot table name should be derived from both table names in alphabetical order and singular form:
course_student(correct)- NOT
student_course - NOT
courses_students
Defining the Relation
use Michalsn\CodeIgniterRelations\Relations\BelongsToMany;
use Michalsn\CodeIgniterRelations\Traits\HasRelations;
class StudentModel extends Model
{
use HasRelations;
protected $table = 'students';
protected $returnType = Student::class;
public function courses(): BelongsToMany
{
return $this->belongsToMany(CourseModel::class);
}
}
class CourseModel extends Model
{
use HasRelations;
protected $table = 'courses';
protected $returnType = Course::class;
public function students(): BelongsToMany
{
return $this->belongsToMany(StudentModel::class);
}
}
Custom Pivot Table and Keys
public function courses(): BelongsToMany
{
return $this->belongsToMany(
CourseModel::class,
'enrollments', // Custom pivot table
'student_key', // Parent foreign key in pivot
'course_key', // Related foreign key in pivot
'id', // Parent primary key
'id' // Related primary key
);
}
Reading Data
Eager Loading
// Get student with courses
$student = model(StudentModel::class)->with('courses')->find(1);
foreach ($student->courses as $course) {
echo "{$course->title} ({$course->code})";
}
// Get course with students
$course = model(CourseModel::class)->with('students')->find(1);
foreach ($course->students as $student) {
echo $student->name;
}
Lazy Loading
$student = model(StudentModel::class)->find(1);
foreach ($student->courses as $course) {
echo $course->title; // Courses are loaded automatically
}
With Query Constraints
$student = model(StudentModel::class)
->with('courses', fn($model) => $model->where('courses.credits >', 3))
->find(1);
Pivot Data
Retrieving Pivot Columns
By default, only foreign keys are included. Use withPivot() to retrieve additional columns:
public function courses(): BelongsToMany
{
return $this->belongsToMany(CourseModel::class)
->withPivot('grade'); // Single column
}
public function coursesWithDetails(): BelongsToMany
{
return $this->belongsToMany(CourseModel::class)
->withPivot(['grade', 'enrolled_at', 'completed_at']); // Multiple columns
}
Access pivot data on related records:
$student = model(StudentModel::class)->with('courses')->find(1);
foreach ($student->courses as $course) {
echo $course->title;
echo $course->pivot->student_id; // Foreign key (always included)
echo $course->pivot->course_id; // Foreign key (always included)
echo $course->pivot->grade; // Additional column from withPivot()
}
Custom Pivot Accessor
Customize the pivot property name using as():
public function courses(): BelongsToMany
{
return $this->belongsToMany(CourseModel::class)
->withPivot('grade')
->as('enrollment'); // Custom accessor
}
// Usage
$course->enrollment->grade;
$course->enrollment->student_id;
Including Timestamps
Use withTimestamps() to automatically include created_at and updated_at:
public function courses(): BelongsToMany
{
return $this->belongsToMany(CourseModel::class)
->withPivot('grade')
->withTimestamps();
}
// Access timestamps
$course->pivot->created_at;
$course->pivot->updated_at;
Custom timestamp column names:
public function courses(): BelongsToMany
{
return $this->belongsToMany(CourseModel::class)
->withTimestamps('enrolled_at', 'modified_at');
}
Writing Data
Attaching Records
Use attach() to create pivot table entries:
$student = model(StudentModel::class)->find(1);
// Attach single course
$student->courses()->attach(3);
// Attach multiple courses
$student->courses()->attach([3, 4, 5]);
// Attach using entity
$course = model(CourseModel::class)->find(3);
$student->courses()->attach($course);
// Attach multiple entities
$courses = model(CourseModel::class)->find([3, 4, 5]);
$student->courses()->attach($courses);
Attaching with Pivot Data
// Single course with pivot data
$student->courses()->attach(3, ['grade' => 'A', 'enrolled_at' => '2024-01-15']);
// Multiple courses with same pivot data
$student->courses()->attach([3, 4, 5], ['grade' => 'B']);
// Multiple courses with different pivot data
$student->courses()->attach([
3 => ['grade' => 'A', 'enrolled_at' => '2024-01-15'],
4 => ['grade' => 'B+', 'enrolled_at' => '2024-01-20'],
5 => ['grade' => 'A-', 'enrolled_at' => '2024-01-25'],
]);
When using withTimestamps(), timestamps are automatically added.
Detaching Records
Use detach() to remove pivot table entries:
$student = model(StudentModel::class)->find(1);
// Detach single course
$count = $student->courses()->detach(3);
// Detach multiple courses
$count = $student->courses()->detach([3, 4]);
// Detach all courses
$count = $student->courses()->detach();
Syncing Relationships
Use sync() to make the relationship match exactly a list of IDs:
$student = model(StudentModel::class)->find(1);
// Student will have exactly these courses
$result = $student->courses()->sync([1, 3, 5]);
// Returns what changed
echo count($result['attached']); // Newly attached IDs
echo count($result['detached']); // Removed IDs
// Sync to empty (detach all)
$student->courses()->sync([]);
Syncing with Pivot Data
// Same pivot data for all
$result = $student->courses()->sync([1, 2, 3], ['active' => true]);
// Different pivot data for each
$result = $student->courses()->sync([
1 => ['grade' => 'A', 'active' => true],
2 => ['grade' => 'B+', 'active' => true],
3 => ['grade' => 'A-', 'active' => false],
]);
// Mixed format
$result = $student->courses()->sync([
1 => ['grade' => 'A+'], // Update grade
2, // Keep (no pivot changes)
3 => ['grade' => 'B'], // Update or set grade
]);
When syncing with pivot data, the result includes:
attached- Newly attached IDsdetached- Removed IDsupdated- Existing IDs with updated pivot data
Creating and Attaching New Records
Use save() to create a new related record and automatically attach it:
$student = model(StudentModel::class)->find(1);
// Create new course and attach
$course = $student->courses()->save([
'title' => 'Machine Learning',
'code' => 'CS405',
'credits' => 4,
]);
// Using entity
$newCourse = new Course();
$newCourse->title = 'Deep Learning';
$newCourse->code = 'CS505';
$newCourse->credits = 5;
$course = $student->courses()->save($newCourse);
Important: save() only works with NEW records (no primary key). Use attach() for existing records.
Use saveMany() to create and attach multiple new records:
$student = model(StudentModel::class)->find(1);
// Create and attach multiple courses
$ids = $student->courses()->saveMany([
['title' => 'AI Basics', 'code' => 'CS501', 'credits' => 3],
['title' => 'Neural Networks', 'code' => 'CS502', 'credits' => 4],
]);
// Disable transaction for partial success
$ids = $student->courses()->saveMany([...], useTransaction: false);
Available Methods
| Method | Description |
|---|---|
attach($ids, $pivotData = []) |
Create pivot entries to link existing records |
detach($ids = null) |
Remove pivot entries (detach all if no IDs provided) |
sync($ids, $pivotData = []) |
Synchronize to match exactly the provided IDs |
save($data) |
Create new related record and attach it |
saveMany($data, $useTransaction = true) |
Create multiple new related records and attach them |
withPivot($columns) |
Include additional pivot table columns |
withTimestamps($created = 'created_at', $updated = 'updated_at') |
Include pivot timestamps |
as($accessor) |
Customize pivot property name |