16: Demon
De Phlo Daemon is een optionele Node sidecar (phlo-daemon.js): een generieke engine die elk Phlo-doel naar een pool van persistente workers dispatcht. De kern van Phlo werkt zonder deze; de daemon versnelt de zware paden en ondersteunt WebSockets en geplande taken.
16.1: Wat het is, en waarom het optioneel is
Elke routine in je app is al aanroepbaar als een eenmalig CLI-proces (php www/app.php <target> [args...], zie het hoofdstuk Tooling). Dat is hoe async helpers, taken en WebSocket-evenementen standaard worden uitgevoerd: een nieuwe PHP-proces per aanroep, dat de app opstart, het werk doet en weer afsluit. Simpel en volledig geïsoleerd, maar een opstart per aanroep.
De daemon verwijdert die opstart. Het is een klein Node-proces dat een pool van resident PHP-werkers per app onderhoudt: elke werker start de app één keer op en beantwoordt vervolgens aanroepen via een pijp, hetzelfde idee als de HTTP-werkmode van FrankenPHP, maar voor niet-HTTP-werk. Het voegt twee dingen toe:
- een werknemerspool die dezelfde aanroepen uitvoert zonder een opstart per aanroep, en
- het langdurige hostproces dat WebSockets en geplande taken nodig hebben.
De daemon is een generieke centrale motor: de dispatch-kern weet niets over specifieke functies. De WebSocket-server (Phlo Realtime) en de planner zijn ingebouwd in de daemon; de PHP-runtime helpers bereiken het via HTTP. Het aannemen ervan is opt-in en omkeerbaar: laat het weg en alles valt terug op het eenmalige pad.
16.2: Het workerprotocol
Elke worker draait php <app.php> phlo_serve, start de app één keer op en beantwoordt vervolgens newline-gescheiden JSON op stdin, met één verzoek in behandeling per worker (concurrentie is gelijk aan de poolgrootte):
in {"id", "target", "args"?, "stream"?}
out {"t":"ready"} // once, after boot
{"id", "t":"line", "data"} // 0..N, only when stream
{"id", "t":"done", "result"} | {"id", "t":"error", "message"} // exactly one, terminal
target wordt verzonden met dezelfde parser als de CLI: Class::method, object.method of een blote function (zie het hoofdstuk Tooling). Per verzoek reset de worker de status (phlo('tech/reset'), sessie sluiten, GC), precies zoals de FrankenPHP worker loop, zodat taken nooit in elkaar lekken. Schrijf worker-veilige targets: geen verzoek- of gebruikersstatus in statics, commit of roll back DB-werk.
16.3: The HTTP API
The daemon binds 127.0.0.1 by default (local only; gate it at the network boundary).
POST /dispatch takes {app, target, args?, stream?, async?}:
| Key | Used by | Meaning |
|---|---|---|
app |
the runtime helpers | The absolute .../app.php path to run. A caller that knows its own app dispatches directly; the pool is keyed by the app path. |
The built-in WebSocket server does not use this endpoint: it resolves each connection's Host to an app through the registry (populated from the hosts map in config/daemon.js) and dispatches in-process. POST /message (the broadcast bridge) and GET /health round out the API.
The response depends on the mode:
| Request | Response |
|---|---|
| default | {status:"ok", result} |
async: true |
202 {status:"ok", queued:true} (fire and forget; returns once accepted, not once run) |
stream: true |
an application/x-ndjson stream of {t:line,data}* then {t:done,result} or {t:error} |
GET /health returns the live worker total against the cap, the per-pool stats keyed by app path, the connected sockets per host, and the configured hosts:
{
"status": "ok",
"workers": 5,
"cap": 7,
"pools": { "/var/www/example/www/app.php": { "workers": 4, "busy": 1, "queued": 0 } },
"sockets": { "app.example.com": { "tokens": 12, "sockets": 18, "perToken": { "9c11922f8c7fff2d...": 2 } } },
"registered": ["app.example.com", "dev.example.com"]
}
perToken gives the socket count per connected token, keyed by sha256 of the token so the endpoint never hands out a credential. It is how an app answers "is this client connected" without keeping a tally of its own, which drifts the moment the daemon restarts.
busy is the workers currently handling a call and queued the calls waiting for a free worker; workers/cap is the live total against the ceiling (one less than the core count). The pool grows itself toward the cap under load and reaps idle workers back down, so these are observations, not knobs.
16.4: Configuratie
De daemon neemt drie argumenten: een poort, de PHP-binaire en de hostmap. Er is geen poolgrootte - de pool schaalt op aanvraag.
require('./phlo-daemon.js')(3001, '/usr/bin/php-zts', {
'dashboard.example.nl': { app: '/var/www/dashboard/www/app.php', build: true },
'demo.example.nl': { app: '/var/www/demo/www/app.php', build: true },
})
| Argument | Default | Betekenis |
|---|---|---|
port |
(vereist) | Poort om te binden op 127.0.0.1; sluit het af bij de proxy, expose het nooit |
php |
(vereist) | PHP-binaire die de workers draaien (/usr/bin/php-zts voor thread-veilige builds) |
hosts |
{} |
De host→app kaart, { host: { app, build } }: elke app die de daemon bedient (zie hieronder) |
De hostkaart is geconfigureerd. Het derde argument is de host-naar-app kaart, gedeclareerd in config/daemon.js en geladen in het register bij opstarten: elke invoer koppelt een Host aan zijn app.php pad en een build vlag. Er is geen /register eindpunt en geen registry.json; apps registreren zichzelf niet. De ingebouwde WebSocket-server gebruikt deze kaart om de Host van een verbinding te bepalen; de runtime helpers dispatchen op hun eigen app pad en hebben geen invoer nodig.
De pool schaalt zichzelf. Elke app krijgt zijn eigen pool die workers op aanvraag spawn tot een globale limiet van één minder dan het aantal cores, en oogst ze zodra ze inactief worden. Een worker wordt gerecycled na een aantal oproepen, en een vastgelopen worker wordt gedood en opnieuw opgestart. Dit is niet geconfigureerd.
Eenmalig of gepoold volgt de build vlag. Een build: true app (ontwikkeling) draait elke oproep als een nieuwe eenmalige proces, voor volledige isolatie en hot-reload. Een release app draait op de residentiële pool. Voor websockets is die vlag de per-host instelling in config/daemon.js; voor CLI dispatch sturen de runtime helpers build per oproep.
Voer het uit onder een procesmanager:
node config.js
# or
pm2 start config.js --name phlo-daemon16.5: Runtime helpers op de pool
De async helpers behouden standaard hun eenmalige subprocess gedrag en schakelen over naar de daemon pool wanneer de app de optionele daemon constant instelt:
phlo_app(
id: 'Api',
host: 'api.example.com',
daemon: 3001,
);
Met de constante ingesteld, worden deze routes via /dispatch door hun eigen app pad geleid; zonder het, starten ze een eenmalig proces precies zoals voorheen:
| Helper | Doet |
|---|---|
phlo_sync('Class::method', ...$args) |
Voert een doel uit en wacht op de retourwaarde |
phlo_async('Class::method', ...$args) |
Plaatst een doel in de wachtrij en vergeet het; retourneert zodra het is geaccepteerd |
await($job, $job, ...) |
Voert veel doelen gelijktijdig uit en verzamelt hun resultaten |
phlo_stream('Class::method', ...$args) |
Geeft de uitvoer van een doel regel voor regel terug |
De winst is het grootst waar één verzoek zich verspreidt in veel aanroepen: await() over 100 ontbrekende vertalingen is 100 app opstarts op het eenmalige pad, maar 100 dispatches naar een resident pool met de daemon. Op de pool is het werk ook begrensd-parallel (gequeue tegen het aantal workers) in plaats van een onbeperkte uitbarsting van subprocessen.
$results = await(
['translate::run', 'nl', $text],
['translate::run', 'de', $text],
['translate::run', 'fr', $text],
)
Omdat het aannemen van de daemon gewoon de daemon constante is, gedraagt een app zich identiek met of zonder deze; alleen de doorvoer verandert.
16.6: Planning: het cron alternatief
Elke app in de hostmap krijgt de cron-vervangende minuut-tik: de daemon voert tasks::run uit op elke bediende app eens per minuut, de eerste uitvoering een minuut na opstarten. Er is niets te configureren per taak: welke taken worden uitgevoerd en hoe vaak, wordt in de app zelf verklaard (%app->tasks, zie het hoofdstuk Taken). Een app die de tasks resource niet laadt, wordt automatisch overgeslagen na de eerste tik.
Er is hier geen planning per doel. Wat er draait en wanneer, bevindt zich in de %app->tasks van elke app (every/daily/weekly, zie het hoofdstuk Taken), zodat de planning een enkele thuisbasis heeft; de daemon levert alleen de minuut-tik. Dit vervangt de per-app cron-invoer uit het hoofdstuk Taken: met de daemon die tasks::run elke minuut uitvoert, heb je de crontab-regel helemaal niet nodig. Cron blijft de no-daemon fallback; het taakmodel, de due-checking en de on-disk status zijn in beide gevallen identiek.
16.7: Consumenten
| Consumer | Relationship |
|---|---|
Phlo Realtime |
De ingebouwde WebSocket-server van de daemon; bezit de sockets en voert de websocket::{auth,connect,receive,close} hooks uit op de pool in-process (receive streams). Zie het hoofdstuk over WebSocket. |
| Runtime helpers | phlo_sync / phlo_async / await / phlo_stream, opt-in via de daemon constante (16.5). |
| Scheduler | Ingebouwd (16.6), draait tasks::run voor elke bediende app elke minuut, ter vervanging van cron. |
Phlo WhatsApp |
Blijft zijn eigen service: een WhatsApp-gateway houdt een persistente telefoonsessie, wat geen taak van de worker-pool is. Het wordt gemonitord, niet opgenomen. |
16.8: Wanneer het uit te voeren
Voer de daemon uit wanneer een app WebSockets nodig heeft, wanneer het taken plant zonder cron, of wanneer een hot path zich verspreidt in veel app-aanroepen per verzoek. Een kleine site die gewone HTTP bedient en nooit uitzendt, heeft het niet nodig: het eenmalige pad is voldoende, en de daemon buiten houden vermindert het aantal bewegende delen. De daemon is de optionele prestatie- en realtime-laag, nooit een afhankelijkheid van de kernverzoekverwerking.
Laatst bijgewerkt op 29-07-2026