In an Eloquent model, what is the difference between $photo->tags and $photo->tags(), and when does each run a query?
answer
- one is a builder, one is results
- property result cached on the model
- method: new query every terminal call
- count() in SQL vs in PHP
- 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 sCalling 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
Recall that the method returns a query builder and the property returns results, and that writes always use the method.
Explain how the property lookup checks the loaded relations first, queries once and caches, and why count() differs between the two forms.
Point to real costs: loading a huge collection just to count it, stale cached relations after writes, and repeated method calls in views.
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()