Queries in a template
Two helpers fetch data straight from the markup, and the difference between them is not syntax but what comes back:
| Helper | Returns | Use it for |
|---|---|---|
query('site_content') | stdClass rows | a number, a flat list, an arbitrary join |
model('Resource') | Eloquent models | relations, scopes, casts, constructor fields |
Both are the same ones you use in controllers; the vocabulary is shared, and only what matters inside a template is covered here.
query(): the query builder
query() returns an Illuminate query builder over the MODX database. Pass the table name without the prefix and without an xPDO class name — site_content, not modResource and not modx_site_content.
📊 Counting records
{query('site_content')->whereNotNull('publishedon')->count()}📋 A list of resources
{set $resources = query('site_content')
->where('template', 4)
->orderBy('menuindex')
->limit(5)
->get()
}
<ul>
{foreach $resources as $res}
<li><a href="{$res->uri}">{$res->pagetitle}</a></li>
{/foreach}
</ul>Rows are objects — read fields with ->
get() returns a PbCollection of stdClass. Dot notation {$res.pagetitle} compiles to $res['pagetitle'] and kills the render: "Cannot use object of type stdClass as array". That is a fatal — a blank 500, not a blank spot on the page.
🔀 join
The signature is Illuminate's, not xPDO's: table, left column, operator, right column. The alias goes into the table string itself (users as u); there is no separate alias():
{set $users = query('users as u')
->join('user_attributes as p', 'u.id', '=', 'p.internalKey')
->where('u.active', 1)
->select('u.username', 'p.fullname')
->orderBy('u.username')
->get()
}
{foreach $users as $user}
<p>{$user->username} ({$user->fullname})</p>
{/foreach}🔍 A record by ID
{set $resource = query('site_content')->find(10)}
{if $resource}
<h2>{$resource->pagetitle}</h2>
{/if}🔧 value() and pluck()
{query('site_content')->where('id', 5)->value('pagetitle')}{set $titles = query('site_content')->where('parent', 0)->pluck('pagetitle')}
<pre>{$titles|print}</pre>📦 JSON columns
data, properties and sync_fields are stored as JSON, and by default the builder returns them as strings. withJsonColumns() decodes the named columns into arrays — and an array is what a template reads with a dot:
{set $rows = query('pb_block_data')->withJsonColumns(['data'])->where('model_id', 1)->get()}
{foreach $rows as $row}
{$row->data.title}
{/foreach}Without it, the same thing is done inline by a modifier: {set $data = $row->data|fromJSON}.
model(): Eloquent models
model() opens a query on a model from PageBlocks\App\Models\, addressed by its short class name, without the namespace:
{model('Resource')->published()->count()}Rows come back as models rather than stdClass, and with them everything the raw builder does not have: relations, scopes, casts, accessors and constructor fields out of the JSON column.
Scopes instead of conditions
What you would spell out by hand with query() already has a name on the model:
{foreach model('Resource')->published()->visible()->limit(5)->get() as $res}
<a href="{$res->uri}">{$res->pagetitle}</a>
{/foreach}Resource has published() (published = 1), visible() (hidemenu = 0) and search($text) — which searches pagetitle, longtitle, description and content at once. User has active(), inactive(), sudo(), inGroup($id), searchByUsername($text) and searchByEmail($text); the last one looks inside the profile, which a raw query() would need a join to reach.
Relations
A relation is read as a property and loads itself:
{set $res = model('Resource')->find(1)}
<h1>{$res->pagetitle}</h1>
{if $res->parentResource}
<a href="{$res->parentResource->uri}">← {$res->parentResource->pagetitle}</a>
{/if}
{foreach $res->children as $child}
<li><a href="{$child->uri}">{$child->pagetitle}</a></li>
{/foreach}A relation inside a loop is one query per iteration. For longer lists, fetch it up front with with():
{foreach model('User')->with('profile')->active()->limit(20)->get() as $user}
<p>{$user->username} — {$user->profile->fullname}</p>
{/foreach}Constructor fields
A field added in the manager has no column of its own — it lives in the data JSON column and the model unpacks it. In a template it is indistinguishable from a real one:
{set $block = model('PbBlockData')->find(7)}
{$block->title}
{$block->seo_text}Dot notation does not reach constructor fields
On a model dot notation works — {$res.pagetitle} and {$res->pagetitle} are the same thing, because Eloquent implements ArrayAccess. But only for real columns. A constructor field lives in the extra attributes and is reachable through __get alone:
{$block->title} {* International freight exchange *}
{$block.title} {* empty *}
{if $block.title} {* always false *}And silently: no error, no log entry — just missing text and a condition that never fires. In templates that touch constructor fields, use -> everywhere rather than keeping track of which field is a real column and which is not.
Available models
Resource, PbResource, PbBlockData, PbTableData, User, UserProfile, UserGroup, UserGroupMember, UserGroupRole, UserSetting, Template, Chunk, Snippet, Plugin, Event, Menu, Namespaces, Context, ContextSetting, SystemSetting, Source, Session.
A name that is not in this set is a fatal Class … not found — a blank 500. Your own site models go in the same place, core/App/Models/, and become reachable by their short name automatically.
Which to pick
The ready-made modifiers cover the most common lists and already account for publication, context and ordering — start there:
{foreach resources(['parent' => 5, 'limit' => 10]) as $res}…{/foreach}
{foreach users(['group' => 2]) as $user}…{/foreach}Not enough — reach for model(). And only when you need a join across tables that have no model, or a bare number, reach for query().
A query in a loop is still a query in a loop
Nothing stops you from putting query() or model() inside a {foreach} and getting a hundred queries per page. Fetch once, before the loop; pull relations with with().