Filament Many-to-Many Select: Automatically Fill Pivot Values

If you have a many-to-many relationship and want to fill the pivot table values automatically with Filament form, it's better to do it on Eloquent level, instead of Filament. Let me show you how.

This tutorial idea comes from a question on the official Filament Discord:

Here's the form I reproduced:

And we need to fill in the confirmation code automatically for each contact.


Preparation: DB Structure and Visual Form

I've tried to re-create the exact situation from the Discord question, simplifying the columns to the bare minimum needed. Here's the structure I've come up with.

Here's the code for DB migrations:

Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
 
// ...
 
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->text('message_text');
$table->timestamps();
});
 
// ...
 
Schema::create('contact_message', function (Blueprint $table) {
$table->foreignId('contact_id')->constrained();
$table->foreignId('message_id')->constrained();
$table->string('confirmation_code');
$table->date('confirmation_date')->nullable();
$table->string('status');
});

As you can see, the pivot column confirmation_date is nullable, with the idea that it's NULL by default and is filled in later after the confirmation.

The other two pivot columns are not nullable, which means they are required to be filled in.

Here's the code for the Models and the belongsToMany relationship between them.

app/Models/Contact.php:

class Contact extends Model
{
use HasFactory;
 
protected $fillable = ['name'];
}

app/Models/Message.php:

class Message extends Model
{
use HasFactory;
 
protected $fillable = ['message_text'];
 
public function contacts(): BelongsToMany
{
return $this->belongsToMany(Contact::class)
->withPivot(['confirmation_code', 'confirmation_date', 'status']);
}
}

And here's the code for the Filament form to create a message and choose the recipients.

app/Filament/Resources/MessageResource.php:

public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Textarea::make('message_text'),
Forms\Components\Select::make('contacts')
->relationship('contacts', 'name')
->multiple(),
]);
}

Here's the visual result:

Now, the main question is how to generate the pivot table fields automatically?


Option 1. Eloquent Pivot Model: booted()

Instead of creating the values in Filament, we can make it on the Eloquent level.

It means that this automation would work even if the record is created outside of Filament, in Laravel via some Artisan command or Queued Job.

For that, we need to create a specific Model for the pivot table. Laravel doesn't require this, but we need the Model if we want to catch the events on the pivot table.

php artisan make:model ContactMessage --pivot

Its structure is a bit different from a typical Eloquent Model. It extends a Pivot class.

use Illuminate\Database\Eloquent\Relations\Pivot;
 
class ContactMessage extends Pivot
{
//
}

So, here, we need to add a method booted() with a creating() method inside.

use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Str;
 
class ContactMessage extends Pivot
{
public static function booted(): void
{
static::creating(function ($record) {
$record->confirmation_code = Str::random(6);
$record->status = 'inactive';
});
}
}

We also need to register that we use this Pivot Model, in the relationship method:

app/Models/Message.php:

class Message extends Model
{
// ...
 
public function contacts(): BelongsToMany
{
return $this->belongsToMany(Contact::class)
->using(ContactMessage::class)
->withPivot(['confirmation_code', 'confirmation_date', 'status']);
}
}

And now, we don't need to add anything in Filament. The Eloquent Pivot Model will take care of everything.


Option 2. Eloquent Observers.

Another alternative to the booted() method in the Model is to create an Eloquent Observer for that Pivot model.

php artisan make:observer ContactMessageObserver --model=ContactMessage

And then that creating() method goes inside of the Observer class instead of the Model booted() method:

app/Observers/ContactMessageObserver.php:

namespace App\Observers;
 
use App\Models\ContactMessage;
use Illuminate\Support\Str;
 
class ContactMessageObserver
{
/**
* Handle the ContactMessage "creating" event.
*/
public function creating(ContactMessage $contactMessage): void
{
$contactMessage->confirmation_code = Str::random(6);
$contactMessage->status = 'inactive';
}
 
}

Finally, to make it work, we need to register that observer. A new common way is a PHP attribute on top of the Eloquent Model class:

app/Models/Message.php:

use App\Observers\ContactMessageObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Str;
 
#[ObservedBy(ContactMessageObserver::class)]
class ContactMessage extends Pivot
{
// public static function booted(): void
// {
// static::creating(function ($record) {
// $record->confirmation_code = Str::random(6);
// $record->status = 'inactive';
// });
// }
}

If you want to play around with this project, I've published a GitHub repository.


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.

avatar

->pivotData() is available in the select field

https://filamentphp.com/docs/3.x/forms/fields/select/#saving-pivot-data-to-the-relationship

👍 4

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 59 courses (1057 lessons, total 42 h 44 min)
  • 78 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials