Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

hvolschenk's avatar

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:

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.

0 likes
9 replies
jj15's avatar

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.

1 like
hvolschenk's avatar

@jj15 Then I lose the ability to do $item->location->{property_name} everywhere else, and would need to know that this one is special all the time.

I also showed that it is already possible, just having some weirdness where it is called twice internally.

Snapey's avatar

A relationship method should only return the relationship and never data

Query for location records, using a scope for the language, then iterate over the results and fix any that come back empty.

hvolschenk's avatar

@Snapey

A relationship method should only return the relationship and never data

Which it does, as I showed above, or is there something I am doing that looks incorrect to you?

Edit: I see now. You mean that I am setting it inside the boot() method. Thinking.

Query for location records, using a scope for the language, then iterate over the results and fix any that come back empty.

The method shown above (inside boot()) works, and means that I do less queries to Google Places, which has become even more expensive lately.

Snapey's avatar

@hvolschenk This is probably why it is called twice

$items = $itemsQuery->paginate(12);

pagination runs two queries, one for the total record count and one for the paginated content.

1 like
hvolschenk's avatar

@Snapey - Thank you. While you were responding I was also messing about with this and made a discovery.

When adding either ->with(['location']) to any query, or when adding $with = ['location']; to the Item model, the location() relationship method gets called with an empty instance of the Item model.

I have no idea why this is, or how it then actually works internally, but this is a good starting point. I have found a way to make it work consistently, if I can guarantee that Location is not eager-loaded, which is not an amazing thing either, as how do I make sure other developers always know this.

hvolschenk's avatar
hvolschenk
OP
Best Answer
Level 1

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.

jj15's avatar

@hvolschenk You're welcome, even though I didn't contribute much except my armchair opinion. Best of luck with your project.

1 like

Please or to participate in this conversation.