Data Objects

Record, Page, Field, FieldValue, FieldType, DataFile and PageCollection.

On this page

Record

Record (Phlat\Data\Record) wraps an _index.json and a colocated _data.json, and exposes content fields through __get. It is the abstract base for Page, User and Item, a collection entry that lives in its collection's database rather than in files.

Properties

Property Type Description
$path string Absolute folder path, no trailing slash
$uri string App-relative folder URI
$uuid string Persisted identity, from _index.json
$locked bool Structural flag
$created / $updated ?string ISO 8601 timestamps

Methods

Method Returns Description
exists() bool True if _index.json exists on disk
path() string The folder path, with a trailing slash
name() string The folder name
folder() string App-relative folder URI
index() DataFile The _index.json
data() DataFile The _data.json. On a page this honours the logged-in editor's draft autosave
publishedData() DataFile The canonical _data.json, bypassing any draft
save(array $data) static Writes structuralKeys() to _index.json and the rest to _data.json, and returns the record as saved

Saving

save() returns the record as it is on disk afterwards and leaves the instance it was called on as it was read, so a record's fields and its structural properties ($published, $view, a user's $role) never disagree. Keep what it returns:

$page = $page->save(['title' => 'New title']);

$page->title; // New title

Field access

A key that isn't a typed property goes through __get, which reads _data.json and returns a FieldValue:

$raw   = $page->data()->get('body'); // string from _data.json
$field = $page->body;                // FieldValue
echo $field;                          // decoded string

__call resolves the field via __get and invokes it when its type is callable, which is how {$page->hero('w=400')} works as shorthand for src(). Any other method name throws BadMethodCallException, so a misspelled method fails instead of returning null.


Page

Page extends Record and is one page in the site tree. Get one from $app->page($uri), never new.

Page adds $view (?string, default null), $published (bool, true unless _index.json says false), $access (string, '' for anyone), $hidden (bool, left out of navigation and child lists) and $router. title is not a structural property; it is a content field like any other.

Methods

Method Returns Description
url(string $path = '', array $query = []) string Public URL, e.g. /blog/my-post/
slug() string The page's own slug
isViewable() bool Whether the visitor may open it. See Publishing and access
assertViewable() void Throws what the visitor gets for a page they may not open: a 404, a login redirect or a 403
isPublic() bool Whether anyone may open it: the page and every ancestor are published and open to all
parent() ?Page Parent, or null at the root
root() Page The root page
section() Page The top-level ancestor
depth() int Nesting depth below the root
siblings(string $query = '') PageCollection Parent's other children
next() / prev() ?Page Neighbouring sibling in order
has(string $slug) bool True if a direct child has this slug
hasChildren() bool True if there are any children
contains(Page $other) bool True if $other is this page or a descendant
isActive() / isOpen() bool The current page / the current page or an ancestor of it
new(string $name, array $data = []) Page Create a child page
delete() void Delete the page and its descendants
move(string $newPath) Page Move or rename the page and its subtree, e.g. blog/2026/post. Returns the page at its new path
parents() PageCollection All ancestors, root first
child(string $slug) ?Page A direct child by slug; doesn't check it exists
children(string $query = '') PageCollection Direct children, hidden excluded, filtered and sorted by $query
find(string $query = '') PageCollection Query across this page's subtree
files() array PageFile objects for the non-JSON files in the folder
file(string $uri) / image(string $uri) PageFile / PageImage One of the page's files, by filename
upload(array $file, array $options = []) PageFile Store an uploaded file
view() PageView The resolved view
fragment() bool Whether the page renders as a bare fragment, from its _index.json or its view
session() Session The page's session scope: get, set, has, delete, flash, pop
render(array $data = [], ?string $block = null) string Render to HTML, running controllers
save(array $data) Page Content to _data.json, structural keys to _index.json. Returns the saved page: $page = $page->save([...])

Field

Field is a named, reusable field definition, read from a fields/{name}/_index.json with its extends chain applied, not what template field access returns. It is read-only: definitions are edited as files.

Property Type Description
$name string The field's name, e.g. body
$type string The field type, e.g. @phlat/markdown
$label string Admin-facing label
$options array Default options (required, max_length, ...)

FieldValue

What $page->fieldname returns: the per-render value, combining a definition (name, type, label, options) with a FieldType instance. It is never persisted or indexed.

Method Returns Description
decode() mixed The decoded value, via the inner FieldType
encode(mixed $value) mixed Encode a value for storage
empty() bool True if the raw value is null, '' or []
type() FieldType The inner FieldType
render() string Render through the type's render template
validate(mixed $rawValue) array Validation errors for a raw value, empty when it passes
filled(mixed $value, array $errors = []) FieldValue This definition applied to a different value, such as what a form posted
editTemplate() / previewTemplate() ?string Path of the type's admin template, or null
__toString() string The decoded string

Any other method call (src(), srcset() on an image, url() on a file) is forwarded to the inner FieldType. Calling the value itself, as in $page->hero('w=400'), invokes the inner type when it is callable.


FieldType

Holds how a type encodes, decodes, validates and indexes a value. It is stateless and shared per class, so every method receives the FieldValue it works on: the value is $field->value, not a property of the type. Subclasses are FieldType plus the type name (FieldTypeMarkdown, FieldTypeImage), and four primitives cover every JSON shape: FieldTypeText, FieldTypeNumber, FieldTypeList and FieldTypeObject. To write one, see Custom fields.

Member Description
decode(FieldValue $field) Override to return the decoded value
encode(FieldValue $field, mixed $raw) Override to encode a value for storage
empty(FieldValue $field) True if the value is null, '' or []
validate(FieldValue $field, mixed $raw) Validation errors for a raw value
preview(FieldValue $field) One-line admin summary
child(FieldValue $field, string $key) A container's sub-field, or null
indexValues(FieldValue $field) / indexChildren(FieldValue $field) The rows a value contributes to search
text(FieldValue $field) The decoded value as a string

DataFile

DataFile extends File and wraps a JSON file, decoding it into $data on construction.

Member Description
$data Decoded JSON as an array
get(string $key, mixed $default = null) Dot-notation access: get('meta.title')
merge(DataFile $other) New DataFile with deeply merged data
write(array $data) Merge, write, and return a fresh DataFile
__get(string $key) Shorthand for get($key)

PageCollection

An iterable list of Page instances, returned by Page::children(), parents() and find(). (RecordCollection is the generic base.) It implements IteratorAggregate and Countable, so it works directly in {foreach}. Pages load from the registry lazily, on iteration.

Method Returns Description
count() int Number of items
isEmpty() bool True if there are no items
first() / last() ?Page First / last item
nth(int $index) ?Page Item at an index
reverse() static Reversed copy
limit(int $n) / offset(int $n) static Cap at N items / skip the first N
contains(Record $item) bool True if the item is in the collection
toArray() array The items as a plain array
each(callable $fn) static Call the callback for every item
any(callable $fn) / all(callable $fn) bool True if any / every item passes
append(Record $item) static Add an item
filter(callable $fn) static Items where the callback returns true
sort(callable $fn) static Sorted by a comparison callback
pluck(string $field) array One field's value across all items

The collection has no find(). The query DSL lives on Page::children(), Page::find() and Registry::find(), so pass the query there:

$page->children('sort=-created')  // right
$page->children()->find('published=1')          // no such method

Query syntax and operators are in Registry.