Data Objects
Record, Page, Field, FieldValue, FieldType, DataFile and PageCollection.
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 Field.
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) |
void |
Writes structuralKeys() to index.json and the rest to data.json |
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, then invokes it if callable. That is how {$page->hero('w=400')} works as shorthand for src().
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, false until published), $access (string, '' for anyone) 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 |
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 |
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 |
render(array $data = [], ?string $block = null) |
string |
Render to HTML, running controllers |
save(array $data) |
void |
Content to data.json, structural keys to index.json |
Field
Field is a named, reusable field definition at site/fields/{name}/index.json, 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. 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 |
url() |
string |
The admin edit-fragment URL, /page/@fieldname |
withKey(string $key) |
FieldValue |
New instance scoped to a collection record key |
__toString() |
string |
The decoded string |
Any other method call (src(), srcset() on an image) is forwarded to the inner FieldType. url() is the exception: it is declared on FieldValue, so it never reaches a file's own url(). For a file or image URL, output the field bare ({$page->cover}) or use ->src(...).
FieldType
Holds a field's encode, decode, empty and validate logic. No page context, rendering or persistence. 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 Fields.
| Member | Description |
|---|---|
$value |
The raw stored value |
$type |
The type name |
decode() |
Override to return the decoded value |
encode(mixed $value) |
Override to encode a value for storage |
empty() |
True if value is null, '' or [] |
create(...) |
Factory; override to consume $options |
__toString() |
(string) $this->decode() |
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('published=1, sort=-created') // right
$page->children()->find('published=1') // no such method
Query syntax and operators are in Registry.