Purpose
The query surface a theme uses to walk the page tree.
$site->pages->findActiveByParent(0) for a top-level nav,
findBySlug('about') for a one-off lookup, levels() for a
sidebar tree, descendants($parent) for a flat blog-archive
list. Returns Page DTOs throughout so templates never deal in
raw iManager Items.
A handful of write methods (save, delete, reorderOne,
slugTaken, wouldCreateCycle, nextPosition) live on the same
class. They power the editor's Pages module. Themes typically do
not call them; they are documented here for completeness, not for
encouragement.
FQCN + file path
- FQCN:
Scriptor\Boot\Frontend\PageRepository - Source:
boot/Frontend/PageRepository.php
When to use
Templates and theme methods reach it through $site->pages. A
plugin can also pull it directly from the container:
$pages = $container->get(\Scriptor\Boot\Frontend\PageRepository::class);
The repository is request-scoped and cheap to read repeatedly,
but does not memoise: every find() call hits the iManager
ItemRepository. Long traversals should fetch once and iterate
the returned array, not call find() in a loop.
Surface
Read
public function find(int $id): ?Page
public function findBySlug(string $slug): ?Page
public function findHome(): ?Page
public function findAll(): array // list<Page>
find() returns null when no page with that id exists or
when the id resolves to an item in a different category.
findBySlug() is LIMIT 1; if you have collisions (e.g. two
pages with the same slug under different parents), use
findByParent() filtered. findHome() is the canonical lookup
for the page that owns / (the page whose slug is the empty
string).
public function findByParent(
int $parentId,
string $orderBy = 'position',
Direction $direction = Direction::Asc,
bool $activeOnly = false,
int $offset = 0,
int $limit = 0,
): array
Children of one parent in the configured order. Direction is
Imanager\Query\Direction::{Asc,Desc}. $limit = 0 means no
limit. $activeOnly = true filters out items where the editor
toggled the page off.
public function findActiveByParent(int $parentId): array
Convenience for the canonical navigation shape: active pages,
position ASC, no offset, no limit. The default
renderNavigation() on Site uses this for the top-level menu.
public function findInTimeRange(
int $start,
int $end,
int $parentId,
string $orderBy = 'created',
Direction $direction = Direction::Desc,
): array
Active pages under $parentId whose created timestamp falls
in [$start, $end). Drives the bundled basic theme's monthly
blog archive.
public function countByParent(int $parentId, bool $activeOnly = false): int
Count without materialising the result list. Useful for "N posts in this section" badges and for pagination math.
Tree walks
public function levels(
int $rootParent = 0,
int $maxDepth = 0,
bool $activeOnly = true,
array $excludeIds = [],
): array
Materialises a tree of pages keyed by parent id. The return type
is array<int, list<Page>>: each key is a parent id, each value
is the active children of that parent. $maxDepth = 0 walks
until exhausted; $maxDepth = 2 stops after two levels of
nesting. Replaces the 1.x Pages::getPageLevels().
The walk is cycle-safe: a self-parented page or a loop in the parent chain is detected and the recursion bails out without infinite-looping.
public function descendants(Page $parent): array
Flat list of every active descendant beneath $parent,
parents-first. Equivalent to a depth-first walk of levels()
followed by a flatten, optimised for callers that only want the
sequence.
Slug / cycle guards (mostly admin-side)
public function slugTaken(string $slug, int $parentId, ?int $exceptId = null): bool
public function wouldCreateCycle(int $pageId, int $candidateParentId): bool
public function nextPosition(): int
These exist for the editor's Pages module to preflight a save before committing. A theme normally has no reason to call them. Documented so a plugin that ships its own page-creation UI has the right invariants in scope.
slugTaken() ignores $exceptId so a page can keep its own
slug on update. wouldCreateCycle() walks the candidate
parent's existing chain upwards; if it passes through $pageId,
the save would close a loop.
Write (admin-side)
public function save(Item $item): Page
public function delete(int $id): void
public function reorderOne(int $movedId, array $idsInOrder): void
public function renumber(array $idsInOrder): void
save() accepts a raw Imanager\Domain\Item (not a Page)
because writes are owned by the iManager item lifecycle. The
returned Page reflects the persisted state. delete() is
idempotent: deleting a non-existent id is a no-op.
reorderOne() is the interactive drag-reorder path: the editor
sends the moved id plus the full new visual order, and the
repository repositions just that one row (cascade-shifting
following rows only when adjacent positions touch). renumber()
is the legacy bulk path that overwrites every page's position
from the supplied id list; kept for seed imports.
Read-only public state
public readonly int $categoryId;
The numeric Pages category id, resolved from the
categorySlug = 'pages' constructor argument. Exposed so a
plugin that wants to query items in the Pages category directly
through iManager (e.g. for a custom field aggregation) does not
have to re-resolve the slug.
Lifecycle
Container-bound, request-scoped singleton. One instance per
request, constructed from
(CategoryRepository $categories, ItemRepository $items, string $categorySlug = 'pages').
The constructor calls categories->findBySlug($categorySlug)
once and stores the resulting id; subsequent reads use that id
directly.
If the configured category slug is not present in the database
(e.g. a brand-new install before bin/scriptor install ran),
construction raises RuntimeException. In normal operation the
slug is always present because install seeds it.
Common patterns
Top-level navigation
foreach ($site->pages->findActiveByParent(0) as $entry) {
$url = $site->siteUrl . '/' . $site->getPageUrl($entry);
$title = $entry->menu_title !== '' ? $entry->menu_title : $entry->name;
echo sprintf(
'<li><a href="%s">%s</a></li>',
htmlspecialchars($url, \ENT_QUOTES),
htmlspecialchars($title, \ENT_QUOTES),
);
}
Blog archive (children of a "blog" parent, paginated)
$blog = $site->pages->findBySlug('blog');
if ($blog === null) {
return;
}
$posts = $site->pages->findByParent(
$blog->id(),
orderBy: 'created',
direction: \Imanager\Query\Direction::Desc,
activeOnly: true,
offset: ($page - 1) * 10,
limit: 10,
);
Sidebar tree two levels deep
$tree = $site->pages->levels(
rootParent: $site->page->parent,
maxDepth: 2,
activeOnly: true,
);
// $tree[$parentId] => list<Page> children
Walk every descendant (e.g. sitemap generator)
$root = $site->pages->findHome();
if ($root !== null) {
foreach ($site->pages->descendants($root) as $page) {
echo $site->getPageUrl($page), "\n";
}
}
See also
Page: the DTO every read method returnsSite: exposes the repository as$site->pages- Build a Theme: Layouts, Assets, Navigation:
walks
findActiveByParent()andgetPageUrl()to build a real navigation - Build a Theme: Blog via Plugin:
walks
findInTimeRange()for monthly archive aggregation