1: Inleiding
Phlo is een compile-to-PHP framework voor compacte full-stack applicaties. Je schrijft .phlo bronbestanden; Phlo bouwt daar gewone PHP-klassen, routes, CSS en JavaScript uit. De productie-release draait op een gedeelde runtime, meestal in /srv/phlo/, terwijl elke app zijn eigen bron, data, gegenereerde PHP en webroot houdt.
1.1: Filosofie
- Bron eerst - je werkt in
.phlo, niet in gegenereerde PHP, CSS of JavaScript. - Een kleine runtime - de webentrypoint laadt
/srv/phlo/phlo.phpen start de app metphlo_app(...). - Expliciete runtime-resources -
data/app.jsonkiest welke Phlo-resources uit de runtimecatalogus een app laadt; appcode blijft in de app. - Dev en release gescheiden - dev bouwt, debugt en kan auth gebruiken; release gebruikt de gegenereerde output.
- HTML, routes en gedrag dicht bij elkaar - een route, view, style of script mag in hetzelfde onderwerpbestand staan als dat de app duidelijker maakt.
1.2: Installatie
Phlo vraagt PHP 8.3 of hoger; de CLI-build draait op dezelfde PHP. Voor productie is FrankenPHP de aanbevolen runtime (ingebouwde webserver, worker-mode); klassiek PHP-FPM achter Nginx werkt ook.
Haal de runtime op en scaffold je eerste app met de meegeleverde installer:
git clone https://github.com/q-ainl/phlo.git /srv/phlo
php /srv/phlo/install.php /srv/example.nl/
De installer vraagt om naam, host en doel, toont de runtimecatalogus en laat je resources kiezen (hun @ requires worden automatisch meegenomen), schrijft entrypoint, data/app.json, data/app.md, een eerste route en .gitignore, en eindigt pas na een schone build met concrete next steps.
Liever een kopie die zichzelf opruimt? Kopieer install.php naar de nieuwe appmap en draai hem daar; na een geslaagde installatie verwijdert hij zichzelf:
cp /srv/phlo/install.php /srv/example.nl/ && cd /srv/example.nl && php install.php
De volgende secties beschrijven wat de installer voor je neerzet, en hoe je hetzelfde handmatig opbouwt.
1.3: Projectstructuur
Een typische app:
/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 en release/ zijn build-output. Pas die alleen aan via de bron en build opnieuw.
1.4: Entrypoint
Dev-entrypoint in www/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/',
data: '/srv/example.nl/data/',
);
Release-entrypoint in release/www/app.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
Gebruik de CLI vanuit de dev-entrypoint:
php www/app.php build::run
php www/app.php build::lint
php www/app.php build::release
build::run transpileert de dev-output, build::lint controleert de gegenereerde PHP en build::release schrijft de release-output.
1.6: Webserver
De webserver wijst naar www/ voor dev en naar release/www/ voor stage/productie. Onbekende paden worden naar app.php herschreven.
Voor FrankenPHP (aanbevolen) volstaat een Caddyfile-blok:
example.nl {
root * /srv/example.nl/release/www
php_server
}
Voor Nginx:
root /srv/example.nl/release/www;
location / {
try_files $uri $uri/ /app.php?$query_string;
}2: Configuratie
data/app.json beschrijft de build: welke resources geladen worden, waar release-output naartoe gaat en welke resources alleen in release of alleen in dev horen.
2.1: Minimale configuratie
{
"resources": [
"security/creds",
"security/security",
"security/token",
"payload",
"session",
"tag",
"phlo.async",
"DOM/form"
],
"release": "%app/release/"
}
Resources verwijzen naar de Phlo runtimecatalogus. Dat is frameworkcode, geen plek voor app-specifieke bestanden. Appcode schrijf je als .phlo in het app-pad; alleen generieke runtimefunctionaliteit hoort bewust in de catalogus.
2.2: Resources
Veelgebruikte resources:
| Resource | Doel |
|---|---|
security/creds |
Credentials uit env en ini-bestanden |
security/security |
Security headers |
security/token |
Tokens genereren |
payload |
POST, PUT, PATCH en uploads lezen |
session |
Session-object |
cookies |
Cookie-object |
DOM/form |
Async formulieren |
phlo.async |
Async frontend requests |
visitors |
Heartbeat/visitor tracking |
useragent |
User-agent parsing |
DB/DB, DB/MySQL, DB/model |
Database en ORM |
Gebruik alleen resources die de app echt nodig heeft. De dashboard-reflectie kan beschikbare resources en afhankelijkheden tonen.
2.3: Dev exclude
In een lokale dev-build wil je bepaalde tracking- en realtime-resources vaak niet meenemen:
{
"exclude": [
"visitors",
"useragent",
"wsCast"
]
}
Dit geldt voor de dev-build. De release-build gebruikt deze exclude niet automatisch; bezoekersregistratie kan daar dus gewoon actief zijn.
2.4: Release
De korte vorm is voldoende:
{
"release": "%app/release/"
}
Phlo schrijft dan release-PHP naar release/ en webassets naar release/www/.
2.5: Paden
%app/ verwijst naar het app-pad uit phlo_app(...). Hou padconfiguratie zoveel mogelijk in www/app.php en release/www/app.php, zodat data/app.json over buildgedrag blijft gaan.
2.6: Gegenereerde output
Bewerk deze bestanden niet handmatig:
php/
www/app.js
www/app.css
release/
Wijzig de .phlo bron, draai build::run, controleer met build::lint en maak daarna een release met build::release.
3: Runtime config
data/app.json beschrijft de build, wat er gecompileerd wordt. Dit hoofdstuk gaat over de runtime, wat er per request gebeurt. Die config staat in www/app.php (en, voor stage/release, release/www/app.php) als argumenten op phlo_app(...).
<?php
require('/srv/phlo/phlo.php');
phlo_app (
id: 'Example',
host: 'dev.example.nl',
auth: true,
build: true,
debug: true,
dashboard: 'phlo',
app: '/srv/example.nl/',
)
Elke key is een named argument dat tegelijk een PHP-constante wordt, overal in je app beschikbaar als bare-word (host, data, composer, etc.). Dat geldt ook voor je eigen custom keys.
3.1: Identiteit en host
| Key | Default | Doel |
|---|---|---|
id |
, | Vrije naam voor de app, gebruikt door dashboard en logs |
host |
null |
Vhost waarop deze entrypoint mag draaien. Phlo weigert verzoeken die niet matchen, geen accidentele cross-host responses |
3.2: Modes: `build`, `debug`, `auth`, `thread`
| Key | Default | Effect |
|---|---|---|
build |
false |
Schakelt build/lint/reflect CLI in én laat Phlo per request gewijzigde sources detecteren en hercompileren |
debug |
false |
Laadt debug.php, activeert debug() / dx() / debug-helpers, geeft volledige stacktraces in plaats van generieke 500's |
auth |
false |
Site-wide HTTP Basic-auth via credentials in data/auth.ini |
thread |
false |
Worker-mode (zie X.7), true = onbeperkt, integer = aantal requests per worker |
Niet combineren: build: true en thread: true. Build schrijft files tussen requests; in een long-running worker is dat unsafe. Phlo gooit een runtime-error als beide aan staan.
Niet combineren: auth: true zonder build: true. Phlo verifieert dat ook.
3.3: Paden
Alleen app is verplicht. De rest valt terug op subdirs van app:
| Key | Default | Bedoeld voor |
|---|---|---|
app |
, (verplicht) | App-root |
data |
<app>/data/ |
Config (app.json, auth.ini), credentials, runtime-state |
php |
<app>/php/ |
Gegenereerde PHP, alleen aanpassen als release-output ergens anders staat |
www |
<app>/www/ |
Web-root |
Je release-entrypoint zet typisch app: '<app>/release/' en php: '<app>/release/' zodat release-output zonder build mode wordt geserveerd.
3.4: Dashboard en WebSocket
| Key | Default | Effect |
|---|---|---|
control |
'phlo' bij build+debug, anders false |
URL-prefix waar de control-UI onder leeft. Bijv. 'beheer' → /beheer; false = uit |
websocket |
null |
Poort waarop PhloWS draait voor deze app. Wordt constante websocket, gebruikt door wsCast() |
De control-UI vereist build: true. Zie hoofdstuk WebSocket voor PhloWS-setup.
3.5: Composer-autoload
Wil je PHP-packages uit vendor/ gebruiken? Geef het pad op:
phlo_app (
composer: '/srv/example.nl/',
...
)
Phlo registreert dan een lazy autoloader die <composer>/vendor/autoload.php pas inlaadt wanneer een onbekende class moet worden geresolved. Geen cold-start kost als je geen Composer-packages aanraakt; volledige Composer-autoload zodra je dat wel doet.
Conventie: composer: '<app>/data/' (de composer.json en vendor/ staan dan in data/, buiten de webroot).
3.6: Trace en CLI
| Key | Default | Effect |
|---|---|---|
trace |
false |
Zet trace-mode aan, zie hoofdstuk Trace |
cli |
'php-zts' (als ZTS) of 'php' |
Pad naar de PHP-binary die Phlo gebruikt voor sub-processes (build, tasks, websocket). Override als je systeem niet-standaard PHP-binaries heeft |
cliis in v4 een string (pad), niet een boolean zoals in v1.
3.7: Worker-mode regels
thread: true zet Phlo in long-running worker-mode (FrankenPHP, ReactPHP, RoadRunner). De runtime blijft tussen requests in geheugen. Drie regels die je moet kennen:
1. Geen die() of exit() in het HTTP-pad. Beide killen de hele worker. Gebruik return of laat een terminating call (view(), apply(), location()) de response sturen.
2. Geen request-state in static properties. Statics overleven tussen requests in een worker; user-data van request A lekt naar request B. Voor caches die wél worker-safe zijn (class-structuur, computed metadata), kun je statics gebruiken, niet voor session-, user-, payload-, time- of DB-state.
3. Mark long-lived objects met $objPers = true. Default cleart Phlo zijn instance-map tussen requests. Voor objecten die je expliciet wil hergebruiken (DB-connectie, prepared statements), zet $this->objPers = true zodat de cleanup ze met rust laat.
3.8: Custom keys: eigen constants
Elk extra named-argument dat je aan phlo_app() geeft wordt automatisch een PHP-constante. Dat is dé manier om app-brede paden of feature-flags te declareren:
phlo_app (
app: '/srv/example.nl/',
langs: '/srv/example.nl/langs/',
files: '/srv/example.nl/files/',
uploads: '/srv/example.nl/data/uploads/',
)
Daarna direct bruikbaar in .phlo:
$dict = parse_ini_file(langs.'en.ini')
$path = files.'avatars/'.$user->id.'.jpg'
$url = uploads
reflect::runtime toont alle gedefinieerde constants. Handig om te ontdekken welke een app aanbiedt zonder www/app.php te openen.
3.9: Voorbeeld: dev en release naast elkaar
phlo_app (
id: 'Example',
host: 'dev.example.nl',
auth: true,
build: true,
debug: true,
dashboard: 'phlo',
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 heeft build, debug, auth, dashboard en trace. Release heeft worker-mode (thread) en wijst webroot/php/php-output naar release/. data, composer, langs blijven hetzelfde, dat is gedeelde state.
4: Syntax & Structuur
Phlo compileert .phlo bronbestanden naar gewone PHP-klassen en gegenereerde assets. De syntaxis is PHP-achtig, maar zonder puntkomma's aan het einde van statements.
4.1: Bestandstructuur
Een .phlo bestand kan top-level controllercode en nodes bevatten:
routefunctionpropstaticmethodview<style><script>
Voorbeeld:
route both GET home => view($this)
prop title = 'Welkom'
view:
<h1>$this->title</h1>4.2: Statements
Statements eindigen op een regel einde, niet op een puntkomma:
$count = 1
if ($active) $count++
Bij multiline calls moet elke argumentregel met een komma eindigen:
apply (
title: 'Klaar',
main: '<p>Gereed</p>',
)4.3: Controllercode
Top-level code die geen node is, wordt controllercode van de gegenereerde class. Die code draait wanneer de instance voor het eerst wordt opgehaald.
prop ready = false
$this->ready = true
Gebruik controllercode voor lichte initialisatie. Zet requestlogica liever in routes en methods.
4.4: Instanties
Gebruik %naam om een Phlo-object via de instance manager op te vragen:
%payload->name
%session->user
%creds->mysql->database
De instance wordt lazy aangemaakt en daarna hergebruikt binnen de request.
4.5: Props
Statische prop:
prop title = 'App'
Computed prop:
prop fullName => $this->first.' '.$this->last
Computed props worden als getter gegenereerd en kun je als property lezen:
$this->fullName4.6: Methods
Singleline method:
method label($value) => ucfirst($value)
Multiline method:
method label($value){
if (!$value) return void
return ucfirst($value)
}4.7: Functies
Globale functies definieer je met function:
function initials($name){
return strtoupper(substr($name, 0, 1))
}
Gebruik dit spaarzaam. Voor appcode is een gewone app-class meestal beter; alleen frameworkbrede helpers horen als runtime-resource.
4.8: Statics
Static value:
static table = 'users'
Static method:
static label($value) => ucfirst($value)
Aanroep:
user::label('jordi')4.9: Strings en operators
Strings en operators volgen PHP:
$name = 'Jordi'
$title = "Hallo $name"
$active = $count > 0 && !$archived
Gebruik void voor een lege string wanneer dat de intentie duidelijk maakt.
4.10: Named arguments
Named arguments werken zoals in PHP en maken calls leesbaar:
%DB->load (
table: 'users',
where: 'active=1',
order: 'name',
)4.11: Error-afhandeling
Gebruik error(...) voor fouten die door de Phlo runtime/debugger afgehandeld moeten worden:
if (!$record) error('Record niet gevonden')
debug: true staat in www/app.php en activeert uitgebreide debug-output.
5: Routing
Routing in Phlo koppelt een spatie-gescheiden pad + HTTP-methode aan een target (meestal een method).
Routes uit alle .phlo-bestanden worden verzameld; de router wordt geactiveerd met app::route().
5.1: Basisvorm
route [async|both] [GET|POST|PUT|DELETE|PATCH] pad [pad2 ...] => target
- Paden schrijf je met spaties (geen
/in de route-definitie). - Target: een directe call of statement blok.
Voorbeeld:
route GET home => $this->main
method main => view($this->home)5.2: sync / async / both
| Keyword | Gedrag |
|---|---|
| (weglaten) | Alleen sync (gewone HTTP) |
async |
Alleen async (requests van Phlo-frontend) |
both |
Sync én async toegestaan |
route both GET data => $this->loadData
route async POST items save => $this->saveItems5.3: Variabelen
Phlo parse’t ieder pad-segment. Segmenten die met $ beginnen zijn variabelen met extra mogelijkheden:
4.3.1 Verplicht (doorgeven aan target)
route GET user $id => $this->showUser($id)
method showUser($id) => view($this->profile)
4.3.2 Optioneel presence met ? → boolean
route GET search $full? => $this->search($full)
- Matcht
/searchen/search/full. $fullis true wanneer het segmentfullaanwezig is, anders false. (De implementatie checkt letterlijk of het request-segment gelijk is aan de naam zonder?.)
4.3.3 Rest (variabele lengte) met =*
route GET file $path=* => $this->serveFile($path)
- Matcht alle resterende segmenten als één string in
$path.
4.3.4 Default-waarde met =
route GET page $slug=home => $this->page($slug)
- Zonder segment →
$slug = 'home'. - Met segment →
$slug = '<waarde>'.
4.3.5 Lengte-eis met .N
route GET code $pin.6 => $this->enter($pin)
- Matcht alleen als de lengte van
$pinexact 6 is.
4.3.6 Keuzelijsten met :a,b,c
route GET report $range:daily,weekly,monthly => $this->report($range)
- Het segment moet één van de opgegeven waarden zijn.
- Bij afwezigheid (leeg) en met default wordt de default toegepast.
- Anders faalt de match.
Je kunt deze vormen combineren. Voorbeelden:
Enum met verplicht id:
route GET export $fmt:csv,json $id => $this->export($fmt, $id)
Enum met default:
route GET theme $name:light,dark=light => $this->theme($name)5.4: Payload check met `@`
Je specificeert exacte body-keys met één @ en comma-separated lijst.
De router vergelijkt dit 1-op-1 met de keys uit %payload (exacte set; volgorde zoals de engine die aanlevert).
route POST user @name,email => $this->createUser
method createUser => dx(%payload->name, %payload->email)
Body-keys bind je niet als method-parameters; je leest ze via
%payload.
5.5: Targets
Lokale method
route GET profile show => $this->show
method show => view($this->profile)
Externe class-method (static)
route GET api version $major => api::getVersion($major)
- Static calls: haakjes verplicht, ook zonder arguments.
- Geef pad-variabelen expliciet door.
5.6: Router activeren
Routes worden pas gematcht na:
app::route()
Plaats deze call bijvoorbeeld in app.phlo (of een andere centrale controller) na je app initialisatie en voor een fallback voor 404 afhandeling.
5.7: Aanbevolen structuur
- Zet routes bovenaan per bestand.
-
Houd pad en methodenaam logisch in lijn:
route GET users list => $this->listUsers route POST users add => $this->addUser - Variabelen altijd doorgeven in target.
bothalleen wanneer een endpoint bewust zowel sync als async moet zijn.- Gebruik keuzelijsten
:…i.p.v. losseif-takken voor vaste varianten.
6: Views
Views staan direct in .phlo-bestanden. Een view compileert naar een PHP-methode die HTML teruggeeft. Je rendert een view met view(...).
6.1: Declaratie
Naamloze view:
view:
<p>Test</p>
Named view:
view home:
<h1>Welkom</h1>
View met argumenten:
view greeting($name):
<p>Hallo $name</p>
Aanroepen:
method show => view($this->greeting('Jordi'))6.2: Multiline blokken
Een multiline view loopt door tot een lege regel. Zet dus geen lege regels midden in view-HTML.
view:
<section>
<h1>Welkom</h1>
<p>Intro</p>
</section>
view footer:
<footer>Phlo</footer>6.3: HTML shorthand
Phlo ondersteunt compacte id/class shorthand:
view:
<p#intro.lead/>
Dit wordt:
<p id="intro" class="lead"></p>
Een trailing slash maakt een tag self-closing in de bron en Phlo schrijft daar een normale open/sluit-tag van.
6.4: Tekst en variabelen
Gewone variabelen en eenvoudige properties kun je direct in tekst gebruiken:
view($name):
<p>Hallo $name</p>
<p>$this->title</p>
Voor method calls, chained access of expressies gebruik je {{ ... }}:
view:
<p>{{ $this->label('start') }}</p>
<p>{{ $this->record->title }}</p>
<p>{{ $this->count > 1 ? 'Meerdere' : 'Een' }}</p>
{( ... )} bestaat als korte expressievorm en wordt intern naar {{ (...) }} vertaald, maar gebruik hem niet als standaardvoorbeeld. In documentatie en appcode is {{ ... }} meestal duidelijker.
6.5: Vertaalbare viewtekst
Voor statische vertaalbare tekst gebruik je de taalshorthand:
view:
<h1>{nl: Welkom}</h1>
<p>{nl: Hallo wereld}</p>
Met argumenten:
view($name):
<p>{nl: Hallo %s ($name)}</p>
Gebruik hiervoor de shorthand; die is korter en laat direct zien welke brontaal de tekst heeft.
6.6: Attributen
Attribuutwaarden zonder spaties of variabelen mogen zonder quotes:
view:
<a href=/contact>Contact</a>
Met variabelen of expressies gebruik je quotes:
view:
<a href="$this->url">Link</a>
<a href="{{ $this->url('contact') }}">Contact</a>6.7: Control flow
Gebruik control-flow tags op eigen regels:
view:
<ul>
<foreach $this->items AS $item>
<li>$item->title</li>
</foreach>
</ul>
Met if:
view:
<if $this->active>
<p>Actief</p>
<else>
<p>Inactief</p>
</if>6.8: Renderen
view(...) rendert de response en stopt de verdere request-afhandeling. Bouw samengestelde pagina's dus in een enkele view of laat een view andere view-methodes inline opnemen met {{ ... }}.
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>6.9: Apply-commando's
apply() accepteert named arguments waarvan elke key een DOM-mutatie of UI-actie is. De runtime-kern levert de basis; resources kunnen extra commando's registreren via app.mod.<naam> (zoals DOM/toasts voor toast: of DOM/dialog voor alert:).
DOM-mutaties
| 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 of array | Verwijder elementen |
attr |
{selector: {attr: value}} |
Zet/verwijder (null = verwijder) |
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 (vervangt) |
settings |
Body data-attributes |
path |
history.pushState (URL wijzigt zonder reload) |
trans |
View-transition class (forward/backward/...) |
scroll |
int (pixels) of #anchor |
Assets (eenmalig per href/src)
css, js, defer, voeg link of script toe; al geladen URL's worden genegeerd.
Navigatie en callbacks
| Cmd | Effect |
|---|---|
location |
Path of true (huidige path opnieuw); externe URL doet location.assign() |
call |
Roep app[name]() aan na de apply |
Meta
| Cmd | Effect |
|---|---|
log / error |
console.log / console.error op de client |
phlo |
Server-side debug-trace, logged in browser console (debug-mode) |
Resource-mods, beschikbaar zodra de bijbehorende resource is geladen:
| Cmd | Resource |
|---|---|
toast |
DOM/toasts |
alert / confirm / prompt |
DOM/dialog |
store |
DOM/store |
setvar |
DOM/CSS.var |
template |
DOM/template |
Geen build-time check op apply-keys. Een typo (
inner→innr) wordt stil genegeerd. Houd deze tabel bij de hand, of consulteer/srv/phlo/docs/apply-protocol.mdvoor de volledige actuele referentie inclusief edge-cases en stream-semantiek.
Voorbeeld dat meerdere commando's combineert:
route async POST item save {
if (!$item = item::save(%payload)) return apply(
error: 'Opslaan mislukt',
class: ['[name=title]' => '!error'],
)
apply(
outer: ['#item-'.$item->id => $this->itemView($item)],
toast: 'Opgeslagen',
scroll: '#item-'.$item->id,
trans: 'fade',
)
}7: CSS
Phlo gebruikt een compacte, puntkomma-vrije CSS-syntaxis binnen <style>-blokken.
Je schrijft regels met dubbele punten als scheiding:
selector: property: value- genest:
A: B: property: value→ output:A B { property: value; }
Regels:
- Één regel = één declaratie. Geen puntkomma’s in de bron; de engine voegt ze toe.
- Dubbele punt
:scheidt ketenniveaus én property van value. - Backslash
\in nestings “plakt” het volgende selector-deel aan de parent (glue). Dat volgende deel kan een pseudo (:…), attribute-selector ([…]), enz. zijn. @media (…)mag ín een selector-blok staan; Phlo hoist dit naar de juiste plek met behoud van de huidige selector.
7.1: `<style>`-blok
<style>
html: height: 100dvh
body {
background: #947b6c
font-family: Sans-serif
p: line-height: 2em
}
</style>
Output (conceptueel):
html { height: 100dvh; }
body { background: #947b6c; font-family: Sans-serif; }
body p { line-height: 2em; }7.2: Keten & groepen
Keten met dubbele punten; groepeer met komma, de volledige context wordt per item toegepast.
<style>
body: h1, p: \:first-letter: color: green
</style>
- Context:
body - Doelen:
h1enp(met gelijmde:first-letter) - Backslash vóór
:first-letterlijmt dat deel aan de voorgaande selector binnen de keten.
Output:
body h1:first-letter,
body p:first-letter { color: green; }7.3: Media queries ín selector
Je mag @media (…) binnen het selector-blok schrijven; Phlo verplaatst het naar de juiste plek en behoudt de selector-context:
<style>
h1 {
color: white
@media (max-width: 768px): color: black
}
</style>
Output:
h1 { color: white; }
@media (max-width: 768px){
h1 { color: black; }
}7.4: Variabelen
Phlo ondersteunt CSS-variabelen via $namen.
Je kunt variabelen definiëren in :root, of op elk ander niveau, maar :root is gebruikelijk voor globale theming.
<style>
:root {
$background: #0d0d0d
$surface: #1a1a1a
$text: #ffffff
$accent: #ff4a00
}
body {
background: $background
color: $text
}
button {
background: $accent
color: $text
}
</style>
Output
:root {
--background: #0d0d0d;
--surface: #1a1a1a;
--text: #ffffff;
--accent: #ff4a00;
}
body {
background: var(--background);
color: var(--text);
}
button {
background: var(--accent);
color: var(--text);
}
👉 Phlo zet $variabelen automatisch om naar --custom-properties en gebruikt var(--...) bij aanroepen.
Je kunt variabelen overal hergebruiken, ook binnen media queries en nested selectors.
7.5: Dynamische variabelen
Phlo’s frontend engine bevat de library DOM/CSS.var, waarmee je gedefinieerde $variabelen in CSS direct kunt benaderen en aanpassen vanuit JavaScript, via het globale app.var object.
Elke $variabele in je CSS wordt automatisch beschikbaar onder app.var.<naam>.
Voorbeeld
<style>
:root {
$background: #0d0d0d
$text: #ffffff
}
</style>
<script>
app.var.background = '#000000'
const textColor = app.var.text
</script>
app.var.background = '#000000'→ past live de waarde van--backgroundin de DOM aan, zonder rebuild of reload.const textColor = app.var.text→ leest de huidige waarde terug.
👉 Deze aanpassingen werken real-time in de browser en beïnvloeden direct alle elementen die de variabele gebruiken.
Je kunt dit gebruiken voor o.a.:
- Themawissels (dark/light mode)
- Dynamisch aanpassen van accentkleuren op basis van user input
- Interactieve UIs zonder aparte CSS klassen togglen
Werking
- De CSS-engine zet
$backgroundom naar--background. - De frontend-engine leest/schrijft deze via
document.documentElement.style. app.varbiedt een eenvoudig proxy-object zodat je kunt werken alsof het gewone JS-properties zijn.
7.6: Volledig voorbeeld
Input
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
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
- Gebruik
$variabelenvoor kleuren, spacing en fonts, dit maakt theming en dark/light modes eenvoudig. - Definieer globale thema-variabelen in
:root. - Gebruik selector-ketens en grouping voor compacte, leesbare code.
- Plaats
@mediagewoon in het blok; Phlo zet ze netjes op de juiste plek. - Gebruik
\in nestings om pseudo’s of attributen vast te plakken aan de parent selector. - Geen puntkomma’s in je code; Phlo zorgt voor correcte CSS-output.
8: ORM
Phlo bevat een krachtige ingebouwde ORM waarmee je database-tabellen als klassen definieert.
Modellen kunnen snel via columns of uitgebreid via een declaratief schema.
Records worden als instances behandeld, met ondersteuning voor props, methods, views, relaties en meerdere database-engines.
8.1: Basisprincipes
Een ORM-model is een .phlo-bestand met:
@ class:naam van het model (en tabel)@ extends: modelstatic tableencolumnsofschema- Optioneel: relaties (
parent,child,many) - Props, methods en views werken per record-instance
Voorbeeld:
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static columns = 'id,name,email,active,created'8.2: Modellen definiëren
7.2.1 Plat met columns (snel en licht)
Gebruik columns voor eenvoudige tabellen:
@ 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']
@ 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 Met schema en field(...) (rijk en declaratief)
Met schema definieer je velden, relaties en UI in één keer:
@ 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),
)
@ 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'),
)
schemais vooral krachtig in combinatie met PhloCMS, maar werkt ook standalone.
8.3: CRUD
Ophalen:
$user = user::record(id: 1)
$list = shipment::records(order: 'created DESC')
Aanmaken:
$shipment = shipment::create(destination: 'Parijs', user: 1)
Bewerken en opslaan:
$shipment->destination = 'Lyon'
$shipment->objSave
Verwijderen:
shipment::delete('id=?', $shipment->id)
record(...)→ enkel record (of null)records(...)→ array van records (class instances)create(...)→ insert + instant ophalenobjSave→ instance opslaan (insert/update afhankelijk van id)delete(...)→ statisch verwijderen met SQL-where
8.4: Relationele navigatie
Relaties zijn beschikbaar via properties:
| Type | Declaratie | Gebruik |
|---|---|---|
| parent | type: parent |
$shipment->user |
| child | type: child |
$user->shipments |
| many | type: many |
$user->groups |
Many-to-many
type: many gebruikt een pivot-tabel:
groups: field (
type: 'many',
obj: 'group',
table: 'user_groups',
)
Navigatie:
$user = user::record(id: 1)
foreach ($user->groups as $group)
echo $group->title
Relaties worden batch-loaded voor performance. Er vinden geen cross-DB joins plaats; elke class laadt uit zijn eigen engine.
8.5: Instance dynamiek
Elke record is een echte instance van je modelklasse. Je kunt props, methods en views gebruiken om virtuele velden, berekeningen of representaties toe te voegen:
@ 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>
Gebruik:
$shipment = shipment::record(id: 'AB12')
echo $shipment->summary
echo $shipment
Een record als string gebruiken roept de viewrepresentatie aan.
Props en methods werken altijd op de record-instance, niet statisch.
8.6: Filtering en queries
Alle querymethodes accepteren named arguments en SQL-achtige filters:
shipment::records(destination: 'Parijs')
shipment::records(where: 'valid=1 AND weight>10')
shipment::pair(columns: 'id,destination')
Ondersteund: where, order, group, joins, caching en schema-bewuste kolommen.
8.7: Caching en prestaties
De ORM gebruikt interne buffers (objRecords, objLoaded) voor relationele lookups en optionele APCu caching via:
static objCache = true
Of een aantal seconden:
static objCache = 600
Records en relaties worden per batch geladen.
Gebruik records() voor bulkselecties i.p.v. record() in lussen.
8.8: Meerdere engines
Phlo ondersteunt meerdere backends via prop DB.
Standaard is %MySQL, maar je kunt elke engine per model instellen.
SQLite
prop DB => %SQLite(data.'users.db')
@ class: notes
@ extends: model
prop DB => %SQLite(data.'notes.db')
static table = 'notes'
static columns = 'id,title,body'
PostgreSQL
prop DB => %PostgreSQL
@ class: invoices
@ extends: model
prop DB => %PostgreSQL
static table = 'invoices'
static columns = 'id,customer_id,total,created'
Tabellen op verschillende engines kunnen gecombineerd worden in relaties; elke class haalt zijn eigen data op.
/data/creds.ini
Voor engines zoals MySQL en PostgreSQL plaats je je credentials in:
/data/creds.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 laadt deze automatisch via %creds->....
8.9: Opt-in features: audit, validatie, custom PK
Een @ extends: model levert standaard CRUD + identity-map + relaties + soft-delete. Drie extra opt-ins activeer je per model met een static flag.
X.9.1 Audit-log
static objAudit = true
Vanaf dan logt elke create, objSave (update) en delete naar een audit-tabel via de security/audit resource:
| Operatie | Wat er gelogd wordt |
|---|---|
create(...) |
volledige nieuwe waarden |
objSave (update) |
diff: alleen gewijzigde velden, from → to |
delete(...) |
volledige oude waarden, per geraakt record |
Setup:
security/audittoevoegen aandata/app.jsonresources.- Eenmalig de schema-SQL importeren:
mysql <database> < /srv/phlo/resources/security/audit.sql.
Sensitive fields uitsluiten:
method afterCreate => %audit->log($this, 'create', [], (array)$this, exclude: ['password_hash'])
Toggle per omgeving, alleen in dev:
static objAudit => debug
Of alleen in release:
static objAudit => !debug
(debug is de runtime-constante uit phlo_app(debug: ...).)
X.9.2 Validatie
static objValidate = true
Vóór create() draait Phlo per veld in static schema() de bijbehorende objValidate($value). Bij errors: create() returnt null, errors via Class::objErrors():
if (!user::create($args)){
return apply(errors: user::objErrors())
}
Field-rules in schema():
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-validatie: override method objValidate($value) in een eigen field-subclass.
X.9.3 Custom primary key
Default is id (auto-increment integer). Overschrijf voor andere PK's:
static idColumn = 'sku'
static idType = 'string'
Effect:
- Identity-map gebruikt
skuals key Class::record(sku: 'XYZ-123')(nietid:)- Bij
create(): PK-waarde moet je zelf meegeven (geen auto-increment) $record->idwerkt niet, gebruik$record->sku
X.9.4 Combineren
De drie opt-ins zijn onafhankelijk en mogen samen:
@ class: giftcard
@ extends: model
static objAudit = true
static objValidate = true
static idColumn = 'sku'
static idType = 'string'
Backwards-compat: alle defaults blijven false / 'id' / 'int'. Bestaande modellen werken onveranderd.
8.10: Overzicht functies
| Functie / Property | Type | Beschrijving |
|---|---|---|
record(...) |
static | Haalt 1 record (of null) |
records(...) |
static | Haalt meerdere records (array) |
create(...) |
static | Insert + ophalen |
objSave |
instance | Insert of update |
delete(where, …) |
static | Verwijderen |
pair, item, column |
static | Snelle queryhelpers |
objParents / schema: parent |
declaratief | Parent-relaties |
objChildren / schema: child |
declaratief | Child-relaties |
schema: many |
declaratief | Many-to-many |
objCache |
static | Optionele APCu caching |
prop DB |
static | Per-model engine |
8.11: Best practices
- Gebruik
columnsvoor snelle, simpele modellen. - Gebruik
schemavoor rijke definities en CMS-integratie. - Definieer een
viewvoor stringrepresentaties. - Gebruik props voor virtuele velden.
objSavei.p.v.save.- Gebruik
records()bij bulkloads. - Scheid modellen per engine met
prop DB. - Gebruik credentials in
/data/creds.ini. - Houd modellen declaratief; logica in props/methods.
9: Instance Management
Phlo gebruikt een eigen instance manager om objecten efficiënt en voorspelbaar te initialiseren en te hergebruiken. Dit systeem bepaalt wanneer controllercode wordt uitgevoerd, hoe instanties worden opgeslagen en hoe cirkelreferenties worden voorkomen.
9.1: Basisuitleg
Wanneer je een .phlo-bestand definieert, wordt dit tijdens de buildfase omgezet naar een class.
Elke aanroep naar een object via %naam gaat via de instance manager (phlo() in /phlo/phlo.php).
Voorbeeld:
prop title = 'Welkom'
route GET home => $this->main
method main => view($this->home)
view home:
<h1>$this->title</h1>
Wanneer de route /home wordt aangeroepen:
- De instance manager kijkt of er al een instantie van deze class bestaat.
- Zo niet, dan wordt deze aangemaakt en opgeslagen.
- Na het aanmaken wordt de controllercode uitgevoerd (zie §8.2).
- Vervolgens wordt de gevraagde method aangeroepen.
9.2: Controllercode
Alle code in een .phlo-bestand die niet onder route, prop, static, method, function, view, <style> of <script> valt, is controllercode.
Deze code wordt na instantiatie uitgevoerd, zodra de instantie volledig bestaat.
Voorbeeld:
prop ready = false
%session->start()
$this->ready = true
De laatste twee regels zijn controllercode omdat ze top-level staan.
- Deze code draait één keer bij de eerste aanmaak van de instantie.
- Het verschil met
__constructis dat de instantie volledig bestaat op het moment dat controllercode wordt uitgevoerd → zo voorkom je cirkelreferenties en incomplete objecten.
9.3: De rol van `__handle()`
Phlo genereert voor elke class een speciale __handle() methode.
Deze wordt aangeroepen door de instance manager wanneer een instantie wordt opgevraagd via %naam.
__handle():
- voert de controllercode uit als dat nog niet gebeurd is;
- zorgt dat props en statics correct beschikbaar zijn;
- zorgt voor caching van de instantie voor volgende aanroepen.
Je hoeft __handle() zelf niet aan te roepen of te overschrijven, het is onderdeel van de gegenereerde klasse en de instance manager.
9.4: Lazy initialisatie
Omdat controllercode pas na constructie wordt uitgevoerd, kunnen instanties naar elkaar verwijzen zonder dat er ongewenste recursieve aanmaak plaatsvindt.
Voorbeeld:
a.phlo:
prop message = 'A ready'
b.phlo:
prop message = 'B ready'
main.phlo:
route GET test => $this->show
method show {
dx(%a->message, %b->message)
}
%aen%bworden lazy aangemaakt.- De controllercode in beide bestanden draait zodra hun instantie volledig bestaat.
- Je kunt probleemloos onderling verwijzen, want de instantie bestaat al vóór de controllercode draait.
9.5: Best practices
- Gebruik controllercode voor initiële setup, niet voor request-afhankelijke logica.
- Zet controllercode bovenaan of direct onder props voor overzicht.
- Vermijd side-effects in
__construct; gebruik controllercode i.p.v. custom constructors. - Laat instanties zichzelf lazy initialiseren via
%naamin plaats van handmatige aanmaak. - Gebruik controllercode bewust om cirkelreferenties op te lossen.
10: Tooling & CLI
Phlo-apps hebben een CLI-laag voor build, lint, release en reflectie. Gebruik die altijd via de dev-entrypoint van de app.
10.1: Build commands
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 hoort een lege array terug te geven:
[]
Als lint fouten meldt, wijzig je de .phlo bron en bouw je opnieuw. Niet de gegenereerde PHP repareren.
10.2: Reflect commands
Reflectie helpt om een app te begrijpen voordat je wijzigt:
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
Deze commands geven JSON terug en zijn bedoeld voor ontwikkelomgevingen met build: true.
10.3: Algemene CLI-dispatch
build:: en reflect:: zijn slechts twee voorbeelden van een algemener mechanisme. Phlo's CLI kan elke static, method of function in je app aanroepen:
php www/app.php tasks::run # static method op een class
php www/app.php app.heartbeat # method op een Phlo-instance
php www/app.php answer "is een paling een vis" # globale function
Drie patronen:
| Pattern | Dispatch | Voorbeeld |
|---|---|---|
Class::method args |
Static method op de class | tasks::run, backup::nightly |
object.method args |
Instance method via phlo(object) |
app.heartbeat, cms.reindex |
function args |
Globale function | answer "vraag" |
Output gaat als JSON naar stdout, errors naar stderr met non-zero exit-code. Dat maakt elke routine in je app direct bruikbaar vanuit cron, deploy-scripts, monitoring of een terminal, zonder dat je er een aparte CLI-laag voor hoeft te bouwen.
Alleen beschikbaar als build: true in www/app.php. Niet draaien tegen een live productie-omgeving.
10.4: Debug helpers
Met debug: true in phlo_app(...) activeert Phlo een set helpers voor inspectie tijdens dev. In productie zijn ze inert.
| Helper | Doel | Gedrag |
|---|---|---|
debug(...$args) |
Dump waarden naar de debug-laag van de response | Verschijnt onderaan de HTML in dev, of als debug:-payload in async-responses |
dx(...$args) |
Dump + abort | Throwt een DebugException, netjes te lezen in dev. In v4 throwt dx() in plaats van die() zoals in v1, dat maakt hem worker-safe |
error($msg, $code = 500) |
Runtime-error voor de Phlo exception-handler | Throwt een PhloException, gelogd in data/errors.json |
trace($node, $args) |
Manuele trace-event (alleen actief met trace: true) |
Voegt een event toe aan de trace-log, zie hoofdstuk Trace |
method buildReport {
$data = $this->load
debug('loaded', count: count($data))
if (!$data) error('Geen data om te rapporteren')
dx($data[0])
}
dx() is je primaire "stop en kijk wat er staat"-tool tijdens ontwikkelen. Vergeet je een dx() in code die naar release gaat? In debug: false mode gedraagt hij zich net als error(), geen silent passthrough.
10.5: Werkwijze
- Lees eerst de bron en reflectie-output.
- Pas alleen
.phlo,data/app.jsonof entrypoints aan. - Draai
build::run. - Draai
build::lint. - Test de relevante HTTP-routes.
- Draai
build::releaseals de stage/release-output bijgewerkt moet worden.
10.6: Dev, stage en productie
Dev heeft meestal:
auth: true,
build: true,
debug: true,
In build+debug-mode staat de ingebouwde control-UI standaard op /phlo; met control: 'pad' in phlo_app(...) kies je een ander pad. Stage/productie heeft meestal geen build en geen debug. De webroot wijst naar release/www/.
10.7: HEAD en async
De huidige runtime ondersteunt normale HTTP-methodes inclusief HEAD. Async requests worden door de frontend-resource afgehandeld en gebruiken dezelfde routes als sync requests, tenzij je een route expliciet anders declareert.
11: Vertalingen
Phlo gebruikt de lang resource voor meertalige viewtekst en vertaling. In views schrijf je statische tekst bij voorkeur met de compacte taalshorthand.
11.1: View shorthand
De normale vorm voor statische vertaalbare tekst in een view is:
view:
<p>{nl: Hallo wereld}</p>
De taalcode voor de dubbele punt is de brontaal van de tekst. Als %app->lang dezelfde taal is, wordt de tekst direct getoond. Als de actieve taal anders is, gebruikt %lang de vertaalcache en plant ontbrekende vertalingen async in.
Met argumenten:
view($name):
<p>{nl: Hallo %s ($name)}</p>
Gebruik hiervoor de view shorthand; die is korter en laat direct zien welke brontaal de tekst heeft.
11.2: Helpers in code
De huidige lang resource levert globale helperfuncties voor Nederlands en Engels:
method title => nl('Welkom')
method intro => en('Build compact full-stack apps')
Die helpers zijn handig in methodes, props of controllercode. In views blijft de shorthand meestal duidelijker:
view:
<h1>{nl: Welkom}</h1>
<p>{en: Build compact full-stack apps}</p>11.3: Actieve taal
De actieve taal staat op %app->lang. Een route kan deze taal zetten voordat de view wordt gerenderd:
route both GET $lang:nl,en=nl guide {
%app->lang = $lang
view($this)
}
In links kun je %lang gebruiken als objectwaarde om de huidige taal te tonen of te verwerken.
11.4: Vertaalcache
Vertalingen worden per taal uit langs/ geladen via %INI(%app->lang, langs). Ontbrekende regels worden op basis van hun hash async vertaald en later uit de cache gelezen.
De kernmethodes zijn:
%lang->translation('nl', 'Hallo wereld')
%lang->translate('nl', 'en', 'Hallo wereld')
Gebruik translation() voor normale app-weergave met cache en async aanvulling. Gebruik translate() alleen wanneer je bewust direct een losse vertaalactie wil uitvoeren.
11.5: Best practices
- Gebruik
{nl: ...}en{en: ...}in views voor vaste tekst. - Gebruik
nl()enen()in PHP/Phlo-code buiten views. - Zet
%app->langvroeg in de route of centrale controller. - Houd de brontekst stabiel; gewijzigde tekst krijgt een nieuwe hash.
- Documenteer alleen taalhelpers die echt als resourcefunctie bestaan.
12: Geavanceerd
Phlo blijft bewust modulair. Je kunt een app klein houden en alleen de resources activeren die de app nodig heeft, of juist meerdere bronpaden en resourcegroepen combineren.
12.1: Appcode en runtime-resources
App-specifieke code hoort in de app zelf. Zet die niet in /srv/phlo/resources/.
/srv/phlo/resources/ is de Phlo runtimecatalogus: frameworkbrede resources die door meerdere apps gedeeld mogen worden en bewust met de runtime worden onderhouden. Alleen generieke, stabiele code hoort daar naartoe.
Wil je code tussen apps delen, maak dan eerst een expliciete gedeelde module of app-bibliotheekpad met duidelijke eigenaar. Promoveer code pas naar de Phlo runtimecatalogus als het echt frameworkfunctionaliteit is.
Een runtime-resource kan een object, functie, style of script leveren. Metadata bovenin het bestand helpt dashboard en manual:
@ summary: Send app notifications
@ package: notifications
@ frontend: false
@ backend: true
method send($message){
return HTTP(%creds->notify->url, POST: ['message' => $message])
}12.2: Meerdere bronpaden
Hou de standaard eenvoudig: appbron in het app-pad. Voeg extra paden alleen toe als een codebase echt gedeeld moet worden.
Padkeuzes horen voorspelbaar te blijven:
- appbron in
/srv/example.nl/ - runtime in
/srv/phlo/ - release in
/srv/example.nl/release/ - data en credentials in
/srv/example.nl/data/
12.3: Integratie met bestaande PHP
Gebruik Phlo naast bestaande PHP door de runtime te laden en alleen de Phlo-entrypoint verantwoordelijk te maken voor routes die de app afhandelt. Bestaande statische bestanden blijven direct door de webserver geserveerd.
<?php
require('/srv/phlo/phlo.php');
phlo_app (
id: 'Legacy',
host: 'dev.legacy.test',
build: true,
debug: true,
app: '/srv/legacy/',
);12.4: Security en visitors
Voor publieke sites is de gebruikelijke basis:
{
"resources": [
"cookies",
"security/security",
"security/token",
"session",
"useragent",
"visitors",
"phlo.async",
"DOM/form"
]
}
Voor lokale dev kun je tracking uitsluiten:
{
"exclude": [
"visitors",
"useragent",
"wsCast"
]
}12.5: Cookiewall: GDPR consent
DOM/cookiewall is een ingebouwde, subtiele consent-banner. Activeren in 3 stappen:
1. Resource in data/app.json:
{ "resources": [..., "DOM/cookiewall"] }
2. Banner in je layout:
view layout:
<body>
{{ %cookiewall->banner }}
<main>...</main>
</body>
De banner verschijnt alleen als de bezoeker nog geen keuze heeft gemaakt. Twee knoppen: "Essential only" en "Accept". De keuze wordt opgeslagen in een cookie cookieChoice ('essential' of 'all'), 1 jaar geldig.
3. Guard rond tracking:
<if %cookiewall->canTrack>
<script src="https://analytics.example.com/script.js"></script>
</if>
| Method | Returnt |
|---|---|
%cookiewall->hasChosen() |
true zodra bezoeker iets gekozen heeft |
%cookiewall->canTrack |
true alleen bij keuze 'all' |
%cookiewall->canAnalytics |
Alias van canTrack, semantisch handig voor analytics-bridge |
%cookiewall->choice |
'essential' / 'all' / null |
Multilingual variant: DOM/cookiewall.translated (gebruikt {nl: ...} shorthand voor de banner-teksten).
12.6: Worker-mode
Phlo draait standaard per-request: PHP-process op, request afhandelen, process klaar. Met thread: true in phlo_app(...) blijft de runtime tussen requests in geheugen, bedoeld voor FrankenPHP, ReactPHP of RoadRunner.
Performance-winst is groot (geen boot per request), maar er gelden drie regels:
1. Geen die() of exit() in het HTTP-pad. Beide killen de hele worker, niet alleen de huidige request. Gebruik return of laat een terminating call (view(), apply(), location()) de response sturen.
2. Geen request-state in static properties. Statics overleven tussen requests. Data van request A lekt naar request B. Statics zijn alleen veilig voor class-structuur of computed metadata die voor alle requests gelijk is, niet voor session-, user-, payload-, time- of DB-state.
3. Mark long-lived objects met $objPers = true. Default cleart Phlo zijn instance-map tussen requests. Voor objecten die je expliciet wil hergebruiken (DB-connectie, prepared statements), zet $this->objPers = true zodat de cleanup ze met rust laat.
Combinatie met build: true is niet toegestaan, build schrijft files tussen requests; in een worker is dat een race-condition. Phlo gooit een runtime-error als je beide aanzet.
12.7: Resources aanpassen zonder forken
Soms wil je een gedeelde resource nét iets anders laten werken in één app, zonder die resource te kopiëren of te wijzigen. Vanuit elk .phlo-bestand kun je een node in een andere class injecteren of overschrijven door de node te benoemen als %<class>.<node>:
static %visitors.table = 'control.visitors'
prop %visitors.db = 'control'
method %model.greet => 'hi'
De eerste regel overschrijft de static $table van het visitors-model; de tweede voegt een db-prop toe aan visitors; de derde voegt een greet-method toe aan model. Bij de build haalt de compiler de %<class>.-prefix eraf en schrijft de node in <class>, een bestaande node met die naam wordt overschreven, een nieuwe wordt toegevoegd. De doel-class moet onderdeel zijn van de build (zijn resource geladen), anders wordt de modifier stil genegeerd. Houd het node-type gelijk aan wat je vervangt (static met static, prop met prop): de hele node wordt omgewisseld.
Praktijkvoorbeeld: laat het gedeelde visitors-model naar een centrale analytics-database schrijven, terwijl alle overige queries van de app gewoon op de eigen connectie blijven:
static %visitors.table = 'control.visitors'
Zo blijft de gedeelde resource agnostisch en geeft elke app er z'n eigen invulling aan.
12.8: Best practices
- Houd entrypoints expliciet; voorkom verborgen configuratie.
- Laat release-output door
build::releaseontstaan. - Zet credentials niet in bronbestanden.
- Gebruik reflectie om resources, routes en functies te controleren.
- Voeg abstrahering pas toe wanneer meerdere apps er echt voordeel van hebben.
13: Bijlagen
Deze bijlagen geven compacte voorbeelden die aansluiten op de productie-release.
13.1: Basisroute met view
prop title = 'Welkom'
route both GET => view($this)
view:
<main>
<h1>$this->title</h1>
</main>13.2: Route met variabele
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 formulier
route both GET contact => view($this)
route async POST contact @name,email,message => $this->send
method send {
%payload->name || error('Naam is verplicht')
return ['html' => ['#status' => 'Verzonden']]
}
view:
<form method=post action="/contact" class=async>
<input name=name>
<input name=email>
<textarea name=message></textarea>
<button>Verstuur</button>
</form>
<p#status/>13.4: Minimale app.json
{
"resources": [
"payload",
"session",
"security/security",
"security/token",
"tag",
"phlo.async",
"DOM/form"
],
"release": "%app/release/"
}13.5: Dev-entrypoint
<?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
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 loopt via PhloWS, een aparte Node.js-server (phloWS.js) die WebSocket-verbindingen multiplexed over meerdere vhosts en elk inkomend bericht doorgeeft als een one-shot PHP CLI-call. Je app implementeert vier hook-functies en broadcast vanuit PHP met wsCast().
14.1: Wat PhloWS is
PhloWS is een lichte broker geschreven in Node.js (ws-library, ~9 KB code). Eén proces op poort 3001 bedient de hele stack: één PhloWS routeert via de Host-header van de WS-handshake naar de juiste app.
Per binnenkomend bericht boot PhloWS een one-shot PHP-process (php-zts <app>/www/app.php ws::<event>). Dat kost ~50-100 ms per message, maar geeft elke handler de complete request-lifecycle: DB, session, resources, alles is gewoon beschikbaar.
Geen persistent worker-state tussen messages. Wil je state delen, doe het via je database of apcu.
14.2: Installatie
PhloWS staat los van het Phlo-framework. Kies een pad, /srv/websocket, /opt/phloWS, ~/code/phloWS, het maakt niet uit. We gebruiken <ws> als placeholder:
git clone https://github.com/q-ainl/phlo-websocket.git <ws>
cd <ws>
npm install
In <ws>/websocket.js map je vhost → app.php-pad:
require('./phloWS.js')(3001, '/usr/bin/php-zts', {
'app.example.com': '<app>/release/www/app.php',
'dev.app.example.com': '<app>/www/app.php',
})
Start als service (systemd / pm2 / supervisord); de phlo-websocket README beschrijft het pm2-runpatroon en het /message-bridgecontract:
node <ws>/websocket.js
Voor productie: zet wss:// door je reverse proxy (Caddy, Nginx, FrankenPHP) naar 127.0.0.1:3001 voor pad /websocket.
14.3: App-hooks
In je app-bron (typisch websocket.phlo) definieer je vier functies. Phlo's websocket resource roept ze aan als ze bestaan:
function wsConnect($host, $token, $socket){
%log->info('ws connect', socket: $socket)
return true
}
function wsAuth($host, $token, $socket){
$user = %user->byToken($token)
if (!$user) return false
%session->user = $user
return true
}
function wsReceive($host, $token, $socket, ...$data){
$type = $data['type'] ?? null
if ($type === 'ping') return wsCast(wsTarget: $socket, pong: time())
if ($type === 'chat.send') chat::send($data['text'], from: %session->user->id)
}
function wsClose($host, $token, $socket){
%log->info('ws close', socket: $socket)
}
| Hook | Wanneer | Return |
|---|---|---|
wsConnect |
Direct na WS-handshake | true accepteert, false sluit |
wsAuth |
Eerste auth-message van de client | true authenticeert, false sluit |
wsReceive |
Voor elk volgend bericht (JSON-decoded en gespread) | irrelevant, gebruik wsCast() om te antwoorden |
wsClose |
Verbinding sluit | irrelevant |
$socket is een opaque string-identifier waarmee je terug kunt broadcasten naar exact deze client.
14.4: Auth-flow
PhloWS implementeert een twee-staps handshake:
- Browser opent
wss://<host>/websocket. - PhloWS roept
wsConnect. Bijfalse: sluiten. - Eerste binnenkomende bericht moet de auth-payload zijn (typisch
{type: 'auth', token: '<string>'}). - PhloWS roept
wsAuth($host, $token, $socket). App valideert tegen%user,%session->tokenof een eigen lookup. - Bij
false: sluit. Bijtrue: socket gemarkeerd als authenticated; daarna gaat alles viawsReceive.
Token komt typisch uit %user->token (per ingelogde user) of een API-key. De client kan deze meesturen via een cookie of als eerste WS-bericht.
14.5: Broadcasten vanuit PHP
wsCast() is een gewone functie (resource wsCast). Hij doet een POST naar PhloWS' interne HTTP-bridge en die pusht door naar de juiste sockets.
wsCast(wsTarget: 'all', toast: 'Nieuw bericht binnen')
wsCast(wsTarget: $socket, path: '/inbox')
wsCast(wsTarget: ['s1', 's2'], inner: ['#count' => $newCount])
| Argument | Default | Betekenis |
|---|---|---|
wsTarget |
'all' |
'all', 'token:<id>', 'token:not:<id>', één socket-id, of array van socket-ids |
wsHost |
host |
Vhost waarvoor de broadcast geldt (default: huidige host) |
wsPort |
websocket (constant uit app-config) |
PhloWS-poort |
...$data |
, | Named args worden de payload, meestal apply()-commando's |
De payload wordt aan de client doorgegeven en automatisch door phlo.js op de DOM toegepast: dezelfde apply()-protocollering die je kent van async routes.
Geen retry, geen dead-letter, geen ACK. Als PhloWS down is, faalt de POST stil. Voor zekere delivery (financiële events): combineer met een DB-queue.
14.6: Client-side
De client doet zelf niets bijzonders. Voeg DOM/websocket toe aan je resources in data/app.json:
{
"resources": [..., "DOM/websocket", "wsCast"]
}
DOM/websocket injecteert een script dat:
- automatisch verbindt op
wss://<host>/websocket - inkomende berichten direct door
apply()haalt,inner:,outer:,class:,toast:,path:werken hetzelfde als bij async routes - reconnect doet met exponential backoff (333 ms → 999 ms → ...)
Wil je vanuit JS sturen: app.websocket.send({type: 'chat.send', text: 'hi'}).
14.7: Mini-voorbeeld: presence
Toon "wie is online" zonder polling.
function wsConnect($host, $token, $socket){
%apcu->set("presence:$socket", time(), 3600)
wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
return true
}
function wsClose($host, $token, $socket){
%apcu->delete("presence:$socket")
wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
}
static count(){
$keys = %apcu->keys('presence:')
return count($keys)
}
De server houdt geen state bij; APCu telt sockets per host. Bij een PHP-restart leegt de cache vanzelf, niet erg, want een lege presence is een aanvaardbare degraded state.
14.8: Bekende limitaties
- One-shot CLI per event, elke message kost een PHP-opstart. Prima voor inbox, presence en notificaties; ongeschikt voor high-frequency telemetry of real-time trading.
- Geen versiebeheer op payloads, bij refactor: alle clients tegelijk migreren.
- Single point of failure, één PhloWS-proces voor de hele stack. Bij crash: alle realtime-features down tot restart. Draai PhloWS onder een process-supervisor.
- Geen built-in encryptie, gebruik je reverse proxy voor TLS-termination (
wss://).
15: Tasks
Phlo heeft een ingebouwde cross-app cron-runner. Eén system-cron entry per app triggert tasks::run elke minuut; de tasks-resource matcht declaratief tegen %app->tasks. Geen cron-syntax in je app, geen externe scheduler.
15.1: Setup
Drie stappen.
1. Activeer de resource in data/app.json:
{
"resources": [..., "tasks"]
}
2. Beschrijf je taken in app.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. Eén cron-entry per app met absoluut pad:
* * * * * php-zts <app>/www/app.php tasks::run
Plaats in /etc/cron.d/example-tasks (systeem, 6 velden incl. user) of via crontab -u <user> (per-user, 5 velden).
15.2: Schedule
Kies precies één scheduling-key per task:
| Key | Format | Voorbeeld |
|---|---|---|
every: |
PHP-leesbare duur-string | 'minute', '5 minutes', '2 hours', '1 day' |
daily: |
'HH:MM' |
'03:00' |
weekly: |
'<weekday> HH:MM' |
'monday 09:00' |
every: 'minute' (zonder leading number) wordt intern '1 minute'. Parsing via strtotime("+$every", 0).
15.3: Callable (`do:`)
Het do:-veld accepteert drie vormen:
| Type | Voorbeeld | Wordt aangeroepen als |
|---|---|---|
| Closure | fn() => external::pull() |
direct |
'Class::method' |
'account::cleanup' |
account::cleanup() |
| Resource-naam | 'backup' |
phlo('backup') |
Anders dan tijdens een normale request loopt een task buiten een HTTP-lifecycle: er is geen %req, geen %session. Schrijf je task zo dat hij self-contained is.
15.4: State op disk (`data/tasks/`)
tasks::run maakt data/tasks/ automatisch aan en bewaakt elke task met drie bestanden:
| File | Inhoud | Wanneer |
|---|---|---|
<name>.last |
raw unix-timestamp | Per succesvolle run, voor de due-check |
<name>.json |
{schedule, return} voor dashboard |
Per succesvolle run |
<name>.lock |
leeg (mtime telt) | Tijdens run, TTL 1 uur |
Locks voorkomen dat een trage task zichzelf inhaalt. De TTL is bewust 1 uur: een gefaalde task is geparkeerd tot de lock verloopt; andere tasks blijven gewoon runnen.
15.5: Error-flow
Geen try/catch in tasks::run. Een Throwable bubblet naar Phlo's framework-exception-handler en schrijft naar data/errors.json, zoals build-errors dat doen. Dashboard toont ze in de tasks-tab.
15.6: Dashboard
Phlo's dev-dashboard detecteert data/tasks/ automatisch:
- Tasks-tab verschijnt in de nav (alleen als de directory bestaat), direct na Home.
- Per task: schedule (uit JSON), last-run-ago, return-value (type-aware: scalar / array / string), lock-status.
- Dashboard is volledig agnostisch over de
tasks-resource en de app, het leest puur uitdata/tasks/. Schedule-info komt uit<name>.json, niet via een app-route (dat zou een HTTP-response triggeren en de dashboard-render verstoren).
15.7: Voorbeeld
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:
* * * * * php-zts <app>/www/app.php tasks::run
Bij elke minuut wordt tasks::run aangeroepen, ziet dat heartbeat every: 'minute' is en lastRun < 60s geleden, en draait app::heartbeat(). Het bestand data/tasks/heartbeat.last, .json en .lock worden bijgewerkt; tussentijdse cron-ticks slaan de task over zolang hij draait.
16: AI
Phlo bundelt een aantal AI-providers (OpenAI, Claude, Gemini, DeepSeek) achter één façade. Je kiest een model, Phlo kiest de juiste engine. Streamen naar de DOM gaat via dezelfde apply()-mechanieken als de rest van Phlo: geen aparte client-side library, geen aparte event-bus.
16.1: Resources
Voeg toe aan data/app.json:
{
"resources": [..., "AI/AI", "AI/OpenAI"]
}
Per provider één file. AI/AI is de façade, die routeert naar de juiste engine op basis van het model:
| Model bevat | Engine |
|---|---|
gpt-*, o1-*, o3-*, o4-*, chatgpt-* |
OpenAI |
claude-* |
Claude |
deepseek-* |
DeepSeek |
gemini-* |
Gemini |
Of expliciet via via: argument: %AI->chat(via: 'claude', model: ...).
Credentials gaan in data/creds.ini:
OpenAI = sk-...
Claude = sk-ant-...
Phlo's security/creds laadt ze automatisch in %creds->OpenAI etc.
16.2: Eén antwoord
Korte vraag, één antwoord:
$answer = %AI->chat(
model: 'gpt-4o-mini',
user: 'Vat dit artikel samen: '.$article->text,
)
echo $answer->answer
Of nog korter, via de answer-helper:
$verdict = answer('Is "carrot" een groente?', 'ja', 'nee', 'misschien')
answer() is ingebouwd in AI/answer. Hij doet één call met lage temperature en geeft alleen het puurste antwoord terug. Met options wordt het een keuze uit de meegegeven mogelijkheden.
16.3: Streamen naar de DOM
Dit is waar Phlo's apply()-protocol echt schittert. Een async route die token-voor-token naar een element schrijft:
route async POST chat::ask {
$question = %payload->question
%res->header('Content-Type', 'text/event-stream')
foreach (%AI->stream(user: $question) AS $chunk){
if (isset($chunk->text)){
echo json_encode(['inner' => ['#answer' => $chunk->text]], jsonFlags).lf
flush()
}
}
}
De Phlo client-runtime leest elke regel als een apply()-commando en zet de DOM-update meteen door. Geen JS schrijven, geen state managen, je hebt direct een streaming UI.
16.4: Tools (function calling)
$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 komen terug onder $res->tools als array van {name, args}. Phlo's façade normaliseert de provider-verschillen.
16.5: Vision
$res = %AI->vision('Wat staat er op deze foto?', '/uploads/photo.jpg')
echo $res->answer
Werkt met OpenAI, Claude en Gemini.
16.6: Embeddings
$vector = %AI->embedding('Phlo is een compile-to-PHP framework', model: 'text-embedding-3-small')
%vectors->store(id: 'doc-1', vector: $vector, meta: ['source' => 'about'])
Default-model is provider-specifiek. Voor OpenAI is dat text-embedding-3-small.
16.7: Transcribe
$file = %files->save(%payload->file('audio'))
$res = %AI->transcribe($file, model: 'whisper-1', language: 'nl')
echo $res->text16.8: Veiligheid
- AI-calls zijn duur en niet-deterministisch. Cache aggressive,
%apcuvoor sessie-scope,JSONDBvoor langere TTL. - Filter user-input voor je het in een prompt zet. Phlo's
esc()is voor HTML; voor prompts gebruik je een eigen sanitizer of een strikte tool-schema. - Logging van prompts/antwoorden kan privacy-implicaties hebben. Standaard logt Phlo niet; je
data/errors.jsonziet alleen exceptions.
17: Trace
Phlo's trace-mode is een runtime-instrumentatielaag die elke aanroep van een gegenereerde method, prop-getter, static of native function logt met timing en argumenten. Per request schrijft Phlo een JSON-dump naar data/trace/<id>.json. Bedoeld voor debuggen en profileren, niet voor productie.
17.1: Wat trace doet
Met trace aan injecteert Phlo's compiler één regel boven aan elke gegenereerde method:
trace('class->method', compact('arg1', 'arg2'))
En Phlo laadt functions.trace.php (een gegenereerde variant van functions.php) zodat ook native helpers, esc(), arr(), loop(), view(), apply(), alles, een trace-call krijgen. Resultaat: een complete chronologische log van wat er tijdens een request gebeurd is.
17.2: Aanzetten
In je dev-entrypoint:
phlo_app (
id: 'Example',
host: 'dev.example.nl',
build: true,
debug: true,
trace: true,
)
Daarna build::run zodat de gegenereerde PHP de trace()-injecties bevat. Vanaf de volgende request wordt elke aanroep gelogd.
Trace-output gaat naar data/trace/. De directory wordt automatisch aangemaakt.
17.3: Wat er in een trace zit
Per request één JSON-bestand met:
| Veld | Inhoud |
|---|---|
id |
Datum-tijd + random suffix, ook de bestandsnaam |
path, method, route |
Request-context |
ts, ms, count |
Start-timestamp, totale duur, aantal events |
active |
Map per file → kind → aantal aanroepen (snel overzicht "wat is er veel aangeraakt") |
sequence |
Volgorde van eerste aanraking per file (visualiseert het request-pad door je app) |
events |
Complete log: zie hieronder |
Elk event in events:
{
"t": 12.345,
"k": "call",
"c": "user",
"n": "byEmail",
"node": "user->byEmail",
"f": "user.phlo",
"args": {"email": "jordi@example.nl"}
}
| Veld | Betekenis |
|---|---|
t |
Offset in ms sinds request-start |
k |
Kind: call, static, get (prop-get), set (prop-set), function |
c, n |
Class en name |
f |
Source-file (resolved via classmap + sourcemap) |
args |
Gesnipte argumenten, zie X.4 |
17.4: Argument-snipping
Volledige argument-waarden zou de trace onleesbaar maken. Phlo snipt:
| Type | Wordt |
|---|---|
null, bool, int, float |
onveranderd |
string > 200 tekens |
... afgekapt op 200 |
array |
'[N items]' (alleen lengte) |
object met id-property |
{class: ..., id: ...} |
Ander object |
{class: ...} |
Dat is genoeg om te zien welke records werden geraakt zonder de hele payload te dumpen.
17.5: Lezen via het dashboard
Het dashboard heeft een Trace-tab die data/trace/index.json leest. De meest recente trace staat bovenaan; via een selectbox open je oudere. Per trace zie je active, sequence en de event-stream.
index.json houdt de laatste 100 traces bij. Oudere worden auto-pruned met hun JSON-bestand.
17.6: Onderhoud: `build::traceShadow`
functions.trace.php is een gegenereerd bestand. Wanneer je iets toevoegt aan of wijzigt in functions.php, regenereer je het:
php www/app.php build::traceShadow
Dit parseert functions.php, en injecteert in elke function foo($a, $b) als eerste statement:
trace('foo', compact('a', 'b'))
Daarna stem je de inhoud van functions.trace.php af op de bron. Het commando is alleen relevant voor wie aan de Phlo-runtime zelf werkt, niet voor appcode.
17.7: Wanneer wel, wanneer niet
Wel:
- Een request is "ergens" traag en je wilt weten waar de tijd zit
- Onbekende app aan het verkennen:
sequencetoont je het werkelijke pad door de code - Een race-condition of side-effect debuggen:
eventstoont exact volgorde + timing - Voor onderwijs: laten zien hoe een Phlo-request feitelijk verloopt
Niet:
- Productie, elke method-call kost een log-entry en disk-write per request
thread: trueworkers, trace schrijft per request en is daarmee net alsbuildniet worker-safe- Performance-meting tot op de microseconde, de instrumentatie zelf voegt overhead toe
Zet trace: true aan wanneer je het nodig hebt; zet hem uit wanneer je klaar bent. Dashboard blijft de historische traces tonen tot de auto-prune ze opruimt.