I have an event management system. Each event has speakers or sponsors. Here, I implement nested resources for Speakers.
When I try to delete a speaker record via the speakers page, I get an error like below.
Filament\Resources\Pages\ListRecords::Filament\Resources\Pages{closure}(): Return value must be of type string, null returned
However, when I delete the record via the edit page, it works without any error.
Here are my codes
EventResource.php
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Event;
use App\Models\Speaker;
use App\Models\Sponsor;
use Filament\Forms\Get;
use App\Models\Currency;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Pages\CreateEventSponsor;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Filament\Tables\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Wizard;
use Illuminate\Support\Facades\Route;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Textarea;
use Filament\Tables\Columns\TextColumn;
use Illuminate\Database\Eloquent\Model;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Actions\ActionGroup;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TimePicker;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Contracts\Support\Htmlable;
use Guava\FilamentNestedResources\Ancestor;
use App\Filament\Resources\EventResource\Pages;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use App\Filament\Resources\EventResource\RelationManagers;
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
use Guava\FilamentNestedResources\Concerns\NestedResource;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use App\Filament\Resources\SpeakerResource\Pages\EditSpeaker;
use App\Filament\Resources\SponsorResource\Pages\EditSponsor;
use App\Filament\Resources\SpeakerResource\Pages\ListSpeakers;
use App\Filament\Resources\SponsorResource\Pages\ListSponsors;
use App\Filament\Resources\SpeakerResource\Pages\CreateSpeaker;
use App\Filament\Resources\SponsorResource\Pages\CreateSponsor;
use App\Filament\Resources\RegistrantResource\Pages\EditRegistrant;
use App\Filament\Resources\RegistrantResource\Pages\ListRegistrants;
use App\Filament\Resources\EventResource\RelationManagers\SponsorsRelationManager;
class EventResource extends Resource
{
protected static ?string $model = Event::class;
protected static ?string $navigationIcon = 'heroicon-o-calendar-days';
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->name;
}
public static function form(Form $form): Form
{
return $form
// ->columns(4)
->schema([
Wizard::make([
Wizard\Step::make('Detail')
->columns(4)
->schema([
SpatieMediaLibraryFileUpload::make('cover')
->required()
->columnSpanFull()
->collection('events')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->columnSpan(2),
Forms\Components\DatePicker::make('start_date')
->required()
->native(false)
->columnSpan(1),
Forms\Components\DatePicker::make('end_date')
->native(false)
->columnSpan(1),
Forms\Components\Select::make('category_id')
->relationship('category', 'name')
->columnSpan(1)
->required()
->createOptionForm([
Forms\Components\TextInput::make('name')
->required(),
]),
Forms\Components\TextInput::make('location')
->maxLength(255)
->columnSpan(2),
Forms\Components\TextInput::make('attendance')
->integer()
->columnSpan(1),
Forms\Components\RichEditor::make('toc')
->label('Terms & Conditions')
->columnSpanFull(),
Forms\Components\Select::make('tags')
->multiple()
->relationship(titleAttribute: 'name')
->createOptionForm([
Forms\Components\TextInput::make('name')
->required(),
]),
Forms\Components\Select::make('is_featured')
->options([
0 => 'No',
1 => 'Yes',
])
->native(false)
->live()
->columnSpan(1),
Forms\Components\TextInput::make('button_text')
->maxLength(255)
->hidden(fn (Get $get): bool => ! $get('is_featured'))
->columnSpan(1),
Forms\Components\TextInput::make('button_link')
->maxLength(255)
->hidden(fn (Get $get): bool => ! $get('is_featured'))
->columnSpan(1),
Select::make('is_registration_active')
->options([
0 => 'No',
1 => 'Yes',
])
->label('Registration')
]),
Wizard\Step::make('Key Topics')
->schema([
Repeater::make('keyTopics')
->relationship()
->schema([
SpatieMediaLibraryFileUpload::make('icon')
->columnSpanFull()
->collection('key-topics')
// ->responsiveImages()
// ->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...')
->helperText('Download icons at svgrepo.com.'),
TextInput::make('title')
->required()
->maxLength(255)
->columnSpanFull(),
RichEditor::make('description')
->required()
->columnSpanFull(),
])
->columns(2)
->columnSpanFull(),
]),
Wizard\Step::make('Agenda')
->schema([
Repeater::make('agenda')
->relationship()
->schema([
TextInput::make('title')
->required()
->maxLength(255)
->columnSpanFull(),
Repeater::make('agendaItems')
->relationship()
->schema([
Forms\Components\TimePicker::make('start_time')
->native(false)
->seconds(false)
->required()
->columnSpan(1),
Forms\Components\TimePicker::make('end_time')
->native(false)
->seconds(false)
->required()
->columnSpan(1),
TextInput::make('title')
->required()
->maxLength(255)
->columnSpan(2),
Textarea::make('description')
->columnSpanFull(),
])
->columns(4)
])
]),
Wizard\Step::make('About')
->schema([
SpatieMediaLibraryFileUpload::make('picture1')
->required()
->label('Picture 1')
->columnSpanFull()
->collection('about-event-1')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
SpatieMediaLibraryFileUpload::make('picture2')
->required()
->label('Picture 2')
->columnSpan(2)
->collection('about-event-2')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
SpatieMediaLibraryFileUpload::make('picture3')
->required()
->label('Picture 3')
->columnSpan(2)
->collection('about-event-3')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
SpatieMediaLibraryFileUpload::make('picture4')
->required()
->label('Picture 4')
->columnSpanFull()
->collection('about-event-4')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
TextInput::make('tagline')
->required()
->maxLength(255)
->columnSpanFull(),
RichEditor::make('description')
->label('About')
->columnSpanFull(),
Repeater::make('highlights')
->relationship()
->schema([
SpatieMediaLibraryFileUpload::make('icon')
->columnSpanFull()
->collection('highlights')
// ->responsiveImages()
// ->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...')
->helperText('Download icons at svgrepo.com.'),
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('description')
->label('Short Description')
->required()
->maxLength(255),
])->columnSpanFull(),
])->columns(4),
Wizard\Step::make('Audiences')
->schema([
Repeater::make('targetAudiences')
->relationship()
->schema([
TextInput::make('name')
->required()
->maxLength(255)
->columnSpanFull(),
])
->columns(4)
]),
Wizard\Step::make('Tickets')
->schema([
Repeater::make('tickets')
->relationship()
->schema([
TextInput::make('name')
->required()
->maxLength(255)
->columnSpan(1),
Select::make('type')
->options([
'complimentary' => 'Complimentary',
'paid' => 'Paid',
])
->native(false)
->columnSpan(1),
Select::make('currency_id')
->label('Currency')
->options(Currency::all()->pluck('code', 'id'))
->searchable(),
TextInput::make('price')
->required()
->columnSpan(1),
Repeater::make('benefits')
->relationship()
->schema([
TextInput::make('benefit')
->required()
->maxLength(255)
->columnSpanFull(),
])->columnSpanFull(),
Textarea::make('note')
->columnSpanFull(),
])
->columns(4)
]),
])->skippable()->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('category.name'),
ToggleColumn::make('is_active')
->label('Status')
->beforeStateUpdated(function ($record, $state) {
if ($record->is_active === $state) {
return $state;
}
return $state;
})
->afterStateUpdated(function ($record, $state) {
if ($record->is_active === $state) {
return;
}
$record->save();
})
->sortable(),
])
->filters([
SelectFilter::make('status')
->options([
'1' => 'Active',
'0' => 'Inactive',
])
->attribute('is_active')
])
->actions([
ActionGroup::make([
// Tables\Actions\ViewAction::make(),
Action::make('Speakers')
->icon('heroicon-m-user-group')
->url(
fn (Event $record): string => static::getUrl('speakers.index', [
'parent' => $record->id,
])
),
Action::make('Registrants')
->icon('heroicon-m-users')
->url(
fn (Event $record): string => static::getUrl('registrants.index', [
'parent' => $record->id,
])
),
Action::make('Sponsors')
->icon('heroicon-m-photo')
->url(
fn (Event $record): string => static::getUrl('sponsors.index', [
'parent' => $record->id,
])
),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->button()
->label('Actions'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListEvents::route('/'),
'create' => Pages\CreateEvent::route('/create'),
'view' => Pages\ViewEvent::route('/{record}'),
'edit' => Pages\EditEvent::route('/{record}/edit'),
'speakers.index' => ListSpeakers::route('/{parent}/speakers'),
'speakers.create' => CreateSpeaker::route('/{parent}/speakers/create'),
'speakers.edit' => EditSpeaker::route('/{parent}/speakers/{record}/edit'),
'sponsors.index' => ListSponsors::route('/{parent}/sponsors'),
'sponsors.create' => CreateSponsor::route('/{parent}/sponsors/create'),
'sponsors.edit' => EditSponsor::route('/{parent}/sponsors/{record}/edit'),
'registrants.index' => ListRegistrants::route('/{parent}/registrants'),
'registrants.edit' => EditRegistrant::route('/{parent}/registrants/{record}/edit'),
];
}
}
SpeakerResource.php
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Speaker;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Contracts\Support\Htmlable;
use App\Filament\Resources\SpeakerResource\Pages;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use App\Filament\Resources\SpeakerResource\RelationManagers;
class SpeakerResource extends Resource
{
public static string $parentResource = EventResource::class;
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->title;
}
protected static ?string $model = Speaker::class;
protected static bool $shouldRegisterNavigation = false;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
SpatieMediaLibraryFileUpload::make('photo')
->required()
->columnSpanFull()
->collection('speakers')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('occupation')
->required()
->maxLength(255),
Forms\Components\Toggle::make('is_active')
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->reorderable('sort_order')
->columns([
SpatieMediaLibraryImageColumn::make('photo')
->collection('speakers')
->conversion('thumb')
->width(150)
->height(150)
->label('Photo'),
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('occupation')
->searchable(),
ToggleColumn::make('is_active')
->label('Status')
->beforeStateUpdated(function ($record, $state) {
if ($record->is_active === $state) {
return $state;
}
return $state;
})
->afterStateUpdated(function ($record, $state) {
if ($record->is_active === $state) {
return;
}
$record->save();
})
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
SelectFilter::make('status')
->options([
'1' => 'Active',
'0' => 'Inactive',
])
->attribute('is_active')
])
->defaultSort('sort_order', 'asc')
->actions([
Tables\Actions\EditAction::make()
->url(
fn (Pages\ListSpeakers $livewire, Model $record): string => static::$parentResource::getUrl('speakers.edit', [
'record' => $record,
'parent' => $livewire->parent,
])
),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
}
**HasParentResource.php
<?php
namespace App\Filament\Traits;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
trait HasParentResource
{
public Model|int|string|null $parent = null;
public function bootHasParentResource(): void
{
if ($parent = (request()->route('parent') ?? request()->input('parent'))) {
$parentResource = $this->getParentResource();
$this->parent = $parentResource::resolveRecordRouteBinding($parent);
if (!$this->parent) {
throw new ModelNotFoundException();
}
}
}
public static function getParentResource(): string
{
$parentResource = static::getResource()::$parentResource;
if (!isset($parentResource)) {
throw new Exception('Parent resource is not set for '.static::class);
}
return $parentResource;
}
protected function applyFiltersToTableQuery(Builder $query): Builder
{
$query = parent::applyFiltersToTableQuery($query);
return $query->where($this->getParentRelationshipKey(), $this->parent->getKey());
}
public function getParentRelationshipKey(): string
{
return $this->relationshipKey ?? $this->parent?->getForeignKey();
}
public function getChildPageNamePrefix(): string
{
return $this->pageNamePrefix ?? (string) str(static::getResource()::getSlug())
->replace('/', '.')
->afterLast('.');
}
public function getBreadcrumbs(): array
{
$resource = static::getResource();
$parentResource = static::getParentResource();
$breadcrumbs = [
$parentResource::getUrl() => $parentResource::getBreadCrumb(),
$parentResource::getRecordTitle($this->parent),
$parentResource::getUrl(name: $this->getChildPageNamePrefix() . '.index', parameters: ['parent' => $this->parent]) => $resource::getBreadCrumb(),
];
if (isset($this->record)) {
$breadcrumbs[] = $resource::getRecordTitle($this->record);
}
$breadcrumbs[] = $this->getBreadCrumb();
return $breadcrumbs;
}
}
How do I make the delete speaker function work?
Solved, by changing Tables\Actions\DeleteAction::make()
to
Action::make('delete')
->requiresConfirmation()
->color('danger')
->icon('heroicon-m-trash')
->action(fn (Sponsor $record) => $record->delete())