Did you find the answer?
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
@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
Please or to participate in this conversation.