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.