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
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:
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 |
cliis 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:
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:
$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
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/',
)
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 bypassesrender(): queued headers (including Content-Type) never leave the server, and in worker modedie()kills the whole worker. This site served its machine-readable endpoints astext/htmlfor weeks that way. Always end withoutput(...),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.