In this lesson, we will manage apartments. In the table, we will automatically calculate and show the average rating. And this resource will have two relation managers for properties and rooms.
Apartments Table
First, we need a Resource for Apartment.
php artisan make:filament-resource ApartmentResource
Next, let's add a form and a table. For calculating the average rating, Filament has aggrregating relationships helpers.
app/Filament/Resources/ApartmentResource.php:
class ApartmentResource extends Resource{ protected static ?string $model = Apartment::class; protected static ?string $navigationIcon = 'heroicon-o-collection'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\Select::make('apartment_type_id') ->preload() ->required() ->searchable() ->relationship('apartment_type', 'name'), Forms\Components\Select::make('property_id') ->preload() ->required() ->searchable() ->relationship('property', 'name'), Forms\Components\TextInput::make('capacity_adults') ->integer() ->required() ->minValue(0), Forms\Components\TextInput::make('capacity_children') ->integer() ->required() ->minValue(0), Forms\Components\TextInput::make('size') ->integer() ->required() ->minValue(0), Forms\Components\TextInput::make('bathrooms') ->integer() ->required() ->minValue(0), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('apartment_type.name'), Tables\Columns\TextColumn::make('property.name'), Tables\Columns\TextColumn::make('size'), Tables\Columns\TextColumn::make('bookings_avg_rating') ->label('Rating') ->placeholder(0) ->avg('bookings', 'rating') ->formatStateUsing(fn (?string $state): ?string => number_format($state, 2)), ]) ->filters([ // ]) ->actions([ Tables\Actions\EditAction::make(), ]) ->bulkActions([ Tables\Actions\DeleteBulkAction::make(), ]); } // ...}
Showing Property for Apartments
Every apartment belongs to a property. So now, let's add a Relation Manager for the Properties.
php artisan make:filament-relation-manager ApartmentResource property name
app/Filament/Resources/ApartmentResource.php:
class ApartmentResource extends Resource{ // ... public static function getRelations(): array { return [ RelationManagers\PropertyRelationManager::class, ]; } // ...}
For the apartment, now it shows properties, but only with the name
field.
So, let's add more fields to the table and the form.
app/Filament/Resources/ApartmentResource/RelationManagers/PropertyRelationManager.php:
use App\Rules\LatitudeRule;use App\Rules\LongitudeRule; class PropertyRelationManager extends RelationManager{ protected static string $relationship = 'property'; protected static ?string $recordTitleAttribute = 'name'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('name') ->required(), Forms\Components\TextInput::make('address_street') ->required(), Forms\Components\TextInput::make('address_postcode') ->required(), Forms\Components\Select::make('city_id') ->relationship('city', 'name') ->preload() ->required() ->searchable(), Forms\Components\TextInput::make('lat') ->required() ->rules([new LatitudeRule()]), Forms\Components\TextInput::make('long') ->required() ->rules([new LongitudeRule()]), Forms\Components\Select::make('owner_id') ->relationship('owner', 'name') ->required() ->searchable(), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('owner.name'), Tables\Columns\TextColumn::make('address'), Tables\Columns\TextColumn::make('city.name'), ]) ->filters([ // ]) ->headerActions([ Tables\Actions\CreateAction::make(), ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) ->bulkActions([ Tables\Actions\DeleteBulkAction::make(), ]); }}
To get the property owner, we need to add this relation to the Property
model.
app/Models/Property.php:
class Property extends Model implements HasMedia{ // ... public function owner(): BelongsTo { return $this->belongsTo(User::class, 'owner_id'); }}
Now we have table with more information.
And form with more fields.
Image upload
Next, we need to add image upload for the properties. In the Re-creating Booking.com API with Laravel and PHPUnit course, we used spatie/laravel-medialibrary package to handle images.
Luckily, Filament has a plugin to upload files and use the same Media Library package. Let's install this plugin and use it.
composer require filament/spatie-laravel-media-library-plugin:"^2.0"
app/Filament/Resources/ApartmentResource/RelationManagers/PropertyRelationManager.php:
class PropertyRelationManager extends RelationManager{ protected static string $relationship = 'property'; protected static ?string $recordTitleAttribute = 'name'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('name') ->required(), Forms\Components\TextInput::make('address_street') ->required(), Forms\Components\TextInput::make('address_postcode') ->required(), Forms\Components\Select::make('city_id') ->relationship('city', 'name') ->preload() ->required() ->searchable(), Forms\Components\TextInput::make('lat') ->required() ->rules([new LatitudeRule()]), Forms\Components\TextInput::make('long') ->required() ->rules([new LongitudeRule()]), Forms\Components\Select::make('owner_id') ->relationship('owner', 'name') ->required() ->searchable(), Forms\Components\SpatieMediaLibraryFileUpload::make('photo') ->image() ->maxSize(5000) ->multiple() ->columnSpanFull() ->collection('avatars') ->conversion('thumbnail'), ]); } // ...}
That's how easily we added file upload.
Next, let's add rooms relation manager.
php artisan make:filament-relation-manager ApartmentResource rooms name
app/Filament/Resources/ApartmentResource.php:
class ApartmentResource extends Resource{ // ... public static function getRelations(): array { return [ RelationManagers\PropertyRelationManager::class, RelationManagers\RoomsRelationManager::class, ]; } // ...}
This adds a second tab in the relation manager.
Next, add additional field to form and table.
app/Filament/Resources/ApartmentResource/RelationManagers/RoomsRelationManager.php:
class RoomsRelationManager extends RelationManager{ protected static string $relationship = 'rooms'; protected static ?string $recordTitleAttribute = 'name'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\Select::make('room_type_id') ->preload() ->required() ->searchable() ->relationship('room_type', 'name'), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('room_type.name'), ]) ->filters([ // ]) ->headerActions([ Tables\Actions\CreateAction::make(), ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) ->bulkActions([ Tables\Actions\DeleteBulkAction::make(), ]); }}
Also, we can move relation managers to tabs.
app/Filament/Resources/ApartmentResource/Pages/EditApartment.php:
class EditApartment extends EditRecord{ protected static string $resource = ApartmentResource::class; protected function getActions(): array { return [ Actions\DeleteAction::make(), ]; } public function hasCombinedRelationManagerTabsWithForm(): bool { return true; } }
In app/Filament/Resources/ApartmentResource.php:
there is mistake one line => RelationManagers\PropertiesRelationManager::class,
should be => RelationManagers\PropertyRelationManager::class,
Thank you, dear Editor of our team :) Fixed now!
Hi Povilas. There is a little problem that we can't create a new Property if we haven't any Apartments now. And we can't add an Apartment without Property. I think the main entity here is a Property but not an Apartment.
I think it worked well for me: first create a property, then apartments inside of it. What error are you getting when creating a property?
The problem is I can't get a Property add form if I haven't any Apartments. This is 2 examples what I say: https://disk.yandex.ru/i/OwP2oG3Omb2DDQ https://disk.yandex.ru/i/6rsX_IvwRp88fw How to create a Property in this case? :)
Oh now I understood the problem. according to the logic of the application, admins don't create apartments and properties, users do that via api, admins only edit them via filament adminpanel.
It would not be logical if admins added properties for someone.
Yes, that's right, agree with you. Thanx
Hi Povilas, Nice course.
Just wanted to say that it's better to remove the "preload()" for Property dropdown from ApartmentResource.php (Edit Apartment) => Forms\Components\Select::make('property_id')
I used the Performance Seeds from related course and I have thousands of properties (99k) and didn't know why the Edit Apartments page was loading in 30 seconds. I noticed the preload(), it loads all 99k properties on that dropdown, so it's better to remove it. :)
Thank you for all courses and especially for 'Booking' ones!
Hi, thanks for the kind words and a suggestion. We will take a look if that makes sense as an edit here or maybe a separate article as a warning
Sadly
hasCombinedRelationManagerTabsWithForm
is no longer an option in Filament v3.PropertyRelationManager seems a bit confusing because in our system, an Apartment belongs to a Property rather than having a belongs to many relationship. I would directly include the action for creating a Property within the select.