Courses

Re-creating Booking.com API with Laravel and PHPUnit

Show Facilities in Apartment Details

Now, let's move to the detailed view of the specific Apartment which would show facilities grouped by category.

This will be a bit shorter lesson compared to others.


Goals of This Lesson

  • New endpoint to Show Apartment
  • Grouping the facility data
  • PHPUnit test for it

By the end of this lesson, we will have this in Postman:

Apartment show facilities Postman


New Endpoint: Show Apartment

Again, from the previous lesson, here's how the apartment detail facilities screen looks on mobile:

property show mobile facilities

For that, we create a new Controller.

php artisan make:controller Public/ApartmentController

Then we add it to the routes:

routes/api.php:

Route::get('search',
\App\Http\Controllers\Public\PropertySearchController::class);
Route::get('properties/{property}',
\App\Http\Controllers\Public\PropertyController::class);
Route::get('apartments/{apartment}',
\App\Http\Controllers\Public\ApartmentController::class);

But wait, we have the third public route and Controller, too much repeating code. Time to move those Controller namespaces into the use section. Did you know you can do something like this?

routes/api.php:

use App\Http\Controllers\Public;
 
Route::get('search', Public\PropertySearchController::class);
Route::get('properties/{property}', Public\PropertyController::class);
Route::get('apartments/{apartment}', Public\ApartmentController::class);

So, do not use a specific Controller, but the whole namespace instead. Cool, right?

Now, inside the Controller, we could simply do something like this, reusing the same resource as in search:

namespace App\Http\Controllers\Public;
 
use App\Http\Controllers\Controller;
use App\Http\Resources\ApartmentSearchResource;
use App\Models\Apartment;
 
class ApartmentController extends Controller
{
public function __invoke(Apartment $apartment)
{
$apartment->load('facilities.category');
 
return new ApartmentSearchResource($apartment);
}
}

But the problem is that we need a different structure to be returned, with facilities grouped into categories. So, we generate another API resource - for the same Apartment model, but for a different purpose.

php artisan make:resource ApartmentDetailsResource

app/Http/Resources/ApartmentDetailsResource.php:

namespace App\Http\Resources;
class ApartmentDetailsResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'name' => $this->name,
'type' => $this->apartment_type?->name,
'size' => $this->size,
'beds_list' => $this->beds_list,
'bathrooms' => $this->bathrooms,
'facility_categories' => $this->facility_categories,
];
}
}

See the facility_categories? How do we populate that? Here's one of the options, using mapWithKeys() on the Collection:

namespace App\Http\Controllers\Public;
 
use App\Http\Controllers\Controller;
use App\Http\Resources\ApartmentDetailsResource;
use App\Models\Apartment;
 
class ApartmentController extends Controller
{
public function __invoke(Apartment $apartment)
{
$apartment->load('facilities.category');
 
$apartment->setAttribute(
'facility_categories',
$apartment->facilities->groupBy('category.name')->mapWithKeys(fn ($items, $key) => [$key => $items->pluck('name')])
);
 
return new ApartmentDetailsResource($apartment);
}
}

As you can see, we're minimizing a load of facilities to load only their names, as we don't need the full data with IDs, timestamps, and pivot tables.

The visual result:

Apartment show facilities Postman


Automated Test

Now let's create the automated test to check if the facilities are shown correctly.

php artisan make:test ApartmentShowTest

tests/Feature/ApartmentShowTest.php:

namespace Tests\Feature;
 
use App\Models\Apartment;
use App\Models\City;
use App\Models\Facility;
use App\Models\FacilityCategory;
use App\Models\Property;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
 
class ApartmentShowTest extends TestCase
{
use RefreshDatabase;
 
public function test_apartment_show_loads_apartment_with_facilities()
{
$owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$cityId = City::value('id');
$property = Property::factory()->create([
'owner_id' => $owner->id,
'city_id' => $cityId,
]);
$apartment = Apartment::factory()->create([
'name' => 'Large apartment',
'property_id' => $property->id,
'capacity_adults' => 3,
'capacity_children' => 2,
]);
 
$firstCategory = FacilityCategory::create([
'name' => 'First category'
]);
$secondCategory = FacilityCategory::create([
'name' => 'Second category'
]);
$firstFacility = Facility::create([
'category_id' => $firstCategory->id,
'name' => 'First facility'
]);
$secondFacility = Facility::create([
'category_id' => $firstCategory->id,
'name' => 'Second facility'
]);
$thirdFacility = Facility::create([
'category_id' => $secondCategory->id,
'name' => 'Third facility'
]);
$apartment->facilities()->attach([
$firstFacility->id, $secondFacility->id, $thirdFacility->id
]);
 
$response = $this->getJson('/api/apartments/'.$apartment->id);
$response->assertStatus(200);
$response->assertJsonPath('name', $apartment->name);
$response->assertJsonCount(2, 'facility_categories');
 
$expectedFacilityArray = [
$firstCategory->name => [
$firstFacility->name,
$secondFacility->name
],
$secondCategory->name => [
$thirdFacility->name
]
];
$response->assertJsonFragment($expectedFacilityArray, 'facility_categories');
}
}

As you can see, we're building the expected array of specific values and then using assertJsonFragment() to check if the facility_categories return exactly that.

We're launching the test, and... green!

Apartment show facilities test

Previous: Facility Structure: Show Property Details
avatar

I think there is this not right syntax in ApartmentShowTest This line:

$response->assertJsonFragment($expectedFacilityArray, 'facility_categories');

This is right syntax one is:

$response->assertJsonFragment(['facility_categories' => $expectedFacilityArray]);

see more in Laravel documentation: https://laravel.com/docs/10.x/http-tests#assert-json-fragment

avatar

Hmmm, could it be that BOTH can work? Cause the test passed successfully.

avatar

Maybe, But the function assertJsonFragment accepts one argument (array $data)

avatar
You can use Markdown
avatar
Ngozi Stephen Onyemauche

Hi, i am getting this error when i run my test assertCount(): Argument #2 ($haystack) must be of type Countable|iterable, null given, called in C:\laragon\www\BookingApp\vendor\lar avel\framework\src\Illuminate\Testing\AssertableJsonString.php on line 74

$response = $this->getJson('/api/apartments/'.$apartment->id); 61▕ $response->assertStatus(200); 62▕ $response->assertJsonPath('name', $apartment->name); ➜ 63▕ $response->assertJsonCount(2, 'facility_categories'); 64▕ 65▕ $expectedFacilityArray = [ 66▕ $firstCategory->name => [ 67▕ $firstFacility->name,

	 Pls what should i do i don't understand this error message 
avatar

Probably your "facility_categories" relationship is returned empty or null, so it's impossible to compare to number 2.

avatar
Ngozi Stephen Onyemauche

okey i understand now

avatar

be sure to use the resource ApartmentDetailsResource in the controller

avatar
You can use Markdown
avatar
Ngozi Stephen Onyemauche
 $response = $this->getJson('/api/apartments/'.$apartment->id);
        $response->assertStatus(200);
        $response->assertJsonPath('name', $apartment->name);
        $response->assertJsonCount(2, 'facility_categories');
 
        $expectedFacilityArray = [
            $firstCategory->name => [
                $firstFacility->name,
                $secondFacility->name
            ],
            $secondCategory->name => [
                $thirdFacility->name
            ]
        ];

        $response->assertJsonFragment($expectedFacilityArray, 'facility_categories');
				```
				
avatar
You can use Markdown
avatar
You can use Markdown