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

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:

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

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.