EN | NL | 中文

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

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

cli is 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:

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

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

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

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

4.3.3 Rest (variabele lengte) met =*

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

4.3.4 Default-waarde met =

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

4.3.5 Lengte-eis met .N

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

4.3.6 Keuzelijsten met :a,b,c

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

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)

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

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 (innerinnr) wordt stil genegeerd. Houd deze tabel bij de hand, of consulteer /srv/phlo/docs/apply-protocol.md voor 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:

Regels:

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>

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>

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

Werking

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

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:

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'),
)

schema is 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)

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, fromto
delete(...) volledige oude waarden, per geraakt record

Setup:

  1. security/audit toevoegen aan data/app.json resources.
  2. 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:

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

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:

  1. De instance manager kijkt of er al een instantie van deze class bestaat.
  2. Zo niet, dan wordt deze aangemaakt en opgeslagen.
  3. Na het aanmaken wordt de controllercode uitgevoerd (zie §8.2).
  4. 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.

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():

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)
}

9.5: Best practices

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

  1. Lees eerst de bron en reflectie-output.
  2. Pas alleen .phlo, data/app.json of entrypoints aan.
  3. Draai build::run.
  4. Draai build::lint.
  5. Test de relevante HTTP-routes.
  6. Draai build::release als 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

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:

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

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:

  1. Browser opent wss://<host>/websocket.
  2. PhloWS roept wsConnect. Bij false: sluiten.
  3. Eerste binnenkomende bericht moet de auth-payload zijn (typisch {type: 'auth', token: '<string>'}).
  4. PhloWS roept wsAuth($host, $token, $socket). App valideert tegen %user, %session->token of een eigen lookup.
  5. Bij false: sluit. Bij true: socket gemarkeerd als authenticated; daarna gaat alles via wsReceive.

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:

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

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:

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

16.8: Veiligheid

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:

Niet:

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.

我们使用必要的 cookies 来使此网站正常工作。在您的许可下,我们还使用分析工具来改善网站。