Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Pawooo's avatar

Filament V3 how to save different M:M relationships using Custom Page single edit/view form?

Unimportant background:

I was able to do it with default Filament Resource before, however in my case I don't need a /table view (multiple entries with delete functionality), so I have to come up with a way to do it using a Custom page. Been stuck for a while at this now.

Did 2 simple blogs in Laravel. My second Filament project, first V3 project (trying to learn it in my free time). I ask very stupid questions from time to time, bear with me 🙇‍♂️

I want:

To save a bunch of IDs for cities and prefectures as M:M relationship (company_prefectures and company_cities) using Filament Custom Page (by Custom Page I mean single page edit/view, no table view like this one)

Solution 1?

I can save it as 2 separate multiselect JSONs with tenant ID attached in a migration as below:

Schema::create('company_regions', function (Blueprint $table) {
            $table->id();
            $table->foreignIdFor(Company::class, 'company_id')->cascadeOnDelete();
            $table->json('prefectures');
            $table->json('cities');
            $table->timestamps();
        });

But I hear JSONs are extremely heavy, especially if you need to be able to quickly search through these IDs.

Solution 2?

Save it as M:M, but I've no idea how to do it using a single custom page (Making 2 separate pages for 2-4 multiselect inputs seems like an overkill to me and is not a good UX). If it's possible, I understand I'll need 2 separate M:M tables (company_prefecture and company_city?) But if I try to pull something like that off, ->relationship returns null in my case.

Filament Relationship Documentation

I have:

My custom page (saving IDs as JSONs)

class CompanyRegions extends Page implements HasForms
{
    public function mount(): void {
        $tenant = Filament::getTenant();
        $companyID = $tenant->id;
        $companyRegions = auth()->user()->companies->find($companyID)->regions;
        
        if($companyRegions === null) {
            $this->form->fill();
        } else {
            $this->form->fill(auth()->user()->companies->find($companyID)->regions->attributesToArray());
        }
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Select::make('prefectures')
                    ->placeholder('Select prefectures you work in')
                    ->label('Prefectures')
                    ->options(Prefecture::all()->pluck('prefecture_ja', 'id'))
                    ->multiple()
                    ->searchable()
                    ->required(),
                Select::make('cities')
                    ->placeholder('Select cities you work in')
                    ->label('Cities')
                    ->options(City::all()->pluck('city_ja', 'id'))
                    ->multiple()
                    ->searchable()
                    ->required(),
            ])
            ->statePath('data');
    }
  protected function getFormActions(): array {
        return [
            Action::make('save')
                ->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
                ->submit('Save'),
        ];
    }

    public function save(): void {
        $tenant = Filament::getTenant();
        $data = $this->form->getState();
        $data['company_id'] = $tenant->id;
        $companyID = $tenant->id;

        $company = auth()->user()->companies->find($companyID);   
        $Entry = $company->regions();

        if ($company->regions === null) {
            $Entry = new ModelsCompanyRegions;
            $Entry->fill($data);
            $Entry->save($data);
            
            Notification::make()
                ->success()
                ->title('Entry Created')
                ->send();
            } else {
                $Entry->update($data);
                Notification::make()
                ->success()
                ->title('Entry Updated)
                ->send();
           }
        }
}

Models/City.php

    public function prefecture(): BelongsTo
    {
        return $this->belongsTo(Prefecture::class);
    }

Models/CompanyRegions.php

    public function regions(): BelongsToMany
    {
        return $this->belongsToMany(Company::class);
    }
    public function cities(): BelongsToMany
    {
        return $this->belongsToMany(City::class);
    }
    public function prefectures(): BelongsToMany
    {
        return $this->belongsToMany(Prefecture::class);
    }

Models/Prefecture.php

    public function cities(): HasMany
    {
        return $this->hasMany(City::class);
    }

migrations/create_company_regions_table.php

    public function up(): void
    {
        Schema::create('company_regions', function (Blueprint $table) {
            $table->id();
            $table->foreignIdFor(Company::class, 'company_id')->cascadeOnDelete();
            // $table->foreignIdFor(City::class, 'city_id')->nullable();
            // $table->foreignIdFor(Prefecture::class, 'prefecture_id')->nullable();
            $table->json('prefectures');
            $table->json('cities');
            $table->timestamps();
        });
        // Schema::create('company_cities', function (Blueprint $table) {
        //     $table->id();
        //     $table->foreignIdFor(Company::class, 'company_id');
        //     $table->foreignIdFor(City::class, 'city_ids');
        //     $table->timestamps();
        // });
        // Schema::create('company_prefectures', function (Blueprint $table) {
        //     $table->id();
        //     $table->foreignIdFor(Company::class, 'company_id');
        //     $table->foreignIdFor(Prefecture::class, 'prefecture_ids');
        //     $table->timestamps();
        // });
    }

migrations/create_cities_table.php

        Schema::create('cities', function (Blueprint $table) {
            $table->id();
            $table->foreignIdFor(Prefecture::class, 'prefecture_id')->index();
            $table->string('city_en');
            $table->string('city_ja');
            $table->string('special_district_ja')->nullable();
            $table->timestamps();
        });

migrations/create_prefectures_table.php

        Schema::create('prefectures', function (Blueprint $table) {
            $table->id();
            $table->string('prefecture_en')->unique();
            $table->string('prefecture_ja')->unique();
            $table->timestamps();
        });

Any advice would be much appreciated

0 likes
3 replies
Pawooo's avatar
Pawooo
OP
Best Answer
Level 5

@Sabawoon-Yaqubi

Hope this helps (one form): https://laracasts.com/discuss/channels/laravel/many-to-many-sync-method-creates-duplicates-is-this-really-the-right-way-to-do-it

The way I did it is like this (honestly feel like there should be a better way to do this):

company-regions.blade.php

<x-filament-panels::page>
    <x-filament-panels::form wire:submit="savePrefectures">
        {{ $this->prefectures }}
        
        <x-filament-panels::form.actions
        :actions="$this->getFormActions('prefectures')"
        />
    </x-filament-panels::form>
    <x-filament-panels::form wire:submit="saveCities">
        {{ $this->cities }}
        
        <x-filament-panels::form.actions
        :actions="$this->getFormActions('cities')"
        />
    </x-filament-panels::form>
</x-filament-panels::page>

Filament Custom Page

class CompanyRegions extends Page implements HasForms
{
    use InteractsWithForms;

    public ?array $prefectureData = [];
    public ?array $cityData = [];

    protected function getForms(): array
    {
        return [
            'cities',
            'prefectures',
        ];
    }

	    public function mount(): void {
        $tenant = Filament::getTenant();
        $companyID = $tenant->id;

        $companyPrefectures = auth()->user()->companies->find($companyID)->prefectures;        
        $existingPrefectures = $companyPrefectures->pluck('id')->toArray();

        $this->prefectures->fill([
            'prefecture_id' => $existingPrefectures,
        ]);

        $companyCities = auth()->user()->companies->find($companyID)->cities;
        $existingCities = $companyCities->pluck('id')->toArray();

        $this->cities->fill([
            'city_id' => $existingCities,
        ]);


    public function prefectures(Form $form): Form
    {
        return $form
            ->schema([
                Select::make('prefecture_id')
                    ->placeholder('Select Prefectures')
                    ->label('Prefecture')
                    ->options(Prefecture::all()->pluck('prefecture_ja', 'id'))
                    ->multiple()
                    ->searchable()
                    ->required(),
            ])
            ->statePath('prefectureData');
    }

    public function cities(Form $form): Form {
        return $form
        ->schema([
                Select::make('city_id')
                    ->placeholder('Select Cities')
                    ->label('Cities')
                    ->options(City::all()->pluck('city_ja', 'id'))
                    ->multiple()
                    ->searchable()
                    ->required(),
        ])
        ->statePath('cityData');
    }

    }

    protected function getFormActions($formName): array {
        if($formName === 'prefectures') {
            return [
                Action::make('savePrefectures')
                ->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
                ->submit('Save'),
            ];
        }
        else if($formName === 'cities') {
            return [
                Action::make('saveCities')
                    ->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
                    ->submit('Save'),
            ];
        }
    }

   public function savePrefectures(): void {
        $tenant = Filament::getTenant();
        $data = $this->prefectures->getState();

        foreach ($data['prefecture_id'] as $prefecture_id) {
            $mergedData[] = [
                'prefecture_id' => $prefecture_id,
            ];
        }
        $data = $mergedData;

        $companyID = $tenant->id;

        $company = auth()->user()->companies->find($companyID);   

        $company->prefectures()->sync($data);
}

       public function saveCities(): void {
            $tenant = Filament::getTenant();
            $data = $this->cities->getState();

            foreach ($data['city_id'] as $city_id) {
                $mergedData[] = [
                    'city_id' => $city_id,
                ];

			$data = $mergedData;
    
            $companyID = $tenant->id;
    
            $company = auth()->user()->companies->find($companyID);   
    
            $company->cities()->sync($data);
            }

Trust me when I say there should be a better way to do this – but that's how I did it

1 like

Please or to participate in this conversation.