Reading Values
The stored column is a single-row read — effectively free. The fresh counterpart recomputes from the source column via a correlated subquery — useful for audit reports or drift detection.
// Single-row fresh recomputation
$category->articles_total; // stored
$category->freshAggregate('articles_total'); // recomputed from source
// Collection-level fresh selects (overlay stored values)
Category::query()->withFreshAggregates()->get();
Category::query()->withFreshAggregates(['articles_total', 'articles_max'])->get();
// Ad-hoc fresh aggregate without declaring a column
use Vusys\NestedSet\Aggregates\Aggregate;
Category::query()->withFreshAggregates([
'descendants_total' => Aggregate::sum('articles')->exclusive(),
])->get();
When to reach for freshAggregate()
| Situation | Use |
|---|---|
| Rendering a tree, hundreds of nodes | stored column — it's already there |
| Drift audit / scheduled health check | freshAggregate() or aggregateErrors() |
| One-off report with a predicate you haven't declared | withFreshAggregates([alias => Aggregate::…]) |
| Source column was just touched outside Eloquent | freshAggregate() until the next repair pass |
Ad-hoc aliases are in-memory only
Aliases passed to withFreshAggregates([...]) that don't match a
declared aggregate column live only on the in-memory model:
$node = Category::query()
->withFreshAggregates([
'descendants_total' => Aggregate::sum('articles')->exclusive(),
])
->find(1);
$node->descendants_total; // computed value, present
$node->save(); // does NOT write descendants_total —
// there is no schema column for it
$node->refresh();
$node->descendants_total; // attribute is gone after refresh
The package writes only declared aggregate columns. Ad-hoc aliases are intended for one-off reads (reports, audits); persisting them would require declaring the column and the aggregate on the model.
When the alias does match a declared aggregate column,
withFreshAggregates() overlays the freshly-computed value on top
of the stored attribute under the same name. Beware: on a
subsequent save(), the model's own dirty tracking treats the
fresh value as the new stored value. If a brief drift window
produced a different fresh result than the stored value, that
drift gets persisted. The safe pattern is to use a separate alias
when you need both the stored and fresh side-by-side:
$node = Category::query()
->withFreshAggregates([
'articles_total_fresh' => Aggregate::sum('articles'),
])
->find(1);
$node->articles_total; // stored value, untouched
$node->articles_total_fresh; // in-memory only, dropped on refresh()