skip to content

In an Eloquent model, what is the difference between $photo->tags and $photo->tags(), and when does each run a query?

level: juniorimportance: must knowfreq 66%

answer

  1. one is a builder, one is results
  2. property result cached on the model
  3. method: new query every terminal call
  4. count() in SQL vs in PHP
  5. writes go through the method

basics

~20 s

$photo->tags() returns the relation object, a query builder that runs a fresh query on each terminal call. $photo->tags returns the loaded results, querying once on first access and then reusing the collection cached on the model.

solid answer

~40 s

Calling the method, `$photo->tags()`, returns a `BelongsToMany` relation: a query builder already constrained to this photo. Nothing runs until a terminal call such as `get()`, `count()` or `first()`, and every terminal call runs a new query — which lets you add constraints like `->where('slug', 'sunset')`. Reading the property, `$photo->tags`, goes through the model's attribute lookup: if the relation is already loaded it returns the cached value, otherwise it runs the relation query once, stores the result on the model and returns it — a `Collection` for to-many relations, a model or `null` for to-one. So `$photo->tags()->count()` runs a `COUNT` query, while `$photo->tags->count()` loads every tag and counts in PHP. Writes such as `attach()`, `sync()` or `associate()` always go through the method.

go deeper

for a junior

Recall that the method returns a query builder and the property returns results, and that writes always use the method.

for a middle

Explain how the property lookup checks the loaded relations first, queries once and caches, and why count() differs between the two forms.

for a senior

Point to real costs: loading a huge collection just to count it, stale cached relations after writes, and repeated method calls in views.

for a principal

Set team conventions on when relation results may be cached on a model and when code must query explicitly, and how reviews spot the difference.

## Two spellings, two different things An Eloquent relation is declared as a method on the model: ```php public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } ``` That single method can be reached in two ways, and they return different things. | Expression | What you get | When SQL runs | |---|---|---| | `$photo->tags()` | A `BelongsToMany` **relation object**, a query builder scoped to this photo | Only on a terminal call (`get()`, `count()`, `first()`, `exists()`, `pluck()`), and **again on every** such call | | `$photo->tags` | The **results**: an Eloquent `Collection` of `Tag` models (for to-one relations, a model or `null`) | Once, on first access if not already loaded; later reads reuse the cached value | ## How the property form works A model has no real `tags` property. Reading `$photo->tags` falls through to the model's attribute lookup. In outline, the framework's `getRelationValue()` then does this: 1. If the relation is already in the model's loaded relations, return it without querying. 2. If `tags` is not a relation method, return `null`. 3. Otherwise call `tags()`, run the relation query, store the result on the model with `setRelation()`, and return it. Step 3 means the **first** read queries and every later read on the same model instance is free. It also means the value is a **snapshot**: after `$photo->tags()->attach(9)`, an already-loaded `$photo->tags` does not contain tag 9 until the relation is reloaded. If the relation method forgets its `return`, the property form throws a `LogicException` saying the method must return a relationship instance. ## How the method form works `$photo->tags()` builds a fresh relation object every time you call it. It behaves like a query builder with the join and the `photo_id` constraint already applied, so you can keep chaining: - `$photo->tags()->where('slug', 'like', 'sun%')->get()` filters in SQL and returns only the matching tags. - `$photo->tags()->orderBy('name')->pluck('name')` reads one column. - `$photo->tags()->exists()` asks the database a yes/no question. None of this touches the cached `tags` value on the model. ## Where the difference matters - **Counting.** `$photo->tags()->count()` sends a `COUNT` query. `$photo->tags->count()` loads every tag row and counts the collection in PHP. With five tags nobody notices; with fifty thousand likes on a popular photo, the second loads fifty thousand models. If the collection is **already loaded**, though, counting it in PHP costs nothing. - **Filtering.** `$photo->tags()->where(...)` filters in the database. `$photo->tags->where(...)` is a `Collection` method that filters rows already in memory — correct, but only after loading all of them. - **Writing.** Every write goes through the method: `$photo->tags()->attach()`, `sync()`, `detach()`, `$photo->album()->associate()`, `$album->photos()->create([...])`. The property is a plain collection or model; it has no `attach()`. - **Repeated reads.** In a Blade view that prints `$photo->tags` three times, the property form queries once. Writing `$photo->tags()->get()` three times queries three times. ## The rule to remember Use the **property** when you want the related records and are happy for them to be cached on this model. Use the **method** when you want to add constraints, ask an aggregate question in SQL, or write through the relation. Loading relations for many parents at once is a separate subject (eager loading), and so is making lazy property reads fail in development. ## Inspecting and resetting the cache The cached value lives in the model's relations array, and a few public methods work with it directly: - `$photo->relationLoaded('tags')` returns `true` once the property has been read (or the relation was loaded some other way), which is handy in a view that should not trigger a query. - `$photo->setRelation('tags', $collection)` stores a value you already have, so a later `$photo->tags` returns it without SQL. - `$photo->unsetRelation('tags')` forgets the cached value; the next property read queries again. This is the cheapest way to make a stale relation fresh after a write. ## A worked example On a photo detail page: 1. `$photo->tags` in the header runs one query and caches five tags. 2. `$photo->tags` in the sidebar costs nothing. 3. `$photo->tags()->where('featured', true)->get()` in the footer runs a new, filtered query and does **not** replace the cached five. 4. `$photo->tags()->attach($newTagId)` writes a pivot row; the cached collection still has five tags until you unset or reload it.

  • After $photo->tags()->sync([2, 4]), why might $photo->tags still show the old tags?
    The property returns whatever was cached on the model the first time it was read. `sync()` writes the pivot table through the relation object and never updates that cached collection. If `$photo->tags` had been read before the sync, you must reload the relation to see the new set; if it had not been read yet, the first read after the sync queries fresh data.
  • What does $photo->album return when the photo has no album?
    For a to-one relation such as `belongsTo`, the property returns the related model or `null` when no row matches, so `$photo->album->title` would fail with an access on null. To-many relations never return `null`: `$photo->tags` is an empty `Collection` when nothing is attached.

The method is a library request slip already filled in with this photo's ID: each time you hand it in, the library fetches again, and you may add conditions first. The property is the stack of books already on your desk: fetched the first time, then simply picked up again.

saying these in an interview costs you the question

  • Saying $photo->tags runs a new query on every access
  • Believing $photo->tags() returns the loaded Collection of tags
  • Calling attach() or sync() on the $photo->tags property
  • Thinking $photo->tags->count() and $photo->tags()->count() always cost the same
  • Expecting the loaded property to update itself after sync()