Custom pivot models — using(), withPivot()
Advanced5 min read·lv-12-024
sql
Concept
Custom pivot models give you a first-class Eloquent model for the pivot table in a many-to-many relationship, enabling accessors, casts, methods, and events on the pivot data itself.
By default, pivot data is accessed as a basic Illuminate\Database\Eloquent\Relations\Pivot object on $model->pivot. With a custom pivot model, you get the full power of Eloquent.
Setup:
- Create a class extending
Illuminate\Database\Eloquent\Relations\Pivot. - Reference it in the relationship with
->using(PivotClass::class). - Declare any extra pivot columns with
->withPivot('column1', 'column2'). - Optionally add
->withTimestamps().
as(string $name): Rename ->pivot to a more descriptive accessor: ->as('subscription') → $user->plan->subscription->expires_at.
Pivot model limitations:
- Pivot models do NOT have
HasFactoryby default. incrementingdefaults tofalseunless the pivot table has its own auto-increment PK.- To use events on a pivot model, the pivot table needs its own
idcolumn and the relationship needs->withPivot('id').
Querying via pivot model: The pivot model is NOT queryable directly like a regular model (Pivot::where() won't work well). Query through the relationship instead.
Code Example
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Casts\Attribute;
// Custom pivot model
class UserRole extends Pivot
{
protected $table = 'role_user'; // explicit table name
public $incrementing = false; // no auto-increment ID unless added
protected $casts = [
'assigned_at' => 'datetime',
'expires_at' => 'datetime',
'permissions' => 'array',
];
// Accessor on pivot data
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at->isPast();
}
public function remainingDays(): int
{
if ($this->expires_at === null) return PHP_INT_MAX;
return max(0, now()->diffInDays($this->expires_at, false));
}
}
// User model
class User extends \Illuminate\Database\Eloquent\Model
{
public function roles(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Role::class)
->using(UserRole::class) // custom pivot class
->as('assignment') // rename ->pivot to ->assignment
->withPivot(['assigned_at', 'expires_at', 'permissions'])
->withTimestamps();
}
}
// Subscription example — pivot with its own table
class PlanSubscription extends Pivot
{
protected $table = 'plan_user';
public $incrementing = true;
protected $casts = [
'started_at' => 'datetime',
'renews_at' => 'datetime',
'cancelled_at' => 'datetime',
];
public function isActive(): bool
{
return $this->cancelled_at === null && $this->renews_at->isFuture();
}
}
// Usage
$user = User::with('roles')->find(1);
foreach ($user->roles as $role) {
echo $role->name;
echo $role->assignment->assigned_at->toDateString(); // from custom pivot
if ($role->assignment->isExpired()) {
$user->roles()->updateExistingPivot($role->id, ['expires_at' => now()->addYear()]);
}
}
// Attaching with pivot data
$user->roles()->attach(3, [
'assigned_at' => now(),
'expires_at' => now()->addYear(),
'permissions' => ['read', 'write'],
]);