It feels to me like it would be cleaner for you to simply leave the location() relationship method as-is to its default duty of just fetching the related model whether it exists or not. Then create a separate method on your model that calls it, applies any query constraints, and performs the necessary business logic.
Relationship with custom query
Hi. I am having some trouble implementing a custom query for a relationship.
Simplified, there are two models: Item and Location.
Each Item BelongsTo a Location, however a Location can be stored in multiple languages
and I'd like to fetch the Location in the request user's language.
There is also a chance that the Location does not exist in the desired language yet,
where it will then need to be fetched from Google Places first.
I have read in another discussion that it should be possible to replace the relationship query completely, but I am not having so much luck with this.
Some code to show what I am trying:
app/Models/Item.php:
class Item extends Model
{
// ...
public static function boot()
{
parent::boot();
self::retrieved(function ($item) {
Log::debug('Item::boot()', ['location' => $item->location]);
if ($item->location === null) {
$item->location = Location::fetchFromGooglePlaces(
$item->googlePlaceID,
App::currentLocale(),
);
}
return $item;
});
}
public function location(): BelongsTo
{
Log::debug('Item->location', [
'googlePlaceID' => $this->googlePlaceID,
'id' => $this->id,
'language' => App::currentLocale(),
]);
$relation = $this->belongsTo(Location::class);
$relation->setQuery(
Location::where([
'googlePlaceID' => $this->googlePlaceID,
'language' => App::currentLocale(),
])->getQuery(),
);
return $relation;
}
}
app/Http/Controllers/ItemController.php:
class ItemController extends Controller
{
public function search(ItemSearchRequest $request)
{
$validated = $request->safe(['query']);
$searchQuery = $validated['query'] ?? null;
$itemsQuery = Item::where('is_given', false);
if ($searchQuery !== null) {
$itemsQuery->where(function (Builder $builder) use ($searchQuery) {
$builder
->whereFullText('name', $searchQuery)
->orWhereFullText('description', $searchQuery);
});
}
$items = $itemsQuery->paginate(12);
foreach ($items as $itemDebug) {
Log::debug('ItemController->search', ['location' => $itemDebug->location]);
}
return view('pages.search', [
'items' => $items,
]);
}
}
The log output from the above is:
local.DEBUG: Item->location {"googlePlaceID":"ChIJawqVBkRfzB0RDN1rJqRkMGQ","id":1,"language":"en"}
local.DEBUG: Item::boot() {"location":{"App\\Models\\Location":{"id":7,"name":"Parklands","language":"en","googlePlaceID":"ChIJawqVBkRfzB0RDN1rJqRkMGQ"}}}
local.DEBUG: Item->location {"googlePlaceID":null,"id":null,"language":"en"}
local.DEBUG: ItemController->search {"items":1}
local.DEBUG: ItemController->search {"location":null}
The strange part about this (in my opinion) is that Item->location() is called twice.
Once with an id and once without.
The without variant always happens after the with variant,
and overwrites the location to null.
I can fix this by adding a check for the id around the custom query:
public function location(): BelongsTo
{
Log::debug('Item->location', [
'googlePlaceID' => $this->googlePlaceID,
'id' => $this->id,
'language' => App::currentLocale(),
]);
$relation = $this->belongsTo(Location::class);
if ($this->id) {
$relation->setQuery(
Location::where([
'googlePlaceID' => $this->googlePlaceID,
'language' => App::currentLocale(),
])->getQuery(),
);
}
return $relation;
}
But why is this happening, or necessary? I feel like I am treating a symptom, and not a cause.
An update. I decided to stick with the custom query for lazy loaded relationships, and to accept that eager-loaded Locations will contain the Location originally added with the Item. I had to add a check to know if the relationship was being eager-loaded, and now the updated model looks like this.
app/Models/Item.php:
class Item extends Model
{
// ...
public function location(): BelongsTo
{
$relation = $this->belongsTo(Location::class);
if ($this->googlePlaceID) {
Location::saveIfNotExists($this->googlePlaceID, App::currentLocale());
$relation->setQuery(
Location::where([
'googlePlaceID' => $this->googlePlaceID,
'language' => App::currentLocale(),
])->getQuery(),
);
}
return $relation;
}
}
Thank you @jj15 and @snapey for your time and assistance here. It is much appreciated.
Please or to participate in this conversation.