Laravel has API Resources but, loaded with relationships, they may cause performance issues. There are conditional methods to help you avoid it.
These methods allow you to show/hide attributes/relationships based on certain conditions. For example:
- If a relationship is not loaded, don't return it
- If a field is null, don't return it
- If a model is missing an attribute, don't return specific data
- And so on...
Let's look at these methods.
When Loaded
Of course, there is a dedicated method to only return a relationship if it's loaded (to avoid N+1 issues):
Controller
$apartments = Apartment::query() ->with(['amenities']) // This has to have the relationship ->paginate(10);
Then, when using it with the resource:
Resource
'amenities' => AmenityResource::collection($this->whenLoaded('amenities')),
Under the hood, it calls relationLoaded()
on the Model to see if it was eager loaded.
Vendor
protected function whenLoaded($relationship, $value = null, $default = null){ if (func_num_args() < 3) { $default = new MissingValue; } if (! $this->resource->relationLoaded($relationship)) { return value($default); } $loadedValue = $this->resource->{$relationship}; if (func_num_args() === 1) { return $loadedValue; } if ($loadedValue === null) { return; } if ($value === null) { $value = value(...); } return value($value, $loadedValue);}
This can prevent you from accidentally having N+1 issues in your API, but that's not all. You can also Hide/Show relationships based on the Model's state.
- No eager loading? No relationship in the response!
- Eager loaded? The relationship is in the response!
A great way to add some control over your API responses. Especially if combined with Query parameters.
Check if the Model has an Attribute
Some attributes depend on other attributes. For example, if you have a price
attribute, you can calculate the tax
attribute based on it:
Resource
'price' => $this->price,'tax' => $this->whenHas('price', fn() => number_format($this->price * 0.21, 2)),
This will only return the tax
attribute if the price
attribute exists on the Model. It will not return an attribute that has not been loaded from the database.
Code under the hood:
Vendor
public function whenHas($attribute, $value = null, $default = null){ if (func_num_args() < 3) { $default = new MissingValue; } if (! array_key_exists($attribute, $this->resource->getAttributes())) { return value($default); } return func_num_args() === 1 ? $this->resource->{$attribute} : value($value, $this->resource->{$attribute});}
It's a great way to prevent errors in your API if some attributes are missing. For example, if this API query did not return the price
field, it would not return the tax
field either.
Check if the Attribute is Appended
A lot of API endpoints can contain custom attributes. These attributes usually come as Appends
in Models. Using the whenAppended()
method, you can conditionally return the attribute:
Model
protected function addressLines(): Attribute{ return Attribute::make( get: fn($value) => explode(',', $this->address), );}
You can use the whenAppended()
method to conditionally return the attribute:
Resource
'address' => $this->address,'address_lines' => $this->whenAppended('address_lines', $this->address_lines),
This will not return the address_lines
attribute if we don't have the address_lines
appended to the Model:
Controller
public function index(): ApartmentCollection{ $apartments = Apartment::query() ->with(['amenities']) ->withExists('amenities') ->paginate(10); $apartments->append('address_lines'); return new ApartmentCollection($apartments);}
Under the hood:
Vendor
protected function whenAppended($attribute, $value = null, $default = null){ if ($this->resource->hasAppended($attribute)) { return func_num_args() >= 2 ? value($value) : $this->resource->$attribute; } return func_num_args() === 3 ? value($default) : new MissingValue;}
Once again, we can easily control when to return the attribute based on the Model's state. If the attribute is not appended, it will not return the attribute.
Return Attribute if Counted
Some API endpoints should only return data if it's counted. For example, if you have a relationship that is counted, you can use the whenCounted()
method:
Resource
'amenities_count' => $this->amenities_count,'amenities' => $this->whenCounted('amenities', AmenityResource::collection($this->amenities)),
This will not return the relationship if we don't have the count of the relationship:
Controller
$apartments = Apartment::query() ->with(['amenities']) ->withCount(['amenities']) ->paginate(10);
This is useful when the relationship needs to be returned only if it's counted. If the count is not loaded, it will not return the attribute.
Under the hood:
Vendor
public function whenCounted($relationship, $value = null, $default = null){ if (func_num_args() < 3) { $default = new MissingValue; } $attribute = (string) Str::of($relationship)->snake()->finish('_count'); if (! array_key_exists($attribute, $this->resource->getAttributes())) { return value($default); } if (func_num_args() === 1) { return $this->resource->{$attribute}; } if ($this->resource->{$attribute} === null) { return; } if ($value === null) { $value = value(...); } return value($value, $this->resource->{$attribute});}
This way, you can easily control when to return to the relationship. Especially if a count is involved - you instantly inform the API consumer if the relationship is empty.
Only When Exists and Loaded - New in Laravel v11.20
Some relationships can only be returned if they exist and are loaded. For that, we use the whenExistsLoaded
method:
'amenities' => $this->whenExistsLoaded('amenities', AmenityResource::collection($this->amenities)),
This will only return the attribute if both of the following conditions are met:
$apartments = Apartment::query() ->with(['amenities']) // We need to load the relationship ->withExists('amenities')// And make sure it exists ->paginate(10);
Otherwise, it does not load anything and does not return the attribute.
Under the hood, there is a simple check:
public function whenExistsLoaded($relationship, $value = null, $default = null){ if (func_num_args() < 3) { $default = new MissingValue; } $attribute = (string) Str::of($relationship)->snake()->finish('_exists'); // Checks if an attribute exists... if (! array_key_exists($attribute, $this->resource->getAttributes())) { return value($default); } if (func_num_args() === 1) { return $this->resource->{$attribute}; } if ($this->resource->{$attribute} === null) { return; } return value($value, $this->resource->{$attribute});}
This checks for the attribute amenities_exists
on the model. If it's not there, it returns the default value or nothing.
If you want to learn more about APIs, we have a course How to Build Laravel 11 API From Scratch
No comments or questions yet...