# 1. Introduction

Phlo is an integrated platform with its own full-stack language. You write `.phlo` source files; Phlo compiles 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, phloWS, phloWA) and the operations platform (the Phlo Dashboard). This guide covers all of them. The production release runs on a shared runtime, usually in `/srv/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 `/srv/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.

---

## 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 /srv/phlo
php /srv/phlo/install.php /srv/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 /srv/phlo/install.php /srv/example.nl/ && cd /srv/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
/srv/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
<?php
require('/srv/phlo/phlo.php');
phlo_app (
	id: 'Example',
	host: 'dev.example.nl',
	auth: true,
	build: true,
	debug: true,
	app: '/srv/example.nl/',
	data: '/srv/example.nl/data/',
);
```

Release entrypoint in `release/www/app.php`:

```php
<?php
require('/srv/phlo/phlo.php');
phlo_app (
	id: 'Example',
	host: 'stage.example.nl',
	app: '/srv/example.nl/release/',
	php: '/srv/example.nl/release/',
	data: '/srv/example.nl/data/',
);
```

---

## 1.5 Build

Use the CLI from the dev entrypoint:

```bash
php www/app.php build::run
php www/app.php build::lint
php www/app.php build::release
```

`build::run` transpiles the dev output, `build::lint` checks the generated PHP, and `build::release` writes the release output.

---

## 1.6 Web server

The web server points to `www/` for dev and to `release/www/` for stage/production. Unknown paths are rewritten to `app.php`.

For FrankenPHP (recommended), a single Caddyfile block is enough:

```caddy
example.nl {
	root * /srv/example.nl/release/www
	php_server
}
```

For Nginx:

```nginx
root /srv/example.nl/release/www;

location / {
	try_files $uri $uri/ /app.php?$query_string;
}
```


---

# 2. Configuration

`data/app.json` describes the build: which resources are loaded, where release output goes, and which resources belong only in release or only in dev.

---

## 2.1 Minimal configuration

```json
{
    "resources": [
        "security/creds",
        "security/security",
        "security/token",
        "payload",
        "session",
        "tag",
        "phlo.async",
        "DOM/form"
    ],
    "release": "%app/release/"
}
```

Resources refer to the Phlo runtime catalog. That is framework code, not a place for app-specific files. You write app code as `.phlo` in the app path; only generic runtime functionality belongs in the catalog, deliberately.

---

## 2.2 Resources

Commonly used resources:

| Resource | Purpose |
| --- | --- |
| `security/creds` | Credentials from env and ini files |
| `security/security` | Security headers |
| `security/token` | Token generation |
| `payload` | Read POST, PUT, PATCH and uploads |
| `session` | Session object |
| `cookies` | Cookie object |
| `DOM/form` | Async forms |
| `phlo.async` | Async frontend requests |
| `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.

---

## 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 `<style>` and `<script>` block compiles into a per-namespace bundle. `ns=docs` lands in `www/docs.css` and `www/docs.js`, `ns=app,docs` in both bundles, and blocks without `ns=` in the default namespace. A page selects its bundle with `view(..., ns: 'docs')`.

Three keys in `data/app.json` control this:

```json
{
    "defaultNS": "app,docs",
    "phloNS": ["app", "docs"],
    "iconNS": "app"
}
```

- `defaultNS` (default `"app"`): the namespace(s), comma-separated, for assets without an explicit `ns=`. Resource assets (frontend helpers such as `onExist`, the cookiewall styles) carry no `ns=`, so when your pages use multiple namespaces, widen `defaultNS` so every bundle gets them.
- `phloNS` (default `["app"]`): the namespaces whose JS bundle embeds the phlo.js runtime. Every namespace whose pages load standalone needs the runtime, so list them all here. `phloJS: true` inverts the list: the runtime then goes into every namespace NOT in `phloNS`.
- `iconNS` (default `"app"`): the namespace that receives the generated icon-sprite CSS when the `icons` engine is used.

Two rules keep a multi-namespace app healthy:

1. Never work around a missing runtime by passing `defer: '/app.js'` to `view()`. On async navigation that re-injects the bundle into a page that already has one and crashes with duplicate declarations. Configure `phloNS` instead.
2. Two runtimes must never meet in one page. Links that cross namespaces (an `app` page linking to a `docs` page) must be plain links, so the browser does a full page load. Only links within the same namespace get `class=async` for SPA navigation.

---

## 2.7 Generated output

Do not edit these files by hand:

```text
php/
www/app.js
www/app.css
release/
```

Change the `.phlo` source, run `build::run`, check with `build::lint`, and then create a release with `build::release`.

---

## 2.8 Credentials

The `security/creds` resource resolves secrets (API keys, database logins, webhook tokens) and exposes them as `%creds->...`. It reads from two sources, so the same code runs on a laptop and in production without edits.

The INI file `data/creds.ini` is the simplest source. Keep it out of version control. A simple secret is a top-level key; a structured one is a section with subkeys:

```ini
OpenAI = sk-...
Claude = sk-ant-...
Grok = xai-...

[mysql]
host = 127.0.0.1
database = app
user = app
password = secret
```

You read them as `%creds->OpenAI` (a scalar) and `%creds->mysql->host` (nested).

Environment variables provide the same values without a file, which suits CI and containers. The prefix `PHLO__` marks a credential, and `__` separates nesting levels:

```text
PHLO__OpenAI=sk-...
PHLO__mysql__host=127.0.0.1
```

A host-scoped form, `PHLO_<HOST>__...`, applies only on a matching host. `<HOST>` is the request host uppercased with every non-alphanumeric turned into `_`, so `factuur.software` becomes `FACTUUR_SOFTWARE`:

```text
PHLO_FACTUUR_SOFTWARE__OpenAI=sk-...
```

Sources are merged in order, each overriding the previous: `data/creds.ini` first, then `PHLO__` globals, then host-scoped `PHLO_<HOST>__` on top. So a host-scoped variable wins over a global one, which wins over the ini file.

A resource declares what it needs with `@ requires: creds:<name>` (for example `creds:OpenAI`, `creds:mysql`). That line is informational: it documents the key, it does not create it. Values are stored as sensitive, so `%creds` masks them in debug output.


---

# 3. Runtime config

`data/app.json` describes the **build**, what gets compiled. This chapter covers the **runtime**, what happens on each request. That config lives in `www/app.php` (and, for stage/release, `release/www/app.php`) as arguments to `phlo_app(...)`.

```php
<?php
require('/srv/phlo/phlo.php');
phlo_app (
	id:        'Example',
	host:      'dev.example.nl',
	auth:      true,
	build:     true,
	debug:     true,
	app:       '/srv/example.nl/',
)
```

Every key is a **named argument** that also becomes a **PHP constant**, available anywhere in your app as a bare word (`host`, `data`, `composer`, etc.). The same applies to your own custom keys.

---

## 3.1 Identity and host

| Key | Default | Purpose |
| --- | --- | --- |
| `id` | none | Free-form name for the app, used by the Phlo Control Center, the Phlo Dashboard and logs |
| `host` | `null` | Vhost this entrypoint is allowed to run on. Phlo rejects requests that do not match, no accidental cross-host responses |

---

## 3.2 Modes: `build`, `debug`, `auth`, `thread`

| Key | Default | Effect |
| --- | --- | --- |
| `build` | `false` | Enables the build/lint/reflect CLI and lets Phlo detect and recompile changed sources per request |
| `debug` | `false` | Loads `debug.php`, activates `debug()` / `dx()` / debug helpers, gives full stack traces instead of generic 500s |
| `auth` | `false` | Site-wide HTTP Basic auth using credentials in `data/auth.ini` |
| `thread` | `false` | Worker mode (see X.7), `true` = unlimited, integer = number of requests per worker |

**Do not combine**: `build: true` and `thread: true`. Build writes files between requests; in a long-running worker that is unsafe. Phlo throws a runtime error if both are on.

**Requires**: `auth: true` needs `build: true`. Phlo errors out at startup if auth is set without build. (This is an implementation detail: the site-wide auth handler ships in the build layer. To protect a non-build stage host, put HTTP Basic auth in your web server in front of Phlo.)

**Control Center path**: when `build: true` and `debug: true`, the Phlo Control Center auto-mounts at `/phlo`, no config needed. Use the optional `control:` key to mount it on a different path (`control: 'admin'` serves it at `/admin`) or set `control: false` to switch it off. The older `dashboard:` key did the same job and still works for now, but `control:` is the current name. Outside build+debug it is off regardless.

---

## 3.3 Paths

Only `app` is required. The rest falls back to subdirectories of `app`:

| Key | Default | Intended for |
| --- | --- | --- |
| `app` | (required) | App root |
| `data` | `<app>/data/` | Config (`app.json`, `auth.ini`), credentials, runtime state |
| `php` | `<app>/php/` | Generated PHP, only change this when release output lives elsewhere |
| `www` | `<app>/www/` | Web root |

Your release entrypoint typically sets `app: '<app>/release/'` and `php: '<app>/release/'` so release output is served without build mode.

---

## 3.4 Control Center and WebSocket

| Key | Default | Effect |
| --- | --- | --- |
| `control` | `'phlo'` with `build`+`debug`, otherwise `false` | URL prefix the control UI lives under. For example `'beheer'` → `/beheer`; `false` = off |
| `websocket` | `null` | Port phloWS runs on for this app. Becomes the constant `websocket`, used by `wsCast()` |

The control UI requires `build: true`. See the WebSocket chapter for phloWS setup.

---

## 3.5 Composer autoload

Want to use PHP packages from `vendor/`? Provide the path:

```php
phlo_app (
	composer: '/srv/example.nl/',
	...
)
```

Phlo then registers a **lazy autoloader** that only loads `<composer>/vendor/autoload.php` when an unknown class needs to be resolved. No cold-start cost if you never touch Composer packages; full Composer autoload as soon as you do.

Convention: `composer: '<app>/data/'` (the `composer.json` and `vendor/` then live in `data/`, outside the webroot).

---

## 3.6 Trace and CLI

| Key | Default | Effect |
| --- | --- | --- |
| `trace` | `false` | Enables trace mode, see the Trace chapter |
| `cli` | `'php-zts'` (if ZTS) or `'php'` | Path to the PHP binary Phlo uses for subprocesses (build, tasks, websocket). Override this if your system has non-standard PHP binaries |

> **`cli` is a string in v4** (a path), not a boolean like in v1.

---

## 3.7 Worker mode rules

`thread: true` puts Phlo in long-running worker mode (FrankenPHP, ReactPHP, RoadRunner). The runtime stays in memory between requests. Three rules you need to know:

**1. No `die()` or `exit()` in the HTTP path.** Both kill the entire worker. Use `return` or let a terminating call (`view()`, `apply()`, `location()`) send the response.

**2. No request state in static properties.** Statics survive between requests in a worker; user data from request A leaks into request B. For caches that are worker-safe (class structure, computed metadata), statics are fine, not for session, user, payload, time or DB state.

**3. Mark long-lived objects with `$objPers = true`.** By default Phlo clears its instance map between requests. For objects you explicitly want to reuse (DB connection, prepared statements), set `$this->objPers = true` so the cleanup leaves them alone.

---

## 3.8 Custom keys: your own constants

Every extra named argument you pass to `phlo_app()` automatically becomes a PHP constant. That is the way to declare app-wide paths or feature flags:

```php
phlo_app (
	app:      '/srv/example.nl/',
	langs:    '/srv/example.nl/langs/',
	files:    '/srv/example.nl/files/',
	uploads:  '/srv/example.nl/data/uploads/',
)
```

Directly usable in `.phlo` afterwards:

```phlo
$dict = parse_ini_file(langs.'en.ini')
$path = files.'avatars/'.$user->id.'.jpg'
$url  = uploads
```

`reflect::runtime` shows all defined constants. Useful for discovering what an app provides without opening `www/app.php`.

---

## 3.9 Example: dev and release side by side

```php
phlo_app (
	id:        'Example',
	host:      'dev.example.nl',
	auth:      true,
	build:     true,
	debug:     true,
	trace:     true,
	app:       '/srv/example.nl/',
	composer:  '/srv/example.nl/data/',
	websocket: 3001,
	langs:     '/srv/example.nl/langs/',
)
```

```php
phlo_app (
	id:        'Example',
	host:      'example.nl',
	thread:    true,
	app:       '/srv/example.nl/release/',
	php:       '/srv/example.nl/release/',
	data:      '/srv/example.nl/data/',
	composer:  '/srv/example.nl/data/',
	websocket: 3001,
	langs:     '/srv/example.nl/langs/',
)
```

Dev has build, debug, auth, the Control Center and trace. Release has worker mode (`thread`) and points the webroot/php output to `release/`. `data`, `composer`, `langs` stay the same, that is shared state.

---

## 3.10 The request and response objects

Two runtime objects carry every request. `%req` (read) and `%res` (write) are always available.

**%req** computed properties:

| Property | Contains |
| --- | --- |
| `%req->method` | HTTP verb, uppercased |
| `%req->path` | Request path without leading slash |
| `%req->part($i)` | Path segment by index |
| `%req->query` | Parsed query string array |
| `%req->async` | `true` when the request comes from phlo.js (SPA navigation, async forms) |
| `%req->cli` | `true` when running from the command line |
| `%req->secure`, `%req->scheme`, `%req->base`, `%req->url` | URL parts, computed once |
| `%req->referer`, `%req->acceptLanguage` | Common headers, normalized |

**%res** surface:

| Member | Does |
| --- | --- |
| `%res->header($key, $value)` | Queue a response header (sent on render) |
| `%res->type` | Content-Type for the response |
| `%res->text($body)` / `%res->json(...)` / `%res->xml($body)` | Set the body (and type for json/xml); chainable |
| `%res->render($code = null)` | Send status, headers and body; marks the response done |
| `%res->streaming` | `true` switches `apply()` to immediate flush-per-command (see the WebSocket chapter) |
| `%res->status`, `%res->done` | Status code; whether output was already sent |

`output($content, $filename = null, $attachment = null, $file = null, $code = null, $type = null)` is the response function for files, blobs and JSON-with-a-status: it serves a file (mime by name, optional `attachment`), or JSON when `$content` is an array (`output(['id' => $id], code: 201)`, `output(['error' => 'not found'], code: 404)`); `type` overrides the content-type for a pre-encoded string body. `view()`/`apply()`/`output()`/`error()`/`location()` are the response functions app code uses; the `%res->json/text/xml/render` members above are the low-level primitives they build on, for the rare hand-assembled response (custom headers, a special content-type). Do not wrap them in per-app `jsonOut()`/`respond()` helpers.

> **Lesson.** `die($content)` looks like it "sends a response", but it bypasses `render()`: queued headers (including Content-Type) never leave the server, and in worker mode `die()` kills the whole worker. This site served its machine-readable endpoints as `text/html` for weeks that way. Always end with `output(...)`, `view()`, `apply()`, `location()` or an explicit `%res->...->render()`.

Runtime errors land in `data/errors.json` with message, source-mapped `.phlo` file and line, the host, a counter and the last occurrence (de-duplicated per host + location + message). Read them with `reflect::errors [limit]` or in the Phlo Control Center.


---

# 4. Syntax & Structure

Phlo compiles `.phlo` source files to plain PHP classes and generated assets. The syntax is PHP-like, but without semicolons at the end of statements.

---

## 4.1 File structure

A `.phlo` file can contain top-level controller code and nodes:

* `route`
* `function`
* `prop`
* `static`
* `method`
* `view`
* `<style>`
* `<script>`

Example:

```phlo
route both GET home => view($this)

prop title = 'Welcome'

view:
<h1>$this->title</h1>
```

---

## 4.2 Statements and the line parser

Statements end at a line break, not at a semicolon:

```phlo
$count = 1
if ($active) $count++
```

This works because the compiler appends a `;` to **every line** and only removes it where the line clearly continues. The full rule set:

- Every line gets a `;` at the end.
- That `;` is removed when the line ends with `(`, `[`, `{`, `}`, `,` or `.`. These are **implicit continuations**: an opening bracket, a trailing comma in an argument list, or a concatenation that breaks after the `.`.
- A line that ends with a single backslash `\` is an **explicit continuation**: the parser removes the backslash and the semicolon and joins the line with the next one. Use this for anything that does not fall under the implicit set, such as a multiline ternary:

```phlo
$label = $visitors === 1 \
	? 'visitor' \
	: 'visitors'
```

- Empty lines never get a `;`.

The mental model: stop thinking about semicolons entirely. Only ask "is my statement complete on this line?" If not, end the line with one of `( [ { , .` (which happens naturally in most multiline code) or with an explicit `\`.

Without an implicit or explicit continuation, every line becomes its own statement. That is why in multiline calls every argument line must end with a comma, including the last one:

```phlo
apply (
	title: 'Done',
	main: '<p>Ready</p>',
)
```

> **Lesson.** The file-level node parser tracks multiline node bodies by counting parentheses, not square brackets. A multiline `prop x => [ ... ]` ends the node after its first line and the remaining lines become stray controller code (`Controller must be in one place`). Open multiline node bodies with a parenthesis: `prop x => arr(...)` or `prop x => array_merge(...)`.

---

## 4.3 Controller code

Top-level code that is not a node becomes controller code of the generated class. That code runs when the instance is first retrieved.

```phlo
prop ready = false

$this->ready = true
```

Use controller code for light initialization. Put request logic in routes and methods instead.

---

## 4.4 Instances

Use `%name` to retrieve a Phlo object through the instance manager:

```phlo
%payload->name
%session->user
%creds->mysql->database
```

The instance is created lazily and then reused within the request.

> **Lesson.** The compiler rewrites `%name` EVERYWHERE in a `.phlo` file, including inside string literals. A page that tried to print the literal text `%session` in a code example shipped `phlo('session')` to its visitors. Example code that must stay verbatim belongs in external files (`.txt`, `.md`) loaded at runtime, never in `.phlo` string literals.

---

## 4.5 Props

Static prop:

```phlo
prop title = 'App'
```

Computed prop:

```phlo
prop fullName => $this->first.' '.$this->last
```

Computed props are generated as getters and can be read as a property:

```phlo
$this->fullName
```

---

## 4.6 Methods

Single-line method:

```phlo
method label($value) => ucfirst($value)
```

Multiline method:

```phlo
method label($value){
	if (!$value) return void
	return ucfirst($value)
}
```

---

## 4.7 Functions

You define global functions with `function`:

```phlo
function initials($name){
	return strtoupper(substr($name, 0, 1))
}
```

Use this sparingly. For app code a regular app class is usually better; only framework-wide helpers belong in a runtime resource.

---

## 4.8 Statics

Static value:

```phlo
static table = 'users'
```

Static method:

```phlo
static label($value) => ucfirst($value)
```

Call:

```phlo
user::label('jordi')
```

---

## 4.9 Strings and operators

Strings and operators follow PHP:

```phlo
$name = 'Jordi'
$title = "Hello $name"
$active = $count > 0 && !$archived
```

Use `void` for an empty string when that makes the intent clear.

---

## 4.10 Named arguments

Named arguments work as in PHP and keep calls readable:

```phlo
%DB->load (
	table: 'users',
	where: 'active=1',
	order: 'name',
)
```

---

## 4.11 Error handling and JSON responses

`error('message', $code = 500)` aborts a request with a status. It throws (no `return` needed), and the engine renders it to fit the request:

- an async/SPA request gets `apply(error: 'message')` (phlo.js shows it);
- a JSON context (the route called `%security->api`, or the client sent `Accept: application/json`) gets a JSON body `{"error": "message"}` with the status code;
- otherwise the HTML error page (the full debug page when `debug: true`, a minimal page otherwise).

```phlo
if (!$record) error('Record not found', 404)
```

Client errors (`$code < 500`) keep their message; server errors (`>= 500`) stay generic (`"Error"`) unless `debug: true`, so uncaught-exception internals are not exposed by default.

For a successful JSON response use `output($data, code:)`: an array is encoded automatically and `code` sets the status. A JSON API route therefore uses `output()` for results and `error()` for failures, with no per-app response wrapper.

`debug: true` (set in `www/app.php`) enables verbose debug output; runtime errors are logged to `data/errors.json`.


---

# 5. Routing

Routing in Phlo maps a **space-separated path** + **HTTP method** to a **target** (usually a method).
Routes from all `.phlo` files are collected; the router is activated with `app::route()`.

---

## 5.1 Basic form

```
route [async|both] [GET|POST|PUT|DELETE|PATCH] pad [pad2 ...] => target
```

* **Paths** are written with **spaces** (no `/` in the route definition).
* **Target**: a direct call or statement block.

Example:

```phlo
route GET home => $this->main
method main => view($this->home)
```

---

## 5.2 sync / async / both

| Keyword      | Behavior                                       |
| ------------ | ---------------------------------------------- |
| *(omitted)*  | **Sync** only (regular HTTP)                   |
| `async`      | **Async** only (requests from a Phlo frontend) |
| `both`       | Sync **and** async allowed                     |

```phlo
route both GET data => $this->loadData
route async POST items save => $this->saveItems
```

---

## 5.3 Variables

Phlo parses every path segment. Segments that start with `$` are **variables** with extra capabilities:

### 4.3.1 Required (passed to the target)

```phlo
route GET user $id => $this->showUser($id)
method showUser($id) => view($this->profile)
```

### 4.3.2 **Optional presence** with `?`  → **boolean**

```phlo
route GET search $full? => $this->search($full)
```

* Matches `/search` **and** `/search/full`.
* `$full` is **true** when the segment `full` is present, otherwise **false**.
  (The implementation literally checks whether the request segment equals the **name** without the `?`.)

### 4.3.3 **Rest** (variable length) with `=*`

```phlo
route GET file $path=* => $this->serveFile($path)
```

* Matches all remaining segments as **one** string in `$path`.

### 4.3.4 **Default value** with `=`

```phlo
route GET page $slug=home => $this->page($slug)
```

* Without a segment → `$slug = 'home'`.
* With a segment → `$slug = '<value>'`.

### 4.3.5 **Length requirement** with `.N`

```phlo
route GET code $pin.6 => $this->enter($pin)
```

* Matches only when the length of `$pin` is exactly **6**.

### 4.3.6 **Value lists** with `:a,b,c`

```phlo
route GET report $range:daily,weekly,monthly => $this->report($range)
```

* The segment must be one of the **listed values**.
* When the segment is absent (empty) **and** a default is set, the default is applied.
* Otherwise the match fails.

You can **combine** these forms. Examples:

Enum with a required id:

```phlo
route GET export $fmt:csv,json $id => $this->export($fmt, $id)
```

Enum with a default:

```phlo
route GET theme $name:light,dark=light => $this->theme($name)
```

---

## 5.4 Payload check with `@`

You specify **exact** body keys with a single `@` and a **comma-separated** list.
The router compares this **1-to-1** with the keys from `%payload` (exact set; order as supplied by the engine).

```phlo
route POST user @name,email => $this->createUser

method createUser => dx(%payload->name, %payload->email)
```

> Body keys are **not** bound as method parameters; you read them via `%payload`.

---

## 5.5 Targets

### Local method

```phlo
route GET profile show => $this->show
method show => view($this->profile)
```

### External class method (static)

```phlo
route GET api version $major => api::getVersion($major)
```

* Static calls: **parentheses required**, even without arguments.
* Pass path variables explicitly.

### What a route may return

The dispatcher inspects the return value for exactly one thing: `false` means "not my route" and matching continues with the next candidate. **Every other return value is discarded.**

> **Lesson.** The assumption "a route returns its response body" produces `route GET hello => 'Hello'`: the route matches and serves a 200 with an empty page. A route produces output exclusively through `view()`, `apply()`, `output()`, `location()` or the `%res` API.

The `false` contract is also a tool: a catch-all `route GET guide $slug` that returns `false` for unknown slugs lets a literal route in a later file (`GET guide index.json`) still match the same URL.

---

## 5.6 Activating the router

Routes are only matched **after**:

```phlo
app::route()
```

Place this call, for example, in `app.phlo` (or another central controller) after your app initialization and before a fallback for 404 handling.

---

## 5.7 Recommended structure

* Put routes at the top of each file.
* Keep path and method name logically aligned:

  ```phlo
  route GET users list  => $this->listUsers
  route POST users add  => $this->addUser
  ```
* **Always** pass variables in the target.
* Use `both` only when an endpoint deliberately needs to be both sync and async.
* Use value lists `:…` instead of separate `if` branches for fixed variants.


---

# 6. Views

Views live directly in `.phlo` files. A view compiles to a PHP method that returns HTML. You render a view with `view(...)`.

One rule before everything else: **a blank line closes the view**. The first empty line after `view ...:` ends the block; HTML after it becomes controller code and the build stops with `HTML outside a view`. Never insert a blank line inside a view for visual spacing.

---

## 6.1 Declaration

Anonymous view:

```phlo
view:
<p>Test</p>
```

Named view:

```phlo
view home:
<h1>Welcome</h1>
```

View with arguments:

```phlo
view greeting($name):
<p>Hello $name</p>
```

Calling it:

```phlo
method show => view($this->greeting('Jordi'))
```

---

## 6.2 Multiline blocks

A multiline view runs until an empty line. So don't put empty lines in the middle of view HTML.

```phlo
view:
<section>
	<h1>Welcome</h1>
	<p>Intro</p>
</section>

view footer:
<footer>Phlo</footer>
```

---

## 6.3 HTML shorthand

Phlo supports compact id/class shorthand:

```phlo
view:
<p#intro.lead/>
```

This becomes:

```html
<p id="intro" class="lead"></p>
```

A trailing slash makes a tag self-closing in the source, and Phlo turns it into a normal open/close tag.

One hard rule: a tag uses EITHER the shorthand OR an explicit `class`/`id` attribute for that property, never both. Combining them can emit a duplicate attribute and the browser keeps only the first, silently dropping the dynamic one. When part of a class list is dynamic, write the whole thing as one attribute; keep shorthands for fully static tags:

```phlo
<a.site-logo href="/">                              valid: fully static
<a class="card {( $active ? 'is-active' : void )}"> valid: dynamic, one attribute
<a.card class="$extra">                             INVALID: duplicate class attribute
```

---

## 6.4 Text and variables

You can use plain variables and simple properties directly in text:

```phlo
view($name):
<p>Hello $name</p>
<p>$this->title</p>
```

For method calls, chained access, or expressions, use `{{ ... }}`:

```phlo
view:
<p>{{ $this->label('start') }}</p>
<p>{{ $this->record->title }}</p>
<p>{{ $this->count > 1 ? 'Multiple' : 'One' }}</p>
```

`{( ... )}` exists as a short expression form and is translated internally to `{{ (...) }}`, but don't use it as the default example. In documentation and app code, `{{ ... }}` is usually clearer.

---

## 6.5 Translatable view text

For static translatable text, use the language shorthand:

```phlo
view:
<h1>{nl: Welkom}</h1>
<p>{nl: Hallo wereld}</p>
```

With arguments:

```phlo
view($name):
<p>{nl: Hallo %s ($name)}</p>
```

Use the shorthand for this; it is shorter and shows at a glance which source language the text has.

---

## 6.6 Attributes

Attribute values without spaces or variables can go unquoted:

```phlo
view:
<a href=/contact>Contact</a>
```

With variables or expressions, use quotes:

```phlo
view:
<a href="$this->url">Link</a>
<a href="{{ $this->url('contact') }}">Contact</a>
```

Attribute values interpolate `$var`, `$this->prop` and `%instance->prop` directly, including with a literal suffix. Wrapping plain property access in `{{ }}` is redundant; reserve `{{ }}` for calls and `{( )}` for expressions:

```phlo
<a href="%base->view/install">       valid: direct interpolation plus suffix
<a href="{{ %base->view }}/install"> works, but redundant and ugly: avoid
```

---

## 6.7 Control flow

Use control-flow tags on their own lines:

```phlo
view:
<ul>
<foreach $this->items AS $item>
	<li>$item->title</li>
</foreach>
</ul>
```

With `if`:

```phlo
view:
<if $this->active>
	<p>Active</p>
<else>
	<p>Inactive</p>
</if>
```

---

## 6.8 Rendering

`view(...)` renders the response and stops further request handling. So build composite pages in a single view, or let a view include other view methods inline with `{{ ... }}`.

```phlo
route both GET home => view($this)

view:
<main>
	{{ $this->hero }}
	{{ $this->content }}
</main>

view hero:
<header>
	<h1>$this->title</h1>
</header>

view content:
<section>
	<p>{nl: Welkom op de site}</p>
</section>
```

All `view()` parameters are optional and named:

| Parameter | Does |
| --- | --- |
| `title` | Page title, combined with the app title via `title()` |
| `css` / `js` / `defer` | Extra assets next to the namespace bundles |
| `options` | Body class list |
| `settings` | Body `data-*` attributes |
| `ns` | Bundle namespace (default `app`; see chapter 2) |
| `path` | Browser URL; `false` keeps the current URL |
| `inline` | Embed local css/js into the HTML instead of linking |
| `bodyAttrs` / `htmlAttrs` | Extra attributes on `<body>` / `<html>` |
| `lang` | Page language |
| trailing named args | Any apply command, e.g. `scroll: 0`, `trans: 'fade'` |

App-level defaults come from `%app` props with the same names. The `<head>` is further fed by `%app->description`, `%app->viewport`, `%app->themeColor`, `%app->nonce`, `%app->head`, `%app->link` and `%app->version` (the asset cache-buster).

---

## 6.9 Apply commands

`apply()` accepts named arguments where each key is a DOM mutation or UI action. The runtime core provides the basics; resources can register extra commands via `app.mod.<name>` (such as `DOM/toasts` for `toast:` or `DOM/dialog` for `alert:`).

**DOM mutations**

| Cmd | Argument | Effect |
| --- | --- | --- |
| `inner` | `{selector: html}` | `el.innerHTML = html` |
| `outer` | `{selector: html}` | `el.outerHTML = html` |
| `before` / `after` | `{selector: html}` | Insert adjacent |
| `prepend` / `append` | `{selector: html}` | Insert inside, first/last |
| `remove` | selector or array | Remove elements |
| `attr` | `{selector: {attr: value}}` | Set/remove (null = remove) |
| `class` | `{selector: 'a b -c !d'}` | Add / remove (`-`) / toggle (`!`) |
| `value` | `{selector: value}` | Form value |
| `data` | `{selector: {key: value}}` | `el.dataset[key]` |

**App state**

| Cmd | Effect |
| --- | --- |
| `title` | `document.title` |
| `lang` | `html.lang` |
| `options` | Body classes (replaces) |
| `settings` | Body data attributes |
| `path` | `history.pushState` (URL changes without reload) |
| `trans` | View-transition class (`forward`/`backward`/...) |
| `scroll` | int (pixels) or `#anchor` |

**Assets** (once per href/src)

`css`, `js`, `defer`, add a link or script; already-loaded URLs are ignored.

**Navigation and callbacks**

| Cmd | Effect |
| --- | --- |
| `location` | Path or `true` (reload current path); an external URL does `location.assign()` |
| `call` | Call `app[name]()` after the apply |

**Meta**

| Cmd | Effect |
| --- | --- |
| `log` / `error` | `console.log` / `console.error` on the client |
| `phlo` | Server-side debug trace, logged in the browser console (debug mode) |

**Resource mods**, available once the corresponding resource is loaded:

| Cmd | Resource |
| --- | --- |
| `toast` | `DOM/toasts` |
| `alert` / `confirm` / `prompt` | `DOM/dialog` |
| `store` | `DOM/store` |
| `setvar` | `DOM/CSS.var` |
| `template` | `DOM/template` |

> **No build-time check on apply keys.** A typo (`inner` → `innr`) is silently ignored. Keep this table at hand, or consult `/srv/phlo/docs/apply-protocol.md` for the complete, up-to-date reference including edge cases and stream semantics.

Example combining multiple commands:

```phlo
route async POST item save {
	if (!$item = item::save(%payload)) return apply(
		error: 'Save failed',
		class: ['[name=title]' => '!error'],
	)
	apply(
		outer: ['#item-'.$item->id => $this->itemView($item)],
		toast: 'Saved',
		scroll: '#item-'.$item->id,
		trans: 'fade',
	)
}
```


---

# 7. CSS

Phlo uses a compact, semicolon-free CSS syntax inside `<style>` blocks.
You write rules with **colons** as separators:

* `selector: property: value`
* nested: `A: B: property: value`  → output: `A B { property: value; }`

**Rules:**

* **One line = one declaration.** No semicolons in the source; the engine adds them.
* **The colon `:`** separates chain levels as well as property from value.
* **A backslash `\` in nestings** "glues" the **next selector part** to the parent (glue). That next part can be a pseudo (`:…`), an attribute selector (`[…]`), and so on.
* **`@media (…)` may appear inside a selector block**; Phlo **hoists** it to the right place while preserving the current selector.

---

## 7.1 `<style>` block

```phlo
<style>
html: height: 100dvh
body {
  background: #947b6c
  font-family: Sans-serif
  p: line-height: 2em
}
</style>
```

Output (conceptually):

```css
html { height: 100dvh; }
body { background: #947b6c; font-family: Sans-serif; }
body p { line-height: 2em; }
```

A `<style>` block can target one or more bundle namespaces with `ns=`: `<style ns=docs>` compiles into `www/docs.css`, `<style ns=app,docs>` into both bundles, and a block without `ns=` into the `defaultNS` from `data/app.json`. See chapter 2 for the namespace model. The same attribute works on `<script>` blocks.

---

## 7.2 Chains & groups

Chain with colons; group with commas, and the **full context** is applied to each item.

```phlo
<style>
body: h1, p: \:first-letter: color: green
</style>
```

* 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
<style>
h1 {
  color: white
  @media (max-width: 768px): color: black
}
</style>
```

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
<style>
:root {
  $background: #0d0d0d
  $surface: #1a1a1a
  $text: #ffffff
  $accent: #ff4a00
}

body {
  background: $background
  color: $text
}

button {
  background: $accent
  color: $text
}
</style>
```

**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.<name>`.

### Example

```phlo
<style>
:root {
  $background: #0d0d0d
  $text: #ffffff
}
</style>

<script>
app.var.background = '#000000'
const textColor = app.var.text
</script>
```

* `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
<i.icon.save/>
```

The generated CSS lands in the `iconNS` bundle (default `app`) and `view()` preloads the sprite automatically.


---

# 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:
<p>$this->summary</p>
<p>$this->tax</p>
```

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 supports multiple backends via `prop DB`.
The default is `%MySQL`, but you can set any engine per model.

### SQLite

```phlo
prop DB => %SQLite(data.'users.db')
```

```phlo
@ class: notes
@ extends: model

prop DB => %SQLite(data.'notes.db')
static table = 'notes'
static columns = 'id,title,body'
```

### PostgreSQL

```phlo
prop DB => %PostgreSQL
```

```phlo
@ class: invoices
@ extends: model

prop 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 + soft delete out of the box. Three additional opt-ins are enabled per model with a static flag.

### X.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 <database> < /srv/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: ...)`.)

### X.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.

### X.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`

### X.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             |
| `prop 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 `prop DB`.
* Keep credentials in `/data/creds.ini`.
* Keep models declarative; put logic in props/methods.


---

# 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:
<h1>$this->title</h1>
```

When the route `/home` is requested:

1. The instance manager checks whether an instance of this class 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`, `<style>` or `<script>` is **controller code**.
This code runs **after instantiation**, once the instance fully exists.

Example:

```phlo
prop ready = false

%session->start()
$this->ready = true
```

The last two lines are controller code because they sit at top level.

* This code runs once, when the instance is **first created**.
* The difference from `__construct` is that the instance **fully exists** by the time controller code runs, which prevents circular references and incomplete objects.

---

## 9.3 The role of `__handle()`

Phlo generates a special `__handle()` method for every class.
The instance manager calls it **whenever an instance is requested via `%name`**.

`__handle()`:

* runs the controller code if it hasn't run yet;
* makes sure props and statics are available correctly;
* caches the instance for subsequent calls.

You never call or override `__handle()` yourself; it is part of the generated class and the instance manager.

---

## 9.4 Lazy initialization

Because controller code runs **only after construction**, instances can **reference each other** without triggering unwanted recursive creation.

Example:

`a.phlo`:

```phlo
prop message = 'A ready'
```

`b.phlo`:

```phlo
prop message = 'B ready'
```

`main.phlo`:

```phlo
route GET test => $this->show

method show {
  dx(%a->message, %b->message)
}
```

* `%a` and `%b` are created lazily.
* The controller code in both files runs once their instance fully exists.
* You can reference instances from each other freely, because the instance already exists before its controller code runs.

---

## 9.5 The obj base class: powertools

Every compiled class extends `obj`, and `obj` is more than `__get`/`__set`. These are the tools you reach for when a class needs to behave dynamically.

**Interception hooks.** Implement `objCall`, `objGet` or `objSet` to trap the access chain. Returning `null` falls through to the normal behavior; anything non-null short-circuits:

```phlo
method objGet($key) => $this->cache[$key] ?? null

method objCall($method, ...$args) => str_starts_with($method, 'find') ? $this->finder($method, $args) : null

method objSet($key, $value) => $key === 'id' ? true : null
```

`objGet` runs before data/closure/method/prop lookup on every read, `objCall` on every unknown method call, and `objSet` before every write (a non-null return swallows the write). This is the mechanism behind decorators, lazy loading and read-only guards.

**Bound closures.** Assign a closure and it binds to the instance: `$obj->greet = fn() => "Hi $this->name"`, later `$obj->greet()` runs with `$this` bound. Handy for per-instance behavior without subclassing.

**Data API.** `objImport(name: 'x', age: 3)` bulk-assigns and returns `$this` (chainable). `objKeys()`, `objValues()` and `objLength()` inspect the data; `objClear()` wipes it. Iterating an obj (`foreach $record AS $key => $value`) and `json_encode($record)` expose exactly the stored data. Every write flips `objChanged`, the dirty flag the ORM uses to decide whether `objSave` writes anything.

**Computed prop caching.** `prop x => ...` caches on first access; the argument form caches per argument set. The same applies to computed statics, cached per class.

**Worker persistence.** `prop objPers = true` makes an instance survive between worker-mode requests: the `phlo()` registry only keeps `objPers` instances on its per-request reset. Right for DB connections and parsed config; wrong for anything request- or user-scoped.

> **Lesson.** A plain prop in a parent class SHADOWS a computed prop in a child. `prop dir = void` in an abstract parent compiles to a real PHP property, so a child's `prop dir => guide` getter is never consulted: `$this->dir` silently reads `void`. When children must override with a computed prop, declare the parent prop computed as well: `prop dir => void`.

---

## 9.6 Best practices

* Use controller code for **initial setup**, not for request-dependent logic.
* Place controller code **at the top** or directly below props for readability.
* Avoid side effects in `__construct`; use controller code instead of custom constructors.
* Let instances initialize themselves lazily via `%name` instead of creating them manually.
* Use controller code deliberately to resolve circular references.


---

# 10. Tooling & CLI

Phlo apps have a CLI layer for build, lint, release and reflection. Always use it through the app's dev entrypoint.

---

## 10.1 Build commands

```bash
php www/app.php build::run
php www/app.php build::lint
php www/app.php build::release
php www/app.php build::config
php www/app.php build::changed
```

`build::lint` should return an empty array:

```json
[]
```

If lint reports errors, fix the `.phlo` source and build again. Never patch the generated PHP.

---

## 10.2 Reflect commands

Reflection helps you understand an app before you change it:

```bash
php www/app.php reflect::context
php www/app.php reflect::compactRoutes
php www/app.php reflect::compactViews
php www/app.php reflect::resourceSummary
php www/app.php reflect::objectIndex
php www/app.php reflect::functionIndex
```

These commands return JSON and are intended for development environments with `build: true`.

The same introspection drives the Graph view of the Phlo Control Center: every class, resource and dependency edge of your app, rendered live from `reflect::graph`:

![The Control Center Graph view: your app's classes, resources and dependency edges, rendered live from reflect::graph](/reflect-graph.webp)

---

## 10.3 General CLI dispatch

`build::` and `reflect::` are just two examples of a more general mechanism. Phlo's CLI can call **any** static, method or function in your app:

```bash
php www/app.php tasks::run                   # static method on a class
php www/app.php app.heartbeat                # method on a Phlo instance
php www/app.php answer "is an eel a fish"   # global function
```

Three patterns:

| Pattern | Dispatch | Example |
| --- | --- | --- |
| `Class::method args` | Static method on the class | `tasks::run`, `backup::nightly` |
| `object.method args` | Instance method via `phlo(object)` | `app.heartbeat`, `cms.reindex` |
| `function args` | Global function | `answer "question"` |

Output goes to stdout as JSON, errors go to stderr with a non-zero exit code. That makes every routine in your app directly usable from cron, deploy scripts, monitoring or a terminal, without building a separate CLI layer for it.

Only available with `build: true` in `www/app.php`. Do not run against a live production environment.

---

## 10.4 Debug helpers

With `debug: true` in `phlo_app(...)`, Phlo activates a set of helpers for inspection during dev. In production they are inert.

| Helper | Purpose | Behavior |
| --- | --- | --- |
| `d(...$args)` | Dump values into the response, continue running | Collected in `%res->dump`; rendered to the browser console at the end of the request. Inert without `debug: true` |
| `dx(...$args)` | Dump + STOP | Sync: full debug page with the source-mapped `.phlo` file and line. Async/CLI/streaming: the dump arrives in the browser console via the apply payload. Worker-safe: throws instead of calling `die()` |
| `debug($msg)` | Append a line to the debug log; `debug()` without arguments returns everything collected | Logged to the browser console with the request stats |
| `error($msg, $code = 500)` | Runtime error for the Phlo exception handler | Throws a `PhloException`, logged in `data/errors.json` |
| `trace($node, $args)` | Manual trace event (only active with `trace: true`) | Adds an event to the trace log, see the Trace chapter |

The lifecycle: helpers collect during the request (`%res->dump`, `%res->debug`); at the end of a sync page an inline script logs everything to the browser console together with memory, duration and trace metadata, and async responses carry the same data in their apply payload. Objects are unwrapped via `objInfo()`. Runtime errors accumulate in `data/errors.json` (message, source-mapped file and line, count, last occurrence); read them with `reflect::errors` or in the Phlo Control Center.

```phlo
method buildReport {
	$data = $this->load
	debug('loaded', count: count($data))
	if (!$data) error('No data to report')
	dx($data[0])
}
```

`dx()` is your primary "stop and look at what's there" tool during development. Forgot a `dx()` in code that ships to release? In `debug: false` mode it behaves just like `error()`, no silent passthrough.

---

## 10.5 Workflow

In a dev environment the Phlo Control Center (at `/phlo`) puts this whole loop in the browser: source, build, release and errors, with every file one click away:

![The Phlo Control Center source view: every .phlo file highlighted and searchable, one click from build, release and errors](/control-center.webp)

1. Read the source and the reflection output first.
2. Only modify `.phlo`, `data/app.json` or entrypoints.
3. Run `build::run`.
4. Run `build::lint`.
5. Test the relevant HTTP routes.
6. Run `build::release` when the stage/release output needs updating.

---

## 10.6 Dev, stage and production

Dev typically has:

```php
auth: true,
build: true,
debug: true,
```

In build+debug mode, the built-in control UI lives at `/phlo` by default; use `control: 'path'` in `phlo_app(...)` to pick a different path. Stage/production typically runs without build and without debug. The webroot points to `release/www/`.

---

## 10.7 HEAD and async

The current runtime supports the normal HTTP methods, including `HEAD`. Async requests are handled by the frontend resource and use the same routes as sync requests, unless you explicitly declare a route otherwise.


---

# 11. Translations

Phlo uses the `lang` resource for multilingual view text and translation. In views, you write static text preferably with the compact language shorthand. You write source text in your own language; the examples below use Dutch (`nl`) as the source language.

---

## 11.1 View shorthand

The standard form for static translatable text in a view is:

```phlo
view:
<p>{nl: Hallo wereld}</p>
```

The language code before the colon is the source language of the text. If `%app->lang` is the same language, the text is shown as is. If the active language differs, `%lang` uses the translation cache and schedules missing translations asynchronously.

The shorthand is for static text only. Everything between the colon and the closing brace becomes a single string argument: `{nl: Hallo wereld}` compiles to `{{ nl('Hallo wereld') }}`, so there is no placeholder or argument syntax inside it.

For dynamic values, call the `nl()` / `en()` function in an expression instead. These take sprintf-style arguments:

```phlo
view($name):
<p>{{ nl('Hallo %s', $name) }}</p>
```

The `%s` lives in the translated source string (so the cache key stays stable), and the argument is substituted after translation. Use the shorthand for static text and the function when you need to interpolate.

---

## 11.2 Helpers in code

The current `lang` resource provides global helper functions for Dutch and English:

```phlo
method title => nl('Welkom')
method intro => en('Build compact full-stack apps')
```

These helpers are useful in methods, props or controller code. In views, the shorthand usually stays clearer:

```phlo
view:
<h1>{nl: Welkom}</h1>
<p>{en: Build compact full-stack apps}</p>
```

---

## 11.3 Active language

The active language lives on `%app->lang`. A route can set it before the view is rendered:

```phlo
route both GET $lang:nl,en=nl guide {
	%app->lang = $lang
	view($this)
}
```

In links you can use `%lang` as an object value to display or process the current language.

---

## 11.4 Translation cache

Translations are loaded per language from `langs/` via `%INI(%app->lang, langs)`. Missing entries are translated asynchronously based on their hash and read from the cache later.

The core methods are:

```phlo
%lang->translation('nl', 'Hallo wereld')
%lang->translate('nl', 'en', 'Hallo wereld')
```

Use `translation()` for normal app rendering with caching and async backfill. Use `translate()` only when you deliberately want to perform a single direct translation.

---

## 11.5 Translation instructions

The `lang` resource feeds an AI translator, and you can steer it. The `instructions` property is extra context the translator receives with every translation: your domain, your terminology, and rules about what must stay verbatim. An app injects it with a build mod:

```phlo
prop %lang.instructions = 'Documentation for the Phlo language. Keep keywords like route, view and prop in English. Keep common English technical terms (best practices, deployment, release) untranslated where that reads naturally, and prefer natural phrasing over forced, over-literal translation.'
```

Without instructions the translator works from the text alone. Use them to keep proper names and keywords intact and to avoid stilted translations of terms that are already common in the target language. The default is `void`. The same instructions also reach markdown documentation translated through the `docs` machinery, which reads `%lang->instructions`.

---

## 11.6 Best practices

* Use `{nl: ...}` and `{en: ...}` in views for static text; use `nl()` / `en()` for anything with arguments.
* Use `nl()` and `en()` in PHP/Phlo code outside views.
* Set `%app->lang` early, in the route or a central controller.
* Keep source text stable; changed text gets a new hash.
* Set `%lang.instructions` so the translator keeps your terminology and avoids forced translations.
* Document only language helpers that actually exist as resource functions.


---

# 12. Advanced

Phlo stays deliberately modular. You can keep an app small and activate only the resources it needs, or combine multiple source paths and resource groups.

---

## 12.1 App code and runtime resources

App-specific code belongs in the app itself. Do not put it in `/srv/phlo/resources/`.

`/srv/phlo/resources/` is the Phlo runtime catalog: framework-wide resources that may be shared across multiple apps and are deliberately maintained alongside the runtime. Only generic, stable code belongs there.

If you want to share code between apps, first create an explicit shared module or app library path with a clear owner. Only promote code to the Phlo runtime catalog when it is truly framework functionality.

A runtime resource can provide an object, function, style or script. Metadata at the top of the file helps the Phlo Control Center and the manual:

```phlo
@ summary: Send app notifications
@ package: notifications
@ frontend: false
@ backend: true

method send($message){
	return HTTP(%creds->notify->url, POST: ['message' => $message])
}
```

---

## 12.2 Multiple source paths

Keep the default simple: app source in the app path. Only add extra paths when a codebase genuinely needs to be shared.

Path choices should stay predictable:

* app source in `/srv/example.nl/`
* runtime in `/srv/phlo/`
* release in `/srv/example.nl/release/`
* data and credentials in `/srv/example.nl/data/`

---

## 12.3 Integrating with existing PHP

Use Phlo alongside existing PHP by loading the runtime and making the Phlo entrypoint responsible only for the routes the app handles. Existing static files keep being served directly by the webserver.

```php
<?php
require('/srv/phlo/phlo.php');
phlo_app (
	id: 'Legacy',
	host: 'dev.legacy.test',
	build: true,
	debug: true,
	app: '/srv/legacy/',
);
```

---

## 12.4 Security and visitors

For public sites, the usual baseline is:

```json
{
    "resources": [
        "cookies",
        "security/security",
        "security/token",
        "session",
        "useragent",
        "visitors",
        "phlo.async",
        "DOM/form"
    ]
}
```

For local dev you can exclude tracking:

```json
{
    "exclude": [
        "visitors",
        "useragent",
        "wsCast"
    ]
}
```

---

## 12.5 Cookiewall: GDPR consent

`DOM/cookiewall` is a built-in, subtle consent banner. Activate it in 3 steps:

**1. Resource** in `data/app.json`:

```json
{ "resources": [..., "DOM/cookiewall"] }
```

**2. Banner in your layout**:

```phlo
view layout:
<body>
	{{ %cookiewall->banner }}
	<main>...</main>
</body>
```

The banner only appears when the visitor hasn't made a choice yet. Two buttons: "Essential only" and "Accept". The choice is stored in a cookie `cookieChoice` (`'essential'` or `'all'`), valid for 1 year.

**3. Guard around tracking**:

```phlo
<if %cookiewall->canTrack>
	<script src="https://analytics.example.com/script.js"></script>
</if>
```

| Method | Returns |
| --- | --- |
| `%cookiewall->hasChosen()` | `true` once the visitor has chosen something |
| `%cookiewall->canTrack` | `true` only for the 'all' choice |
| `%cookiewall->canAnalytics` | Alias of `canTrack`, semantically useful for an analytics bridge |
| `%cookiewall->choice` | `'essential'` / `'all'` / `null` |

Multilingual variant: `DOM/cookiewall.translated` (uses the `{nl: ...}` shorthand for the banner texts).

---

## 12.6 Worker mode

By default Phlo runs per request: PHP process starts, handles the request, process ends. With `thread: true` in `phlo_app(...)`, the runtime stays in memory between requests, intended for FrankenPHP, ReactPHP or RoadRunner.

The performance gain is large (no boot per request), but three rules apply:

**1. No `die()` or `exit()` in the HTTP path.** Both kill the entire worker, not just the current request. Use `return` or let a terminating call (`view()`, `apply()`, `location()`) send the response.

**2. No request state in static properties.** Statics survive between requests. Data from request A leaks into request B. Statics are only safe for class structure or computed metadata that is identical for all requests, not for session, user, payload, time or DB state.

**3. Mark long-lived objects with `$objPers = true`.** By default Phlo clears its instance map between requests. For objects you explicitly want to reuse (DB connection, prepared statements), set `$this->objPers = true` so the cleanup leaves them alone.

Combining with `build: true` is **not allowed**: build writes files between requests, and in a worker that is a race condition. Phlo throws a runtime error if you enable both.

---

## 12.7 Modifying resources without forking

Sometimes you want a shared resource to behave just slightly differently in one app, without copying or changing that resource. From any `.phlo` file, you can inject or override a node in a **different** class by naming the node as `%<class>.<node>`:

```phlo
static %visitors.table = 'control.visitors'
prop %visitors.db = 'control'
method %model.greet => 'hi'
```

The first line overrides the `static $table` of the `visitors` model; the second adds a `db` prop to `visitors`; the third adds a `greet` method to `model`. During the build, the compiler strips the `%<class>.` prefix and writes the node into `<class>`: an existing node with that name is **overwritten**, a new one is added. The target class must be part of the build (its resource loaded), otherwise the modifier is silently ignored. Keep the node type identical to what you replace (`static` with `static`, `prop` with `prop`): the entire node is swapped.

A practical example: have the shared `visitors` model write to a central analytics database, while all other queries in the app stay on the app's own connection:

```phlo
static %visitors.table = 'control.visitors'
```

This keeps the shared resource agnostic while every app gives it its own interpretation.

---

## 12.8 File metadata: the complete @ reference

Every `.phlo` file can open with `@ key: value` lines. Any key is stored as file metadata; these have engine or tooling meaning:

| Key | Effect |
| --- | --- |
| `@ class:` | Override the PHP class name |
| `@ extends:` | PHP inheritance (default: `obj`) |
| `@ implements:` | PHP interfaces, comma-separated |
| `@ use:` | PHP use statement (`Full\Name as Alias`) |
| `@ namespace:` | PHP namespace |
| `@ type:` | `class` (default), `abstract class`, `interface` or `trait` |
| `@ summary:` | One-line description, shown in the manual, reflection and the Phlo Control Center |
| `@ package:` | Group name for tooling |
| `@ frontend:` / `@ backend:` | Marks a resource frontend- or backend-only |
| `@ requires:` | Dependencies, resolved when the resource is enabled; `name?` is optional, `php-ext:` and `creds:` entries are informational |
| `@ provides:` / `@ binds:` | Frontend APIs offered / selectors hooked; feeds `reflect::selectorGraph` |
| `@ tags:` | Free-form labels, shown in reflection indexes |
| `@ advice:` | Developer guidance, shown in `reflect::objectIndex` |

---

## 12.9 Best practices

* Keep entrypoints explicit; avoid hidden configuration.
* Let release output come from `build::release`.
* Never put credentials in source files.
* Use reflection to verify resources, routes and functions.
* Add abstraction only when multiple apps genuinely benefit from it.


---

# 13. Appendices

These appendices give compact examples that match the production release.

---

## 13.1 Basic route with a view

```phlo
prop title = 'Welcome'

route both GET => view($this)

view:
<main>
	<h1>$this->title</h1>
</main>
```

---

## 13.2 Route with a variable

```phlo
route both GET product $id => $this->product($id)

method product($id){
	$record = product::record(id: $id) ?? http_response_code(404)
	view($this->productView, $record)
}

view productView($record):
<article>
	<h1>$record->title</h1>
</article>
```

---

## 13.3 Async form

```phlo
route both GET contact => view($this)
route async POST contact @name,email,message => $this->send

method send {
	%payload->name || error('Name is required')
	return ['html' => ['#status' => 'Sent']]
}

view:
<form method=post action="/contact" class=async>
	<input name=name>
	<input name=email>
	<textarea name=message></textarea>
	<button>Send</button>
</form>
<p#status/>
```

---

## 13.4 Minimal app.json

```json
{
    "resources": [
        "payload",
        "session",
        "security/security",
        "security/token",
        "tag",
        "phlo.async",
        "DOM/form"
    ],
    "release": "%app/release/"
}
```

---

## 13.5 Dev entrypoint

```php
<?php
require('/srv/phlo/phlo.php');
phlo_app (
	id: 'Example',
	host: 'dev.example.nl',
	auth: true,
	build: true,
	debug: true,
	app: '/srv/example.nl/',
	data: '/srv/example.nl/data/',
);
```

---

## 13.6 Release entrypoint

```php
<?php
require('/srv/phlo/phlo.php');
phlo_app (
	id: 'Example',
	host: 'example.nl',
	app: '/srv/example.nl/release/',
	php: '/srv/example.nl/release/',
	data: '/srv/example.nl/data/',
);
```


---

# 14. WebSocket

Realtime in Phlo runs through **phloWS**, a separate Node.js server (`phloWS.js`) that multiplexes WebSocket connections across multiple vhosts and hands every incoming message off as a one-shot PHP CLI call. Your app implements four hook functions and broadcasts from PHP with `wsCast()`.

---

## 14.1 What phloWS is

phloWS is a light broker written in Node.js (`ws` library, ~9 KB of code). One process on port `3001` serves the entire stack: a single phloWS routes to the right app via the `Host` header of the WS handshake.

For each incoming message phloWS boots a **one-shot PHP process** (`php-zts <app>/www/app.php ws::<event>`). That costs ~50-100 ms per message, but gives every handler the complete request lifecycle: DB, session, resources, everything is simply available.

No persistent worker state between messages. If you want to share state, do it through your database or `apcu`.

---

## 14.2 Installation

phloWS lives outside the Phlo framework. Pick a path, `/srv/websocket`, `/opt/phloWS`, `~/code/phloWS`, it doesn't matter. We use `<ws>` as a placeholder:

```bash
git clone https://github.com/q-ainl/phlo-websocket.git <ws>
cd <ws>
npm install
```

In `<ws>/websocket.js` you map vhost → app.php path:

```javascript
require('./phloWS.js')(3001, '/usr/bin/php-zts', {
	'app.example.com':     '<app>/release/www/app.php',
	'dev.app.example.com': '<app>/www/app.php',
})
```

Run it as a service (systemd / pm2 / supervisord); the [phlo-websocket README](https://github.com/q-ainl/phlo-websocket) describes the pm2 run pattern and the `/message` bridge contract:

```bash
node <ws>/websocket.js
```

For production: pass `wss://` through your reverse proxy (Caddy, Nginx, FrankenPHP) to `127.0.0.1:3001` for the path `/websocket`.

---

## 14.3 App hooks

In your app source you define four functions; Phlo's `websocket` resource calls them if they exist. Put them in a file like `app.ws.phlo`: do not name the file `websocket.phlo`, because that class name collides with the engine's `websocket` resource when it is loaded.

```phlo
function wsConnect($wsHost, $wsToken, $wsSocket){
	%log->info('ws connect', socket: $wsSocket)
	return true
}

function wsAuth($wsHost, $wsToken, $wsSocket){
	$user = %user->byToken($wsToken)
	if (!$user) return false
	%session->user = $user
	return true
}

function wsReceive($wsHost, $wsToken, $wsSocket, ...$data){
	$type = $data['type'] ?? null
	if ($type === 'ping') return wsCast(wsTarget: $wsSocket, pong: time())
	if ($type === 'chat.send') chat::send($data['text'], from: %session->user->id)
}

function wsClose($wsHost, $wsToken, $wsSocket){
	%log->info('ws close', socket: $wsSocket)
}
```

| Hook | When | Return |
| --- | --- | --- |
| `wsConnect` | Right after the WS handshake | `true` accepts, `false` closes |
| `wsAuth` | First auth message from the client | `true` authenticates, `false` closes |
| `wsReceive` | For every subsequent message (JSON-decoded and spread) | irrelevant, use `wsCast()` to respond |
| `wsClose` | Connection closes | irrelevant |

`$wsSocket` is an opaque string identifier you can use to broadcast back to exactly this client.

The connection-context arguments are **`ws`-prefixed by convention** (`$wsHost`, `$wsToken`, `$wsSocket`), exactly like `wsCast`. This is not cosmetic: `wsReceive` spreads the JSON payload into named arguments (`...$data`), so an unprefixed `$host`/`$token`/`$socket` parameter would fatally collide with a payload that carries a `host`, `token` or `socket` key. Keep the prefix and your payload keys stay free.

---

## 14.4 Auth flow

phloWS implements a two-step handshake:

1. Browser opens `wss://<host>/websocket`.
2. phloWS calls `wsConnect`. On `false`: close.
3. The first incoming message must be the auth payload (typically `{type: 'auth', token: '<string>'}`).
4. phloWS calls `wsAuth($wsHost, $wsToken, $wsSocket)`. The app validates against `%user`, `%session->token` or a custom lookup.
5. On `false`: close. On `true`: the socket is marked authenticated; everything after that goes through `wsReceive`.

The token typically comes from `%user->token` (per logged-in user) or an API key. The client can send it along via a cookie or as the first WS message.

---

## 14.5 Broadcasting from PHP

`wsCast()` is a regular function (resource `wsCast`). It does a `POST` to phloWS' internal HTTP bridge, which pushes it on to the right sockets.

```phlo
wsCast(wsTarget: 'all',          toast: 'New message received')
wsCast(wsTarget: $wsSocket,        path: '/inbox')
wsCast(wsTarget: ['s1', 's2'],   inner: ['#count' => $newCount])
```

| Argument | Default | Meaning |
| --- | --- | --- |
| `wsTarget` | `'all'` | `'all'`, `'token:<id>'`, `'token:not:<id>'`, a single socket id, or an array of socket ids |
| `wsHost` | `host` | Vhost the broadcast applies to (default: current host) |
| `wsPort` | `websocket` (constant from app config) | phloWS port |
| `...$data` | none | Named args become the payload, usually `apply()` commands |

The payload is passed through to the client and applied to the DOM automatically by `phlo.js`: the same `apply()` protocol you know from async routes.

> **No retry, no dead-letter, no ACK.** If phloWS is down, the POST fails silently. For guaranteed delivery (financial events): combine with a DB queue.

---

## 14.6 Client side

The client itself does nothing special. Add `DOM/websocket` to your resources in `data/app.json`:

```json
{
	"resources": [..., "DOM/websocket", "wsCast"]
}
```

`DOM/websocket` injects a script that:
- connects automatically to `wss://<host>/websocket`
- pipes incoming messages straight through `apply()`, `inner:`, `outer:`, `class:`, `toast:`, `path:` work the same as with async routes
- reconnects with exponential backoff (333 ms → 999 ms → ...)

If you want to send from JS: `app.websocket.send({type: 'chat.send', text: 'hi'})`.

---

## 14.7 Mini example: presence

Show "who is online" without polling.

```phlo
function wsConnect($wsHost, $wsToken, $wsSocket){
	%apcu->set("presence:$wsSocket", time(), 3600)
	wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
	return true
}

function wsClose($wsHost, $wsToken, $wsSocket){
	%apcu->delete("presence:$wsSocket")
	wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
}

static count(){
	$keys = %apcu->keys('presence:')
	return count($keys)
}
```

The server keeps no state; APCu counts sockets per host. On a PHP restart the cache empties by itself, which is fine, because an empty presence is an acceptable degraded state.

---

## 14.8 Known limitations

* **One-shot CLI per event**, every message costs a PHP startup. Fine for inbox, presence and notifications; unsuitable for high-frequency telemetry or real-time trading.
* **No versioning on payloads**, when refactoring: migrate all clients at once.
* **Single point of failure**, one phloWS process for the entire stack. On a crash: all realtime features are down until restart. Run phloWS under a process supervisor.
* **No built-in encryption**, use your reverse proxy for TLS termination (`wss://`).

---

## 14.9 Streaming responses: realtime-light without phloWS

Not every progressive update needs a WebSocket. Set `%res->streaming = true` in a route and every subsequent `apply(...)` is printed and flushed immediately as one JSON line over the same HTTP response; phlo.js keeps applying the commands as they arrive:

```phlo
route async POST report::generate {
	%res->streaming = true
	foreach ($this->steps AS $i => $step){
		$step->run
		apply(inner: arr('#progress' => $i + 1 .'/'. count($this->steps)))
	}
	apply(toast: 'Done')
}
```

Use streaming when one client triggered the work and watches its own progress (AI token streams, imports, batch jobs). Use phloWS when OTHER clients must receive the update too: streaming follows the request, broadcasting follows the fleet of connections.


---

# 15. Tasks

Phlo has a built-in cross-app cron runner. One system cron entry per app triggers `tasks::run` every minute; the `tasks` resource matches declaratively against `%app->tasks`. No `cron` syntax in your app, no external scheduler.

---

## 15.1 Setup

Three steps.

**1. Activate the resource** in `data/app.json`:

```json
{
	"resources": [..., "tasks"]
}
```

**2. Describe your tasks** in `app.phlo`:

```phlo
prop tasks => arr(
	cleanup: arr(do: 'account::cleanup', every: '5 minutes'),
	poll:    arr(do: fn() => external::pull(), every: 'minute'),
	backup:  arr(do: 'backup::run', daily: '03:00'),
	report:  arr(do: 'report::weekly', weekly: 'monday 09:00'),
)
```

**3. One cron entry per app** with an absolute path:

```cron
* * * * * php-zts <app>/www/app.php tasks::run
```

Place it in `/etc/cron.d/example-tasks` (system, 6 fields incl. user) or via `crontab -u <user>` (per-user, 5 fields).

---

## 15.2 Schedule

Pick exactly one scheduling key per task:

| Key | Format | Example |
| --- | --- | --- |
| `every:` | PHP-readable duration string | `'minute'`, `'5 minutes'`, `'2 hours'`, `'1 day'` |
| `daily:` | `'HH:MM'` | `'03:00'` |
| `weekly:` | `'<weekday> HH:MM'` | `'monday 09:00'` |

`every: 'minute'` (without a leading number) becomes `'1 minute'` internally. Parsing via `strtotime("+$every", 0)`.

---

## 15.3 Callable (`do:`)

The `do:` field accepts three forms:

| Type | Example | Is called as |
| --- | --- | --- |
| Closure | `fn() => external::pull()` | directly |
| `'Class::method'` | `'account::cleanup'` | `account::cleanup()` |
| Resource name | `'backup'` | `phlo('backup')` |

Unlike during a normal request, a task runs outside an HTTP lifecycle: there is no `%req`, no `%session`. Write your task so that it is self-contained.

---

## 15.4 State on disk (`data/tasks/`)

`tasks::run` creates `data/tasks/` automatically and guards each task with three files:

| File | Contents | When |
| --- | --- | --- |
| `<name>.last` | raw unix timestamp | Per successful run, for the due check |
| `<name>.json` | `{schedule, return}` for the Control Center | Per successful run |
| `<name>.lock` | empty (mtime counts) | During a run, TTL 1 hour |

Locks prevent a slow task from lapping itself. The TTL is deliberately 1 hour: a failed task is parked until the lock expires; other tasks keep running as usual.

---

## 15.5 Error flow

No `try/catch` in `tasks::run`. A `Throwable` bubbles up to Phlo's framework exception handler and writes to `data/errors.json`, just like build errors do. The Phlo Control Center shows them in the tasks tab.

---

## 15.6 Phlo Control Center

The Phlo Control Center detects `data/tasks/` automatically:

* A **Tasks tab** appears in the nav (only if the directory exists), right after Home.
* Per task: schedule (from JSON), last-run-ago, return value (type-aware: scalar / array / string), lock status.
* The Control Center is fully agnostic about the `tasks` resource and the app, it reads purely from `data/tasks/`. Schedule info comes from `<name>.json`, not via an app route (that would trigger an HTTP response and disrupt the Control Center render).

---

## 15.7 Example

```phlo
prop tasks => arr(
	heartbeat: arr(do: 'app::heartbeat', every: 'minute'),
)

static heartbeat => file_put_contents(data.'heartbeat.log', date('Y-m-d H:i:s').' tasks::run fired'.lf, FILE_APPEND | LOCK_EX)
```

Cron entry:

```cron
* * * * * php-zts <app>/www/app.php tasks::run
```

Every minute `tasks::run` is invoked, sees that `heartbeat` is `every: 'minute'` and `lastRun < 60s` ago, and runs `app::heartbeat()`. The files `data/tasks/heartbeat.last`, `.json` and `.lock` are updated; intermediate cron ticks skip the task while it is running.


---

# 16. AI

Phlo bundles a number of AI providers (OpenAI, Claude, Gemini, DeepSeek, Grok) behind a single facade. You pick a model, Phlo picks the right engine. Streaming to the DOM uses the same `apply()` mechanics as the rest of Phlo: no separate client-side library, no separate event bus.

---

## 16.1 Resources

Add to `data/app.json`:

```json
{
	"resources": [..., "AI/AI", "AI/OpenAI"]
}
```

One file per provider. `AI/AI` is the facade, which routes to the right engine based on the model:

| Model contains | Engine |
| --- | --- |
| `gpt-*`, `o1-*`, `o3-*`, `o4-*`, `chatgpt-*` | OpenAI |
| `claude-*` | Claude |
| `deepseek-*` | DeepSeek |
| `gemini-*` | Gemini |
| `grok-*` | Grok |

Or explicitly via the `via:` argument: `%AI->chat(via: 'claude', model: ...)`.

The `model` argument is optional. Without one the facade falls back to `%app->model` and then to its own default, `gpt-5.4-mini` (which routes to OpenAI), so an app with just an OpenAI key in `creds.ini` works out of the box. Set `%app->model` to change the default per app, or override `%AI.model` with a build mod to change it system-wide, including the default engine.

The facade exposes the same methods for every engine, but not every provider backs every one:

| Engine | chat | stream | tools | vision | embeddings | transcribe |
| --- | --- | --- | --- | --- | --- | --- |
| OpenAI | yes | yes | yes | yes | native | yes |
| Claude | yes | yes | yes | yes | OpenAI | no |
| Gemini | yes | yes | yes | yes | native | no |
| DeepSeek | yes | yes | yes | no | OpenAI | no |
| Grok | yes | yes | yes | yes | OpenAI | no |

`OpenAI` in the embeddings column means the engine has no embedding model of its own and delegates to OpenAI, so it also needs an OpenAI key. DeepSeek and Grok are thin layers on top of OpenAI (same protocol, different endpoint and key), so they share its method set; a `no` cell means the provider has no model or endpoint behind that call and it will error. The matrix is the source of truth: only invoke a capability that is marked for the engine you target.

Credentials go in `data/creds.ini`:

```ini
OpenAI = sk-...
Claude = sk-ant-...
Grok = xai-...
```

Phlo's `security/creds` loads them automatically into `%creds->OpenAI` etc. See Configuration for the full credential format, environment variables and precedence.

---

## 16.2 A single answer

Short question, one answer:

```phlo
$answer = %AI->chat(
	model: 'gpt-4o-mini',
	user: 'Summarize this article: '.$article->text,
)
echo $answer->answer
```

Or even shorter, via the `answer` helper:

```phlo
$verdict = answer('Is "carrot" a vegetable?', 'yes', 'no', 'maybe')
```

`answer()` is built into `AI/answer`. It makes one call with a low temperature and returns only the purest answer. With options it becomes a choice from the given possibilities.

---

## 16.3 Streaming to the DOM

This is where Phlo's `apply()` protocol really shines. An async route that writes token by token into an element:

```phlo
route async POST chat::ask {
	%res->streaming = true
	foreach (%AI->stream(user: %payload->question) AS $chunk){
		if (isset($chunk->text)) apply(append: arr('#answer' => $chunk->text))
	}
}
```

Set `%res->streaming = true` and every `apply()` flushes to the client the moment you call it, instead of being buffered until the response ends. Each token is appended to `#answer` through the same `apply()` protocol you use everywhere else: no SSE plumbing, no manual `flush()`, no JS to write and no state to manage, a streaming UI right away.

---

## 16.4 Tools (function calling)

```phlo
$tool = obj(
	name: 'get_weather',
	desc: 'Get the current weather for a location',
	args: arr(
		location: arr(type: 'string', desc: 'City and country, e.g. "Paris, FR"'),
	),
)

$res = %AI->chat(
	model: 'gpt-4o-mini',
	user: 'What is the weather in Amsterdam?',
	tools: [OpenAI::tool($tool)],
)

foreach ($res->tools ?? [] AS $call){
	if ($call->name === 'get_weather') weather::fetch($call->args['location'])
}
```

Tool calls come back under `$res->tools` as an array of `{name, args}`. Phlo's facade normalizes the provider differences.

---

## 16.5 Vision

```phlo
$res = %AI->vision('What is in this photo?', '/uploads/photo.jpg')
echo $res->answer
```

Works with OpenAI, Claude, Gemini and Grok.

---

## 16.6 Embeddings

```phlo
$vector = %AI->embedding('Phlo is a compile-to-PHP framework', model: 'text-embedding-3-small')
%vectors->store(id: 'doc-1', vector: $vector, meta: ['source' => 'about'])
```

The default model is provider-specific. For OpenAI it is `text-embedding-3-small`.

---

## 16.7 Transcribe

```phlo
$file = %files->save(%payload->file('audio'))
$res = %AI->transcribe($file, model: 'whisper-1', language: 'nl')
echo $res->text
```

---

## 16.8 Safety

* AI calls are expensive and non-deterministic. Cache aggressively, `%apcu` for session scope, `JSONDB` for longer TTLs.
* Filter user input before you put it in a prompt. Phlo's `esc()` is for HTML; for prompts use your own sanitizer or a strict tool schema.
* Logging prompts/answers can have privacy implications. By default Phlo does not log; your `data/errors.json` only sees exceptions.


---

# 17. Trace

Phlo's trace mode is a runtime instrumentation layer that logs every call to a generated method, prop getter, static or native function with timing and arguments. Per request Phlo writes a JSON dump to `data/trace/<id>.json`. Meant for debugging and profiling, not for production.

---

## 17.1 What trace does

With trace on, Phlo's compiler injects one line at the top of every generated method:

```php
trace('class->method', compact('arg1', 'arg2'))
```

And Phlo loads `functions.trace.php` (a generated variant of `functions.php`) so that native helpers, `esc()`, `arr()`, `loop()`, `view()`, `apply()`, everything, get a trace call too. The result: a complete chronological log of what happened during a request.

---

## 17.2 Enabling

In your dev entrypoint:

```php
phlo_app (
	id:    'Example',
	host:  'dev.example.nl',
	build: true,
	debug: true,
	trace: true,
)
```

Then run `build::run` so the generated PHP contains the `trace()` injections. From the next request on, every call is logged.

Trace output goes to `data/trace/`. The directory is created automatically.

---

## 17.3 What a trace contains

One JSON file per request with:

| Field | Contents |
| --- | --- |
| `id` | Date-time + random suffix, also the file name |
| `path`, `method`, `route` | Request context |
| `ts`, `ms`, `count` | Start timestamp, total duration, number of events |
| `active` | Map per file → kind → call count (quick overview of "what got touched a lot") |
| `sequence` | Order of first touch per file (visualizes the request path through your app) |
| `events` | Complete log: see below |

Each event in `events`:

```json
{
	"t":    12.345,
	"k":    "call",
	"c":    "user",
	"n":    "byEmail",
	"node": "user->byEmail",
	"f":    "user.phlo",
	"args": {"email": "jordi@example.nl"}
}
```

| Field | Meaning |
| --- | --- |
| `t` | Offset in ms since request start |
| `k` | Kind: `call`, `static`, `get` (prop get), `set` (prop set), `function` |
| `c`, `n` | Class and name |
| `f` | Source file (resolved via classmap + sourcemap) |
| `args` | Snipped arguments, see X.4 |

---

## 17.4 Argument snipping

Full argument values would make the trace unreadable. Phlo snips:

| Type | Becomes |
| --- | --- |
| `null`, `bool`, `int`, `float` | unchanged |
| `string` > 200 characters | `...` truncated at 200 |
| `array` | `'[N items]'` (length only) |
| `object` with an `id` property | `{class: ..., id: ...}` |
| Other `object` | `{class: ...}` |

That is enough to see *which* records were touched without dumping the entire payload.

---

## 17.5 Reading via the Phlo Control Center

The Phlo Control Center has a **Trace** tab that reads `data/trace/index.json`. The most recent trace is at the top; a selectbox opens older ones. Per trace you see `active`, `sequence` and the event stream.

`index.json` keeps the last **100 traces**. Older ones are auto-pruned along with their JSON file.

---

## 17.6 Maintenance: `build::traceShadow`

`functions.trace.php` is a generated file. Whenever you add something to or change something in `functions.php`, regenerate it:

```bash
php www/app.php build::traceShadow
```

This parses `functions.php` and injects, in every `function foo($a, $b)`, as the first statement:

```php
trace('foo', compact('a', 'b'))
```

Then you align the contents of `functions.trace.php` with the source. The command is only relevant for those working on the Phlo runtime itself, not for app code.

---

## 17.7 When to use it, when not

**Do**:
- A request is slow "somewhere" and you want to know where the time goes
- Exploring an unfamiliar app: `sequence` shows you the actual path through the code
- Debugging a race condition or side effect: `events` shows exact order + timing
- For teaching: showing how a Phlo request actually unfolds

**Don't**:
- Production, every method call costs a log entry and a disk write per request
- `thread: true` workers, trace writes per request and is therefore, just like `build`, not worker-safe
- Performance measurement down to the microsecond, the instrumentation itself adds overhead

Turn `trace: true` on when you need it; turn it off when you're done. The Control Center keeps showing the historical traces until the auto-prune cleans them up.


---

# 18. Philosophy

This chapter explains why Phlo is built the way it is: the deliberate trade-offs behind the parser, the compiler, the runtime and the tooling. The short version: Phlo is an integrated platform with its own full-stack language, and its power is in the vertical integration of all layers, one language and one mental model from code to fleet.

Underneath every trade-off is one intent: to give a person or a small team back ownership and oversight of what they build. A lot of modern web development quietly works against that. Dependency trees too deep to audit, frameworks that churn faster than you can learn them, ceremony and boilerplate that bury what the code actually does, and usage-metered hosting whose bill grows precisely when you succeed. Each one adds a layer between you and your software, until no single person holds the whole picture anymore and the cost of running it is no longer yours to predict. Phlo treats those as paradigms to step away from, not to adopt. Code carries no decoration it does not need, so it stays readable and reviewable in one sitting. The output is plain PHP you own. It runs on a machine you control, at a cost that tracks the machine rather than every request. Legibility, ownership and affordability are not features bolted on top; they are the point, and the sections below are how the language earns them.

---

## 18.1 One system, four layers

Phlo is not a library you add to a stack; it is the stack. Four layers share one design:

* **The language**: custom `.phlo` syntax that compiles to readable PHP, CSS and JavaScript.
* **The application platform**: the backend (`obj`, resources, functions) and the frontend (phlo.js, the integrated SPA engine, the `apply()` protocol) speak the same protocol, so a route can update the page without you writing client glue.
* **The server platform**: phloWS for realtime, phloWA as WhatsApp gateway, mail, and FrankenPHP worker mode for production.
* **The operations platform**: the Phlo Dashboard (a separate application) manages your fleet, hosts, domains, databases and notifications.

Each layer would be unremarkable on its own. Together they mean you never translate between ecosystems: the same conventions, the same error pages, the same CLI, from a single view to a fleet of servers. A comparison like "a compact alternative to Laravel or HTMX" touches only one layer and misses the point.

---

## 18.2 A line-based parser, no AST

Phlo reads source line by line: a statement ends at a line ending, a node header opens a block, a blank line closes a view. There is no tokenizer building an abstract syntax tree. The whole parser is a few hundred lines, readable in a sitting, and because line N in maps to a known line out, the sourcemap and the error pages come almost for free.

The line-terminator model is complete and small: every line gets a `;`, lines ending in `(` `[` `{` `}` `,` or `.` are implicit continuations, and a trailing `\` continues explicitly (see chapter 4).

The cost is strictness: no multiline quoted strings, a blank line ends a view, CSS declarations stay on one line. Phlo's answer is better diagnostics, not a more forgiving parser: violations stop the build with the `.phlo` file and line. A real grammar would relax the rules at the price of the parser's legibility, and that is not a trade Phlo makes.

---

## 18.3 Compiles to readable PHP

`.phlo` compiles to plain PHP classes: one class per file, a header naming the source, and a per-class sourcemap recording PHP line to `.phlo` line. You can always drop into the generated PHP to see exactly what runs; there is no hidden runtime interpreting templates. And when something breaks at runtime, the error is translated back to the `.phlo` line you wrote, in the error page and in the Phlo Control Center.

The cost is a build step. In development it disappears behind rebuild-on-request (X.6); in production you build once.

---

## 18.4 The `obj` magic base class

Every compiled class extends `obj`: arbitrary data via `__get`/`__set`, bound closures, and computed properties written as `_name()` methods, called without parentheses and cached on first access. In `.phlo`, `prop now => time()` compiles to a cached `_now()`. One access model replaces getters, lazy initialization and value objects; the same object serves as view model, record and config bag.

The cost: magic access is less statically analysable than explicit properties, and the model demands discipline about caches. The static structure caches in `obj` only ever hold class shape, never request- or user-scoped values, which is what keeps it safe under worker mode.

---

## 18.5 `phlo()` as a tiny service registry

`phlo('MySQL')` returns a shared instance; in `.phlo` source, `%MySQL` compiles to exactly that call. Each class can implement `__handle()` to decide its own identity (singleton, multiton by argument, always new) and opt into surviving between worker requests. You get dependency lookup without a container framework, configuration or annotations, and the `%name` shorthand keeps call sites short and greppable.

The honest label is service location, not injection: dependencies are implicit at the call site. In exchange there is zero wiring, and the registry itself is a dozen lines.

---

## 18.6 Rebuild on request in development

With `build: true`, Phlo checks whether any source changed and recompiles before handling the request, throttled so the check is cheap in a hot loop. The edit-refresh loop feels interpreted while the runtime stays compiled, with no watcher process and no manual build.

The cost: building writes files during a request, which is unsafe in a long-running worker, so `build` and `thread` are mutually exclusive. Development uses on-request builds; production runs worker mode on a release build. The split is intentional, not a limitation to engineer around.

---

## 18.7 Zero dependencies

The engine ships its own CSS transpiler, JS minifier, icon-sprite builder and SPA runtime, plus more than 150 bundled resources instead of a package tree. Composer is supported, but lazy and optional. The engine has nothing to audit but itself, upgrades on its own schedule, and stays small enough to hold in your head. For a platform whose premise is legibility, a vendor tree would undercut the premise.

The cost: Phlo reimplements things mature libraries already solve, so those implementations must be tested and can have edge cases a large library would not. The bet is that a small, owned surface is worth more here than breadth.

---

## 18.8 Self-hosted ownership

A Phlo app is a directory of files on a server you control. It runs as one FrankenPHP process, stays in memory in worker mode, and answers requests without a per-invocation meter running. Its cost is a server, not a usage bill: the same machine serves your hundredth visitor and your hundred-thousandth, and the line on the invoice does not bend upward with success.

This is deliberate distance from the cloud and serverless default, where the bill is smallest exactly when you have no users and grows with every request once you do. For a product that works, that model can turn growth into a liability: the more it is used, the more it costs to keep running, sometimes past the point where the numbers still make sense. Phlo bets the other way. You own the language output (readable PHP), the data (a file or a database you administer), and the machine it all runs on. The Phlo Dashboard manages that as your fleet, not as rented capacity you never see.

Self-hosted does not mean unscalable. A stateless PHP application tier behind a load balancer, with shared state in a database or cache, is the architecture that has run the largest sites on the web for over 25 years. Phlo drops straight into it: it compiles to ordinary PHP on FrankenPHP, and worker mode is share-nothing per request, so you scale the application tier by running more identical nodes behind the load balancer, at a predictable per-node cost. Multiple nodes need shared session state (point PHP's session store at Redis or a database, or use sticky sessions), and the database becomes the tier that takes the real scaling work, replicas and partitioning, exactly as in any such stack. The application tier is the cheap, easy part; the data tier is where the engineering goes. The deployment docs cover the concrete multi-node setup.

The cost: ownership is also responsibility. You provision the server, you patch it, you take the backups, you carry the pager. There is no autoscaling to zero and no managed control plane absorbing the operational load. The bet is that for most products a predictable server you understand beats an elastic bill you cannot.

---

## 18.9 Agent-first by design

SKILL.md is a complete language and build reference written so an AI agent can work without prior knowledge; the `reflect::` CLI exposes routes, views, the parsed structure, search and dependency graphs as JSON; apps keep a `data/app.md` scratchpad an agent reads first and updates after. The same properties that make Phlo legible to you (one closed loop, source-mapped errors, a single skill document) make it tractable for an agent, and treating that as a first-class goal multiplies a small team.

The cost: SKILL.md and `reflect::` are part of the contract. A change to the language, the build or the CLI is not done until the documentation reflects it. That maintenance burden is accepted on purpose.

---

## 18.10 What Phlo is not

Honesty about the boundaries:

* **A young ecosystem.** No marketplace of plugins, no Stack Overflow archive. The bundled resources and the guide are the ecosystem.
* **Opinionated by construction.** The line parser's strictness, the blank-line rule in views, dots-not-underscores in file names: these are not configurable.
* **Not dependency injection.** `%instance` is service location; if you want explicit constructor wiring, Phlo will feel implicit.
* **Realtime with a boot cost.** The phloWS one-shot CLI model starts a PHP process per event: simple and robust, but not built for thousands of events per second.
* **Dev and production are different modes.** Rebuild-on-request and worker mode exclude each other by design; you cannot have both at once.

If those constraints fit, you get the payoff this chapter described: one language and one mental model, vertically integrated from your first view to your whole fleet.

