# Phlo: full documentation
Generated from https://phlo.tech. Includes the complete guide plus a manual summary. For the full structured API schema see https://phlo.tech/manual/schema.
---
# 1. Introduction
Phlo is an integrated platform with its own full-stack language. You write `.phlo` source files; Phlo transpiles them to PHP, CSS and JavaScript you can open and read, with every runtime error pointing back at the `.phlo` line you wrote. The same language carries four layers: the language itself, the application platform (backend resources plus the phlo.js SPA engine), the server platform (FrankenPHP, the Phlo Daemon, Phlo Realtime, Phlo WhatsApp) and the operations platform (the Phlo Dashboard). This guide covers all of them. The production release runs on a shared runtime, usually in `/opt/phlo/`, while each app keeps its own source, data, generated PHP and webroot.
---
## 1.1 Philosophy
* **Source first**: you work in `.phlo`, not in generated PHP, CSS or JavaScript.
* **A small runtime**: the web entrypoint loads `/opt/phlo/phlo.php` and starts the app with `phlo_app(...)`.
* **Explicit runtime resources**: `data/app.json` selects which Phlo resources an app loads from the runtime catalog; app code stays in the app.
* **Dev and release separated**: dev builds, debugs and can use auth; release uses the generated output.
* **HTML, routes and behavior close together**: a route, view, style or script can live in the same topic file when that makes the app clearer.
* **Server-driven frontend**: mark a form `
` or a link `` and Phlo intercepts the submit or click, posts to your route, and patches the DOM with the response. You write routes and views, not `fetch` calls or client-side state.
---
## 1.2 Installation
Phlo requires PHP 8.3 or higher; the CLI build runs on the same PHP. For production, [FrankenPHP](https://frankenphp.dev) is the recommended runtime (built-in web server, worker mode); classic PHP-FPM behind Nginx works too.
Fetch the runtime and scaffold your first app with the bundled installer:
```bash
git clone https://github.com/q-ainl/phlo.git /opt/phlo
php /opt/phlo/install.php /var/www/example.nl/
```
The installer asks for a name, host and target, shows the runtime catalog and lets you pick resources (their `@ requires` are included automatically), writes the entrypoint, `data/app.json`, `data/app.md`, a first route and `.gitignore`, and only finishes after a clean build with concrete next steps.
Prefer a copy that cleans up after itself? Copy `install.php` into the new app directory and run it there; after a successful installation it removes itself:
```bash
cp /opt/phlo/install.php /var/www/example.nl/ && cd /var/www/example.nl && php install.php
```
The next sections describe what the installer sets up for you, and how to build the same thing by hand.
---
## 1.3 Project structure
A typical app:
```text
/var/www/example.nl/
app.phlo
page.home.phlo
data/
app.json
auth.ini
creds.ini
php/
app.php
classmap.php
release/
www/
app.php
www/
app.php
```
`php/`, `www/app.js`, `www/app.css` and `release/` are build output. Only change them through the source and rebuild.
---
## 1.4 Entrypoint
Dev entrypoint in `www/app.php`:
```php
`) |
| `phlo.async` | Backendhelper `phlo_async()` voor CLI-calls via de daemonpool of een eenmalig subprocess |
| `visitors` | Heartbeat/visitor tracking |
| `useragent` | User-agent parsing |
| `DB/DB`, `DB/MySQL`, `DB/model` | Database and ORM |
Only use resources the app actually needs. The Phlo Control Center can show available resources and dependencies.
Do not confuse the two async layers. SPA navigation and async forms run through the frontend runtime generated in `www/app.js`, with resources such as `DOM/link` and `DOM/form`. The separate `phlo.async` resource provides the backend function `phlo_async()` for dispatching a PHP/CLI call outside the current request.
To configure a resource for one app without forking it, inject a value onto its node from `app.phlo` (`prop %AI.model = 'claude-opus-4-8'`, `static %model.DB => %MySQL`). See Advanced, "Modifying resources without forking".
---
## 2.3 Dev exclude
In a local dev build you often want to leave out certain tracking and realtime resources:
```json
{
"exclude": [
"visitors",
"useragent",
"wsCast"
]
}
```
This applies to the dev build. The release build does not use this exclude automatically; visitor tracking can therefore still be active there.
---
## 2.4 Release
The short form is enough:
```json
{
"release": "%app/release/"
}
```
Phlo then writes release PHP to `release/` and web assets to `release/www/`.
---
## 2.5 Paths
`%app/` refers to the app path from `phlo_app(...)`. Keep path configuration in `www/app.php` and `release/www/app.php` as much as possible, so `data/app.json` stays about build behavior.
---
## 2.6 Namespaces and bundles
Every `
```
Output (conceptually):
```css
html { height: 100dvh; }
body { background: #947b6c; font-family: Sans-serif; }
body p { line-height: 2em; }
```
A `
```
* Context: `body`
* Targets: `h1` and `p` (with the glued `:first-letter`)
* The **backslash** before `:first-letter` glues that part to the preceding selector within the chain.
Output:
```css
body h1:first-letter,
body p:first-letter { color: green; }
```
---
## 7.3 Media queries inside a selector
You may write `@media (…)` **inside** the selector block; Phlo moves it to the right place and keeps the selector context:
```phlo
```
Output:
```css
h1 { color: white; }
@media (max-width: 768px){
h1 { color: black; }
}
```
---
## 7.4 Variables
Phlo supports **CSS variables** via `$names`.
You can define variables in `:root`, or at any other level, but `:root` is the usual place for global theming.
```phlo
```
**Output**
```css
:root {
--background: #0d0d0d;
--surface: #1a1a1a;
--text: #ffffff;
--accent: #ff4a00;
}
body {
background: var(--background);
color: var(--text);
}
button {
background: var(--accent);
color: var(--text);
}
```
👉 Phlo automatically converts `$variables` to `--custom-properties` and uses `var(--...)` where they are referenced.
You can reuse variables anywhere, including inside media queries and nested selectors.
---
## 7.5 Dynamic variables
Phlo's frontend engine includes the **`DOM/CSS.var`** library, which lets you **read and update defined `$variables` in CSS directly from JavaScript**, via the global `app.var` object.
Every `$variable` in your CSS automatically becomes available as `app.var.`.
### Example
```phlo
```
* `app.var.background = '#000000'` → live-updates the value of `--background` in the DOM, without a rebuild or reload.
* `const textColor = app.var.text` → reads back the current value.
👉 These updates work **in real time** in the browser and immediately affect all elements that use the variable.
You can use this for, among other things:
* **Theme switches** (dark/light mode)
* Dynamically adjusting accent colors based on user input
* Interactive UIs without toggling separate CSS classes
### How it works
* The CSS engine converts `$background` to `--background`.
* The frontend engine reads/writes it via `document.documentElement.style`.
* `app.var` provides a simple proxy object, so you can work with these as if they were plain JS properties.
---
## 7.6 Full example
**Input**
```phlo
html: height: 100dvh
body {
background: #947b6c
font-family: Sans-serif
p: line-height: 2em
}
body: h1, p: \:first-letter: color: green
h1 {
color: white
@media (max-width: 768px): color: black
}
p {
color: navy
\:last-child: color: yellow
}
```
**Output**
```css
body {
background: #947b6c;
font-family: Sans-serif;
}
body h1:first-letter,
body p:first-letter {
color: green;
}
body p {
line-height: 2em;
}
h1 {
color: white;
}
html {
height: 100dvh;
}
p {
color: navy;
}
p:last-child {
color: yellow;
}
@media (max-width: 768px){
h1 {
color: black;
}
}
```
---
## 7.7 Best practices
* **Use `$variables`** for colors, spacing, and fonts; this makes theming and dark/light modes easy.
* Define global theme variables in `:root`.
* Use selector chains and grouping for compact, readable code.
* Put `@media` right inside the block; Phlo hoists it to the right place.
* Use `\` in nestings to glue pseudos or attributes to the parent selector.
* No semicolons in your code; Phlo produces correct CSS output.
---
## 7.8 Icon sprites
Point `icons` in `data/app.json` at one or more folders of PNG files and the build composes them into a single `www/icons.png` sprite plus the CSS to use them:
```json
{
"icons": "%app/icons/",
"iconNS": "app"
}
```
Naming convention: `save.png` becomes class `.icon.save`; `save.dark.png` becomes the same class scoped to `body.dark`, so one icon name can have per-context variants (themes, states). Usage in a view:
```phlo
```
The generated CSS lands in the `iconNS` bundle (default `app`) and `view()` preloads the sprite automatically.
---
**Reference.** The DOM resources are documented per node in the [Manual](/manual/dom), generated from the resource files so it never drifts from the code.
---
# 8. ORM
Phlo ships with a powerful built-in **ORM** that lets you define database tables as classes.
Models can be defined quickly via `columns` or in full via a declarative `schema`.
Records are treated as **instances**, with support for props, methods, views, relations and multiple database engines.
---
## 8.1 Basics
An ORM model is a `.phlo` file with:
* `@ class:` the name of the model (and table)
* `@ extends: model`
* `static table` and `columns` or `schema`
* Optional: relations (`parent`, `child`, `many`)
* Props, methods and views operate per **record instance**
Example:
```phlo
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static columns = 'id,name,email,active,created'
```
---
## 8.2 Defining models
### 7.2.1 Flat with `columns` (quick and lightweight)
Use `columns` for simple tables:
```phlo
@ class: shipment
@ extends: model
view: $this->destination ($this->user)
static table = 'shipments'
static order = 'changed DESC'
static columns = 'id,user,destination,costs,valid,weight,shipped,created,changed'
static objParents = ['user' => 'user']
```
```phlo
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static order = 'changed DESC'
static columns = 'id,name,email,level,active,created,changed'
static objChildren = ['shipments' => 'shipment']
```
---
### 7.2.2 With `schema` and `field(...)` (rich and declarative)
With `schema` you define fields, relations and UI in one place:
```phlo
@ class: shipment
@ extends: model
view: $this->destination ($this->user)
static table = 'shipments'
static schema => arr (
id: field (type: 'token', length: 4, title: 'ID'),
destination: field (type: 'text', required: true, search: true),
user: field (type: 'parent', obj: 'user', required: true),
costs: field (type: 'price', prefix: '€ '),
valid: field (type: 'bool'),
attachments: field (type: 'child', obj: 'attachment', list: true),
)
```
```phlo
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static schema => arr (
id: field (type: 'token'),
name: field (type: 'text', search: true, required: true),
email: field (type: 'email', required: true),
shipments: field (type: 'child', obj: 'shipment'),
groups: field (type: 'many', obj: 'group', table: 'user_groups'),
)
```
> `schema` is especially powerful in combination with PhloCMS, but works standalone too.
---
## 8.3 CRUD
Fetching:
```phlo
$user = user::record(id: 1)
$list = shipment::records(order: 'created DESC')
```
Creating:
```phlo
$shipment = shipment::create(destination: 'Paris', user: 1)
```
Editing and saving:
```phlo
$shipment->destination = 'Lyon'
$shipment->objSave
```
Deleting:
```phlo
shipment::delete('id=?', $shipment->id)
```
* `record(...)` → single record (or null)
* `records(...)` → array of records (class instances)
* `create(...)` → insert + instant fetch
* `objSave` → save the instance (insert/update depending on id)
* `delete(...)` → static delete with an SQL where clause
---
## 8.4 Relational navigation
Relations are available as properties:
| Type | Declaration | Usage |
| ------ | -------------- | ------------------ |
| parent | `type: parent` | `$shipment->user` |
| child | `type: child` | `$user->shipments` |
| many | `type: many` | `$user->groups` |
### Many-to-many
`type: many` uses a pivot table:
```phlo
groups: field (
type: 'many',
obj: 'group',
table: 'user_groups',
)
```
Navigation:
```phlo
$user = user::record(id: 1)
foreach ($user->groups as $group)
echo $group->title
```
Relations are **batch-loaded** for performance. There are no cross-DB joins; each class loads from its own engine.
---
## 8.5 Instance dynamics
Every record is a **real instance** of your model class.
You can use props, methods and views to add virtual fields, computations or representations:
```phlo
@ class: shipment
@ extends: model
prop summary => $this->destination.' ('.$this->user.')'
method tax => $this->costs * 0.21
view:
$this->summary
$this->tax
```
Usage:
```phlo
$shipment = shipment::record(id: 'AB12')
echo $shipment->summary
echo $shipment
```
Using a record as a string invokes its view representation.
Props and methods always operate on the **record instance**, never statically.
---
## 8.6 Filtering and queries
All query methods accept named arguments and SQL-like filters:
```phlo
shipment::records(destination: 'Paris')
shipment::records(where: 'valid=1 AND weight>10')
shipment::pair(columns: 'id,destination')
```
Supported: `where`, `order`, `group`, `joins`, caching and schema-aware columns.
---
## 8.7 Caching and performance
The ORM uses internal buffers (`objRecords`, `objLoaded`) for relational lookups and optional **APCu caching** via:
```phlo
static objCache = true
```
Or a number of seconds:
```phlo
static objCache = 600
```
Records and relations are loaded in batches.
Use `records()` for bulk selections instead of `record()` in loops.
---
## 8.8 Multiple engines
Phlo is database-agnostic: a model resolves its engine from `static DB`, and there is no implicit default. Set the engine once for the whole app by injecting it onto the model base from `app.phlo`:
```phlo
static %model.DB => %MySQL
```
Override it for a single model with its own `static DB`.
### SQLite
```phlo
@ class: notes
@ extends: model
static DB => %SQLite(data.'notes.db')
static table = 'notes'
static columns = 'id,title,body'
```
### PostgreSQL
```phlo
@ class: invoices
@ extends: model
static DB => %PostgreSQL
static table = 'invoices'
static columns = 'id,customer_id,total,created'
```
> Tables on different engines can be combined in relations; each class fetches its own data.
---
### `/data/creds.ini`
For engines such as MySQL and PostgreSQL, place your credentials in:
```
/data/creds.ini
```
```ini
[mysql]
host = localhost
database = db_name
user = db_user
password = db_password
[postgresql]
host = localhost
database = my_pg_db
user = pg_user
password = pg_pass
```
Phlo loads these automatically via `%creds->...`.
---
## 8.9 Opt-in features: audit, validation, custom PK
An `@ extends: model` gives you CRUD + identity map + relations out of the box. Three additional opt-ins are enabled per model with a static flag.
### 8.9.1 Audit log
```phlo
static objAudit = true
```
From then on, every `create`, `objSave` (update) and `delete` is logged to an audit table via the `security/audit` resource:
| Operation | What gets logged |
| --- | --- |
| `create(...)` | full new values |
| `objSave` (update) | diff: only changed fields, `from` → `to` |
| `delete(...)` | full old values, per affected record |
Setup:
1. Add `security/audit` to the resources in `data/app.json`.
2. Import the schema SQL once: `mysql < /opt/phlo/resources/security/audit.sql`.
Excluding sensitive fields:
```phlo
method afterCreate => %audit->log($this, 'create', [], (array)$this, exclude: ['password_hash'])
```
Toggle per environment, dev only:
```phlo
static objAudit => debug
```
Or release only:
```phlo
static objAudit => !debug
```
(`debug` is the runtime constant from `phlo_app(debug: ...)`.)
Reading the log. The `security/audit` resource also exposes a small query API, with the model as the first argument:
| Call | Returns |
| --- | --- |
| `%audit->history($model, $recordId, $limit = 50)` | every change to one record, newest first |
| `%audit->byUser($model, $userId, $fromTs = 0, $limit = 100)` | every change a user made since `$fromTs` |
| `%audit->purge($model, $olderThanSeconds = 31536000)` | delete entries older than the cutoff, for retention |
```phlo
$rows = %audit->history(invoice, $invoiceId)
```
Each `audit_log` row carries `ts`, `user` (the `%session->user` at the time of the change, or null), `model`, `record_id`, `action`, `ip`, and a `changes` JSON blob whose shape follows the action: a `create` stores the full new row, an `update` stores only the changed fields as `{"col": {"from": ..., "to": ...}}`, and a `delete` stores the full old row.
### 8.9.2 Validation
```phlo
static objValidate = true
```
Before `create()`, Phlo runs the matching `objValidate($value)` for each field in `static schema()`. On errors: `create()` returns `null`, with errors available via `Class::objErrors()`:
```phlo
if (!user::create($args)){
return apply(errors: user::objErrors())
}
```
Field rules in `schema()`:
```phlo
static schema => arr (
email: field (type: 'email', required: true),
name: field (type: 'text', length: 100, required: true),
slug: field (type: 'text', pattern: '^[a-z0-9-]+$'),
status: field (type: 'text', enum: ['draft', 'sent', 'paid']),
)
```
Custom field validation: override `method objValidate($value)` in your own field subclass.
### 8.9.3 Custom primary key
The default is `id` (auto-increment integer). Override it for other PKs:
```phlo
static idColumn = 'sku'
static idType = 'string'
```
Effect:
- The identity map uses `sku` as its key
- `Class::record(sku: 'XYZ-123')` (not `id:`)
- With `create()`: you supply the PK value yourself (no auto-increment)
- `$record->id` does not work, use `$record->sku`
### 8.9.4 Combining
The three opt-ins are independent and can be combined:
```phlo
@ class: giftcard
@ extends: model
static objAudit = true
static objValidate = true
static idColumn = 'sku'
static idType = 'string'
```
Every feature is off by default (`false`, `'id'`, `'int'`). A model without opt-ins stays a plain model.
---
## 8.10 Function overview
| Function / Property | Type | Description |
| ------------------------------- | ----------- | --------------------------------- |
| `record(...)` | static | Fetches 1 record (or null) |
| `records(...)` | static | Fetches multiple records (array) |
| `create(...)` | static | Insert + fetch |
| `objSave` | instance | Insert or update |
| `delete(where, …)` | static | Delete |
| `pair`, `item`, `column` | static | Quick query helpers |
| `objParents` / `schema: parent` | declarative | Parent relations |
| `objChildren` / `schema: child` | declarative | Child relations |
| `schema: many` | declarative | Many-to-many |
| `objCache` | static | Optional APCu caching |
| `static DB` | static | Per-model engine |
---
## 8.11 Best practices
* Use `columns` for quick, simple models.
* Use `schema` for rich definitions and CMS integration.
* Define a `view` for string representations.
* Use props for virtual fields.
* `objSave` instead of `save`.
* Use `records()` for bulk loads.
* Separate models per engine with `static DB`; set the app-wide default with `static %model.DB`.
* Keep credentials in `/data/creds.ini`.
* Keep models declarative; put logic in props/methods.
---
**Reference.** The database package are documented per node in the [Manual](/manual/db), generated from the resource files so it never drifts from the code.
---
# 9. Instance Management
Phlo uses its own **instance manager** to initialize and reuse objects efficiently and predictably. This system determines **when controller code runs**, how instances are stored, and how circular references are prevented.
---
## 9.1 The basics
When you define a `.phlo` file, the build phase turns it into a class.
Every call to an object via `%name` goes through the **instance manager** (`phlo()` in `/phlo/phlo.php`).
Example:
```phlo
prop title = 'Welcome'
route GET home => $this->main
method main => view($this->home)
view home:
$this->title
```
When the route `/home` is requested:
1. The instance manager checks, via the identity that `__handle()` (or the default rule) derives from the arguments, whether a matching instance already exists.
2. If not, it is **created and stored**.
3. After creation, **the controller code** runs (see §8.2).
4. Then the requested method is called.
---
## 9.2 Controller code
All code in a `.phlo` file that does **not** belong to `route`, `prop`, `static`, `method`, `function`, `view`, `