Data Objects

Record is the abstract base for Page, User, and Field. Understanding the hierarchy clarifies how field access, saving, and collection queries work throughout the framework.

Record

Record (Phlat\Data\Record) wraps an index.json file plus a colocated data.json, and exposes content fields through __get. It is the abstract base for Page, User, and Field. All reads and writes go through it.

Properties

Property Type Description
$path string Absolute filesystem path to the folder (no trailing slash)
$uri string App-relative URI of the folder
$uuid string Persisted identity, read from index.json
$hidden bool Structural flag
$status string Structural flag, 'published' by default
$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 Same folder path, with a trailing slash
name() string The folder name, last path segment
folder() string App-relative folder URI
save(array $data) void Splits $data by structuralKeys(): those keys to index.json, the rest to data.json

Field access via __get

Accessing a key not declared as a typed PHP property triggers __get, which reads the value from data.json and passes it through the field() hook, returning a FieldValue.

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

__call is also defined: $page->hero(...) resolves the hero field via __get, then invokes it if it's callable, this is how {$page->hero('w=400')} works as shorthand for src(...).


Page

Page extends Record and represents a single content page in the site tree. Always constructed via $app->page($uri), never directly.

Structural properties

Inherited from Record, populated from index.json. Page adds one of its own:

Property Type Default
$uuid string Minted at creation
$view ?string null
$hidden bool false
$status string 'published'
$locked bool false
$created / $updated ?string ISO 8601 timestamp

title is not a structural property, it's an ordinary content field read from data.json like any other.

Methods

Method Returns Description
url(string $path = '', array $query = []) string The public URL of the page, e.g. /blog/my-post/
parent() ?Page Parent page, or null at the root
parents() PageCollection All ancestors, root-first
child(string $slug) ?Page A direct child page by slug, doesn't check it exists
children(string $query = '') PageCollection Direct children (hidden excluded by default), filtered/sorted by $query
find(string $query = '') PageCollection Recursive query across this page's subtree
files() array PageFile objects for non-JSON files in the page folder
view() PageView The resolved view for this page
render(array $data = [], ?string $block = null) string Render the page to HTML, running controllers
save(array $data) void Persists content to data.json; structural keys to index.json

Fetching pages

$post    = $app->page('blog/my-post');  // by path
$root    = $app->page('/');             // root page
$current = $app->page();                // current request page

Content fields vs structural properties

$page->title;  // FieldValue, from __get, a content field like any other
$page->view;   // string|null, typed readonly property

Field

Field extends Record too, but it's a different thing from a page: a named, reusable field definition at site/fields/{name}/index.json, holding a label, a type name, and default options. It is not what template field access returns.

Properties

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

Methods

Method Returns Description
optionFields() FieldValueSet This field's own options, resolved against its type's option schema

FieldValue

What $page->fieldname actually returns, the per-render resolved value, combining a Field-like definition (name/type/label/options) with a FieldType inner instance. Never persisted, never registry-indexed.

Methods

Method Returns Description
decode() mixed The decoded value (HTML for markdown, etc.), delegates to 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 underlying FieldType instance
render() string Render through the field type's render template
url() string Hypermedia edit-fragment URL for this field, /page/@fieldname, not a file/image URL
withKey(string $key) FieldValue New instance scoped to a collection record key
__toString() string Returns (string) inner, decoded string

Any method not declared directly on FieldValue (e.g. src(), srcset() on an image field) is forwarded to the inner FieldType via __call. But url() is declared directly on FieldValue, so calling it never reaches the inner file's own url(), it always returns the edit-fragment URL above. For a file/image's public URL, use the field bare ({$page->cover}) or ->src(...), never ->url().

Always check empty() before rendering optional fields, a FieldValue is always truthy in PHP regardless of its value:

{if !$page->cover->empty()}
    <img src="{$page->cover}" alt="">
{/if}

FieldType

The class holding a field's actual encode/decode/empty/validate logic. No page context, no rendering, no persistence. Subclasses are named FieldType + the type name (FieldTypeMarkdown, FieldTypeImage, etc.), and the four primitives, FieldTypeText, FieldTypeNumber, FieldTypeList, FieldTypeObject, cover every JSON value shape.

Properties

Property Type Description
$value mixed The raw stored value
$type string The type name

Methods

Method Returns Description
decode() mixed Override to return the decoded value
encode(mixed $value) mixed Override to encode a value for storage
empty() bool True if value is null, '', or []
create(...) static Factory method, override to consume $options
__toString() string Returns (string) $this->decode()

Custom field types

Create site/fieldtypes/{type}/{type}.php:

<?php
namespace Phlat;

class FieldTypeRating extends FieldTypeNumber
{
    public function decode(): mixed
    {
        return max(0, min(5, (int) $this->value));
    }

    public function stars(): string
    {
        $n = $this->decode();
        return str_repeat('★', $n) . str_repeat('☆', 5 - $n);
    }
}

Add site/fieldtypes/{type}/index.json with a title/description for the admin UI. For |render output, add site/views/fields/{type}.latte, it receives $field (the FieldValue) and $value (the decoded value). Reference the type from a field definition:

// site/fields/score/index.json
{ "label": "Score", "type": "rating", "options": {} }

Types in site/fieldtypes/ override matching types in phlat/fieldtypes/.


DataFile

DataFile extends File and wraps a JSON file on disk. Decodes the file into a $data array on construction. Provides dot-notation key access.

Properties

Property Type Description
$data array Decoded JSON as an associative array

Methods

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

PageCollection

An iterable, filterable list of Page instances (RecordCollection is the generic base, for other Record types). Returned by Page::children(), Page::parents(), and Page::find(). Implements IteratorAggregate and Countable, use directly in {foreach} loops.

Methods

Method Returns Description
count() int Number of items
first() ?Page First item, or null
last() ?Page Last item, or null
nth(int $index) ?Page Item at a given index, or null
reverse() static New collection, reversed
limit(int $n) static Cap iteration at N items
offset(int $n) static Skip the first N items
filter(callable $fn) static New collection of items where callback returns true
sort(callable $fn) static New collection sorted by comparison callback
pluck(string $field) array Values of one field across all items

There is no find() on the collection itself. The query DSL lives on Page::children(string $query), Page::find(string $query), and Registry::find(string $query), pass the query directly into one of those instead of chaining ->find() after the fact:

$page->children('status=published, sort=-created')  // correct
$page->children()->find('status=published')          // wrong, no such method

Query syntax

Conditions are comma-separated. All must match (AND logic). One sort= clause is allowed.

$page->children('status=published, sort=-created');
$page->children('tags*=php, sort=title');
Operator Meaning
= Exact match
!= Not equal
*= Contains
^= Starts with
$= Ends with
< > <= >= Comparison

Sort: sort=field ascending, sort=-field descending. Page objects are loaded from the registry lazily, only when iterated.