Courses

Filament 3 From Scratch: Practical Course

BelongsToMany: Multi-Select and Relation Managers

Summary of this lesson:
- Setting up many-to-many relationships
- Using select multiple field
- Implementing relation managers
- Managing attach/detach functionality in the relation managers

Now, let's see how we can add data to a many-to-many relationship. Let's say that our Product may have many Tags.

For Tags, I've created a simple Model with one field, seeded a few tags, and then generated this simple Filament Resource:

php artisan make:filament-resource Tag --simple --generate

The result is this:

But, of course, it's not the topic of this lesson. What we need to do is add tags to products. Multiple tags.

The relationship in Eloquent is this:

app/Models/Product.php:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Product extends Model
{
// ...
 
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
}

There are a few ways to do it.


Option 1. Just Attach: Select Multiple

Let's add one more field in the form of ProductResource.

app/Filament/Resources/ProductResource.php:

return $form
->schema([
// ...
Forms\Components\Select::make('category_id')
->relationship('category', 'name'),
Forms\Components\Select::make('tags')
->relationship('tags', 'name')
->multiple(),
]);

Yes, that's it, all you need to do is add ->multiple() to the Select field, and it becomes a multi-select!

It also includes a search with auto-complete by default. Here's how it looks:


Option 2. Create and Attach: Relation Managers

Filament also offers a function to create a related record immediately without leaving the current form. So, if you want to create a Product and then create a Tag "on-the-fly", you can use Relation Manager.

Have you noticed that we haven't used this auto-generated method of the Resource?

app/Filament/Resources/ProductResource.php:

class ProductResource extends Resource
{
// ...
 
public static function getRelations(): array
{
return [
//
];
}
}

So yeah, this is the exact method we will fill now.

Let's generate a Relation Manager, which will contain a Table and a Form, like a Resource:

php artisan make:filament-relation-manager ProductResource tags name

We need to provide three things:

  • The name of our primary resource: ProductResource
  • The relationship name: tags
  • What "tags" field is used to identify tags: name

Here's the file that has been generated for us. It may look like a lot of code, but similarly to the Filament Resource, you wouldn't need to care about most of it. In this case, we will not edit that file at all.

app/Filament/Resources/ProductResource/RelationManagers/TagsRelationManager.php:

namespace App\Filament\Resources\ProductResource\RelationManagers;
 
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
 
class TagsRelationManager extends RelationManager
{
protected static string $relationship = 'tags';
 
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
]);
}
 
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
]);
}
}

Then we register that Relation Manager in the Resource method I mentioned above:

app/Filament/Resources/ProductResource.php:

class ProductResource extends Resource
{
// ...
 
public static function getRelations(): array
{
return [
RelationManagers\TagsRelationManager::class,
];
}
}

And that's it. We have our Relation Manager, which is represented by a separate CRUD table below our Product create/edit form:

So, here you can manage tags to products, edit them, and create new ones.


Attach and Detach Existing Tags

We can now add and delete new tags in the relation manager. But what if we need to attach existing tags or detach tags from products without deleting them?

When creating Relation Manager, we can pass --attach flag to add attach actions to the Relation Manager.

If you forgot to pass this flag or decided later that you need such a feature, you can manually add actions to the table in the Relation Manager.

app/Filament/Resources/ProductResource/RelationManagers/TagsRelationManager.php

class TagsRelationManager extends RelationManager
{
// ...
 
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
Tables\Actions\AttachAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
Tables\Actions\DetachAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
Tables\Actions\DetachBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
]);
}
}

Now, in the table, we have additional actions.

For this feature to work, the Tag Model also needs to relation to the Products Model.

app/Models/Tag.php:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Tag extends Model
{
use HasFactory;
 
protected $fillable = ['name'];
 
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class);
}
}

So now we can attach existing tags or detach them without deleting them.


Show BelongsToMany in Table

The final thing in this lesson is to show product tags in the table.

Guess what? The exact syntax of TextColumn::make('[relationship].[field]') will work for both belongsTo and belongsToMany!

app/Filament/Resources/ProductResource.php:

return $table
->columns([
// ...
 
Tables\Columns\TextColumn::make('category.name'),
Tables\Columns\TextColumn::make('tags.name'),
])

Filament automatically generates the comma-separated string and shows it in the table column.


And that's it. That's all you need to know about the basics of many-to-many in Filament.

Of course, as with almost every topic in this course, there are many more options and parameters that Filament offers. Dive into the official documentation for those.

Previous: Generate Simple Resources and hasMany Count
avatar

This section seemed to be a little less considered around creating the pivot table and the requirement to include use Illuminate\Database\Eloquent\Relations\BelongsToMany;. I worked around these and eventually got it working but just noting it since there appears to be some missing connections in this lesson. Great course though.

👍 6
avatar

That is true, and I do like (and miss) some of the hand-holding sometimes, but Povilas did mention that the pivot tables would not be his emphasis topic of this lesson. Probably many ways to do it, but I just added the migration:

php artisan make:migration create_product_tag

then added the middle columns like so (there are other ways):

Schema::create('product_tag', function (Blueprint $table) { $table->id(); $table->foreignId('product_id'); $table->foreignId('tag_id'); $table->timestamps(); });

Add the relationships in the models (in Product.php add):

public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); }

add the inverse in Tag.php :

public function products(): BelongsToMany { return $this->belongsToMany(Product::class); }

Some sample data in the intermediary (pivot) table, and it worked fine.

avatar

Thanks @johnogle to provide this instruction.

@PBTraining, I've just added the "use BelongsToMany" line of code. But, as Johnogle said, in this course I'm not hand-holding for the Laravel part, as Filament is a tool ON TOP of Laravel, which implies that you already have the relationships/tables set up. The course is already quite long even without the Laravel part, and if something is not working for you, feel free to ask questions in the comments.

avatar

As always - great tutorial Povilas, thank you. And i like the text format, i think for this type of tutorials it's the best.

As for Laravel part, I would also prefer at least to see a mention of required pivot table and similar things. It's not too many extra lines of text and code, but would totally make it clear what we're working with.

avatar

I think that instead of literal hand-holding, it would be better just to give instructions as a list on what is required to move forward. So, something like:

  1. Make a Tag model with migration and seed data
  2. Create a pivot table so that a product can have many tags
  3. Update the Tag and Product models with the relevant relationships

As a bonus, it'd be nice for there to be links either to other courses or to the Laravel docs so that people can go off and fill in gaps / refresh their knowledge and then continue.

The only thing I found a bit disruptive to learning was the fact that I wasn't specifically told what I needed to do to prepare for the lesson. So then part way through, I was kinda thinking, "hmm.. I'm going to need a pivot table here"

I definitely agree with the idea of not adding lots of extra details about how to do this though as it's out of scope. But a few signposts would be nice I think :-)

avatar
You can use Markdown
avatar

Dear Povilas, the implementation for attaching existing tags is missing in the TagsRelationManager. While you've provided the capability to create new tags when adding a new product, the functionality to attach existing tags seems to be absent. This incompleteness in the relation manager was observed. I've also examined your source code, and it appears that this particular aspect remains the same.

avatar

same question

avatar
You can use Markdown
avatar
Davlatbek Ushurbakiyev

Hi everyone, I have a question.

Select::make('tags') ->relationship('tags', 'title') ->multiple(),

When use multiple(), there is not loading tags at the beggining, for example i need select filter, and when i try to use multiple i cant choose something because it hadnt loaded.

avatar

I think it is loaded when you start typing the first symbols then it's auto-completing it. You can enable/disable that behavior with ->searchable() from what I remember. Take a look at the docs for all Select options.

avatar
Davlatbek Ushurbakiyev

Thanks for an answer, I found solution in documentation, for this you can just do preload() method, in place where using multiple()

avatar
Luis Antonio Parrado

Chain ->preloadRecordSelect() method to Tables\Actions\AttachAction::make()

// ...
->headerActions([
    Tables\Actions\CreateAction::make(),
    Tables\Actions\AttachAction::make()
		->preloadRecordSelect(),
    ])
// ...
avatar
You can use Markdown
avatar

How would one Edit the AttachAction? seemd like this method is missing? if I have a pivot table with attributes, I can update the pivot attributes but not the _id fields in the the pivot table for example if I have an ingredient_recipe pivot table with attributes like quantity and unit per ingredient(row) when I add the IngredientsRelationsManager to the RecipeResource it updates the ingredients.name instead of the ingredient_id, ingredient_recipe in the pivot table the attributes are updated. thanks

avatar

Sorry, I don't exactly understand what you mean. Maybe show some demo or something.

avatar
You can use Markdown
avatar

Asking for any advice anyone has. I'm struggling to fnd the answer. Using method 1 above: If I have an additional required field on the pivot table (year) that should be populated by a text field on for submission. Any ideas on how to add it into the form?

I have tried:

  • Forms\Components\TextInput::make('category_tag.year')
  • Forms\Components\TextInput::make('category.year')
  • Forms\Components\TextInput::make('year')

But it keeps throughing an error SQLSTATE[HY000]: General error: 1364 Field 'year' doesn't have a default value

Frustrating the heck out of me cause I am sure it is stupid easy to fix.

avatar

Ooooh think I found a video on it... but it uses the relation manager (option 2). That will complicate things but meh, if it works I guess. https://www.youtube.com/watch?v=ERbpyZF8G5M

Single attaching is a PITA and not really what I am after. I'd much rather just have a simple multi select field and year field in the main form but I can't seem to get that to work.

avatar
You can use Markdown
avatar

How can I use the same form in the relation manager as on the related resource?

avatar

thank you

avatar
You can use Markdown
avatar

The Relation Manager is not shown on the Create pages, only on the Edit pages. The reason apparently is that the product id needs to be known, before tags can get added through the Relation Manager.

avatar
You can use Markdown
avatar

Hi - Great instructions, as always, from Povilas! I've set up a Panel for the user's to enter their data and am looking for a way to automatically attach the authorized user to a new model. The specific scenario is User, Teacher, and School. The Teacher belongsTo a User and the Teacher has a BelongsToMany relationship with the School. When the teacher creates a new school for herself, I want that automatically attached via the school_teacher table. This can be done manually on the Edit form, but it's an unnecessary step in my business case. Any thoughts on this? Thanks!

avatar

Hi, there are a few ways to do that.

  1. Using an observer - Automatically add the missing ID on saving event
  2. Using afterSave() on your create/edit form: https://filamentphp.com/docs/3.x/panels/resources/relation-managers#importing-related-records

Both ways will check for a specific event to happen and then add the data you need to the model

avatar

Thanks, Modestas, this is very helpful!

avatar
You can use Markdown
avatar
karlaustriainocencio

I'd like to know how to get the client_name though when I created it appears the name, I tried to edit but it appears only the id of the model. does anyone know how to do it appear the name?

// This is my code of the Select form component

Select::make('audit_id') ->label('Client') ->options(AuditSchedule::with('audit.clientCertType.client')->get()->pluck('audit.clientCertType.client.client_name', 'id')) ->searchable(),

avatar

Try this:
Select::make('audit_id')
->relationship('MODEL_RELATION_NAME', 'FIELD_NAME_TO_DISPLAY')
->label('Client')
->options(AuditSchedule::with('audit.clientCertType.client')->get()->pluck('audit.clientCertType.client.client_name', 'id'))
->searchable()\


An example of one of mine:
Select::make('discipline_id')->relationship('discipline', 'name')->searchable()->preload()->required(),

avatar
You can use Markdown
avatar

Great course! A little improvement would be to add a 'link-icon' next to the headers, so in my documentation I can link directly to for example the header 'Attach and Detach Existing Tags'.

avatar

I don't think all our headers are so important and clear so people would link to them. For some of them, yes, but if we make it in all headers for all lessons in all courses, it would not make much sense.

avatar
You can use Markdown
avatar

Really great course. A question:in the table product is possibile to show only some tags? filtered by a condition? I tried using something like: ->formatStateUsing(function ($record) { return $record->tags->filter->isCurrent()->pluck('name'); }) but not working.. I need something similar in my project, where I have to show on the table only memeberships of the current year Thanks!

avatar

You can create a custom view for this and display whatever is needed inside

avatar

You mean something similar to Creating CRM with Filament 3: Step-By-Step -> Creating Tags for Customers ?? Good idea, I haven't thought to do like this, i believed there was an easiest way.. on the query level

avatar

Yes, pretty much like that!

Of course, this can be done via query, but it's a bit too much to explain how. I can only give you a direction to use modifyQueryUsing() on the table itself, as anything else would require me to understand your database structure

avatar

First of all thank you for your answers (and sorry for my english "scolastic", but here in Italy we aren't so good as in other european countries!).

I solved the problem with a workaround: I created a new relationship on the User model:

public function currentMemberships(): HasMany
    {
        return $this->hasMany(Membership::class)
							->where('year',date('Y'));
    }

so I have a second relation with only the membership of the actual year and I used in the table instead of the normal memberships relation.

Just to let you know, the database has a normal User table, without strange "things", and a Membership table like this:

Schema::create('memberships', function (Blueprint $table) {
            $table->id();
						$table->foreignIdFor('User::class');
            $table->year('year');
            $table->tinyInteger('type');    //Cast con ENUM 
            $table->string('number');
            $table->timestamps();
        });

So I need in the users table to view only the users with a membership on the current year.

I don't know if my workaround is "elegant", but works, and it's reusable in other places!

avatar

This is actually a perfect workaround! This gives you more than just the table solution!

ps. Don't ever stress about your english - if we can understand you - it's GOOD! (we are also non-english speakers here :) )

avatar

Thanks a lot! You're a great team!!!

avatar

Hi laravel daily team! I disturb you with another problem, in the same I project:

I have races, and in every race a user could subscrive, but the price change as far is the race date (e.g. till 31/07 €10; from 01/08 to 31/08 € 15 etc..) So my idea is to create a tranches table, where I store expire date a cost, of corse a Race has many Tranches; and I think that Users and Tranches are connected with a many-to-many relationship (using pivot table). Here the db schema (just to explain easily): https://drawsql.app/teams/dr-capins/diagrams/teammanager

In theory everything works great, but in filament I have an Issue: I don't want to create a TrancheResurce (it doesn't make sense), so I manage tranches CRUD from RaceResource using a Relationship Manager (and it works correctly).

But now, how can I manage subscriptions to the race? The administrator needs to attach a user to the race (passing for one of the tranche model). Teorically I need a kind of belongsToManyThrough, because every race should have many users, but passing through the Tranche model. I really don't know hot to manage this in filament. Maybe with a page? But how?

The last resource for me is to change the db schema, connecting directly races, tranches and users in a 3-model-pivotTable, but I really dislike this solution, because the db will not be normalized anymore (in a pivot row of this table we will have user_id, race_id and tranche_id, but it's clear that race_id is redundant.

Do you think there should be a solution keeping the db schema? Is possible to "combine" to relationship? (I also look for the has-many-deep package of Stoudamire, but doesn't seems to be for my situation.

Thanks a lot for your attention.

avatar
You can use Markdown
avatar

Awesome, really I dont find any competitors to this package <3

avatar
You can use Markdown
avatar

Hi, I am wondering if it would be possible to create a simpler interface for adding /attaching tags (or related items) similar to how wordpress handles it. With the option to create hierarchical relationships

Sample

In many cases I think records would be predefined. And users should simply be able to connect them via a checkbox.

And if they need to add new they can do so.

I suppose the first method presented would be closer to this set up, but Im not sure if the check box options in a scollable field is an option within filament?

avatar
You can use Markdown
avatar

I faced one problem in this section, the new tag created in this section also attached with product by default and detach is seems working fine we can detach and it does not show not assigned tags to product. Any one please guide

avatar
You can use Markdown
avatar
You can use Markdown