Now, let's try to show the property and apartment information in the search results, similarly to how it's done on the Booking.com website:
As you can see, every apartment may have its type, size in square meters, number of small/large/sofa beds, and number of bedrooms, living rooms, and bathrooms. So let's return all of that.
By the end of this lesson, we will have this DB structure:
Let's add two fields to the apartments: their type and size (in square meters).
Types should have a separate DB table, with a relationship, so we do exactly that.
php artisan make:model ApartmentType -m
Migration file:
use App\Models\ApartmentType; public function up(): void{ Schema::create('apartment_types', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); ApartmentType::create(['name' => 'Entire apartment']); ApartmentType::create(['name' => 'Entire studio']); ApartmentType::create(['name' => 'Private suite']);}
For such simple seeds, I often prefer doing them right in the migration file, instead of creating a separate Seeder. But that's a personal preference.
The Model is very simple.
app/Models/ApartmentType.php:
class ApartmentType extends Model{ use HasFactory; protected $fillable = ['name'];}
Next, we create a migration for adding both type and size.
php artisan make:migration add_apartment_type_size_to_apartments_table
Migration file:
public function up(): void{ Schema::table('apartments', function (Blueprint $table) { $table->foreignId('apartment_type_id') ->nullable() ->after('id') ->constrained(); $table->unsignedInteger('size')->nullable(); });}
Apartment type should be nullable
, as I've noticed on the page that not all apartments show their type.
Next, we add those fields to the fillables in the Model and create a relationship to the type.
app/Models/Apartment.php:
class Apartment extends Model{ protected $fillable = [ 'property_id', 'apartment_type_id', 'name', 'capacity_adults', 'capacity_children', 'size', ]; public function apartment_type() { return $this->belongsTo(ApartmentType::class); }}
Finally, we need to modify our search to return that type as a relationship.
app/Http/Controllers/Public/PropertySearchController.php:
class PropertySearchController extends Controller{ public function __invoke(Request $request) { return Property::with('city', 'apartments.apartment_type') // ... when() conditions ->get(); }}
These are new fields, visible in the search results now:
Our DB structure will get a bit more complicated, with rooms within apartments:
In reality, travelers are filtering for the number of spots to sleep: a large bed can usually fit 2 people, king size bed may fit even more.
So this is exactly what the property owner should specify. I found these form screenshots online:
There are actually two types of rooms: with and without beds. So I think it's safe to assume that for no-beds rooms we're interested only in their amount.
So we can add bathrooms
as a simple field in the apartments
table.
Migration file:
Schema::table('apartments', function (Blueprint $table) { $table->unsignedInteger('bathrooms')->default(0);});
Adding it to fillables in the Model.
app/Models/Apartment.php:
class Apartment extends Model{ protected $fillable = [ 'property_id', 'apartment_type_id', 'name', 'capacity_adults', 'capacity_children', 'size', 'bathrooms', ];
So this was a simple part. Now, in other rooms with beds, that structure will be more complicated.
I suggest this logic:
belongsTo(Apartment::class)
and belongsTo(RoomType::class)
relationshipsbelongsTo(Room::class)
and belongsTo(BedType::class)
relationshipsSo, step by step.
php artisan make:model RoomType -m
Migration file:
use App\Models\RoomType; return new class extends Migration{ public function up(): void { Schema::create('room_types', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); RoomType::create(['name' => 'Bedroom']); RoomType::create(['name' => 'Living room']); }};
Again, a few records are seeded inside the migration file itself, no need for a separate seeder.
Next, the fillable field in the Model.
app/Models/RoomType.php:
class RoomType extends Model{ use HasFactory; protected $fillable = ['name'];}
Next, the Room model.
php artisan make:model Room -m
Migration file:
public function up(): void{ Schema::create('rooms', function (Blueprint $table) { $table->id(); $table->foreignId('apartment_id')->constrained(); $table->foreignId('room_type_id')->nullable()->constrained(); $table->string('name'); $table->timestamps(); });}
app/Models/Room.php:
class Room extends Model{ use HasFactory; protected $fillable = ['apartment_id', 'room_type_id', 'name']; public function room_type() { return $this->belongsTo(RoomType::class); }}
Let's also create a hasMany
relationship with Apartments.
app/Models/Apartment.php:
class Apartment extends Model{ // ... public function rooms() { return $this->hasMany(Room::class); }}
Next, Bed Types.
php artisan make:model BedType -m
Migration file:
use App\Models\BedType; return new class extends Migration{ public function up(): void { Schema::create('bed_types', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); BedType::create(['name' => 'Single bed']); BedType::create(['name' => 'Large double bed']); BedType::create(['name' => 'Extra large double bed']); BedType::create(['name' => 'Sofa bed']); }};
app/Models/BedType.php:
class BedType extends Model{ use HasFactory; protected $fillable = ['name'];}
Finally, the Beds model. I've been thinking whether a bed should have a name, but let it exists but be nullable :)
php artisan make:model Bed -m
Migration file:
public function up(): void{ Schema::create('beds', function (Blueprint $table) { $table->id(); $table->foreignId('room_id')->constrained(); $table->foreignId('bed_type_id')->constrained(); $table->string('name')->nullable(); $table->timestamps(); });}
app/Models/Bed.php:
class Bed extends Model{ use HasFactory; protected $fillable = ['room_id', 'bed_type_id', 'name']; public function room() { return $this->belongsTo(Room::class); } public function bed_type() { return $this->belongsTo(BedType::class); }}
Also, let's create a hasMany
relationship from Rooms to Beds:
app/Models/Room.php:
class Room extends Model{ // ... public function beds() { return $this->hasMany(Bed::class); }}
Great, so we have this DB structure, visually:
Now, remember, our goal is to return the search results with rooms and beds as a summary:
Currently, in we have this:
Property::with('city', 'apartments.apartment_type')->when(...)->get();
So we need to expand that with()
part with rooms and beds.
app/Http/Controllers/Public/PropertySearchController.php:
class PropertySearchController extends Controller{ public function __invoke(Request $request) { return Property::query() ->with([ 'city', 'apartments.apartment_type', 'apartments.rooms.beds.bed_type' ]) ->when(...) ->when(...) ->get();
I've changed with()
to accept an array and formatted the sentence with query()
to be on a separate line.
The result is a pretty huge JSON. Postman screenshot wouldn't fit, so posting the actual result.
Endpoint: /api/search?city_id=1&adults=2&children=1
Result JSON:
[ { "id": 2, "owner_id": 2, "name": "Central Hotel", "city_id": 2, "address_street": "16-18, Argyle Street, Camden", "address_postcode": "WC1H 8EG", "lat": "51.5291450", "long": "-0.1239401", "created_at": "2023-02-13T13:21:04.000000Z", "updated_at": "2023-02-13T13:21:04.000000Z", "city": { "id": 2, "country_id": 2, "name": "London", "lat": "51.5073510", "long": "-0.1277580", "created_at": "2023-02-13T09:04:51.000000Z", "updated_at": "2023-02-13T09:04:51.000000Z" }, "apartments": [ { "id": 2, "apartment_type_id": 1, "property_id": 2, "name": "Large Apartment", "capacity_adults": 3, "capacity_children": 2, "created_at": "2023-02-27T11:33:41.000000Z", "updated_at": "2023-02-27T11:33:41.000000Z", "size": 50, "bathrooms": 0, "apartment_type": { "id": 1, "name": "Entire apartment", "created_at": "2023-02-28T09:02:54.000000Z", "updated_at": "2023-02-28T09:02:54.000000Z" }, "rooms": [ { "id": 1, "apartment_id": 2, "room_type_id": 1, "name": "Bedroom", "created_at": "2023-03-02T10:07:05.000000Z", "updated_at": "2023-03-02T10:07:05.000000Z", "beds": [ { "id": 1, "room_id": 1, "bed_type_id": 1, "name": null, "created_at": "2023-03-02T10:08:22.000000Z", "updated_at": "2023-03-02T10:08:22.000000Z", "bed_type": { "id": 1, "name": "Single bed", "created_at": "2023-03-02T07:37:43.000000Z", "updated_at": "2023-03-02T07:37:43.000000Z" } }, { "id": 2, "room_id": 1, "bed_type_id": 2, "name": null, "created_at": "2023-03-02T10:08:22.000000Z", "updated_at": "2023-03-02T10:08:22.000000Z", "bed_type": { "id": 2, "name": "Large double bed", "created_at": "2023-03-02T07:37:43.000000Z", "updated_at": "2023-03-02T07:37:43.000000Z" } } ] }, { "id": 2, "apartment_id": 2, "room_type_id": 2, "name": "Living Room", "created_at": "2023-03-02T10:07:05.000000Z", "updated_at": "2023-03-02T10:07:05.000000Z", "beds": [] } ] } ] }]
Great, so we're delivering the data from the API, but that JSON size becomes a problem. In the next lesson, let's spend time optimizing it and loading what the front-end actually needs.