14: WebSocket
Realtime in Phlo draait via Phlo Realtime, de WebSocket-server die is ingebouwd in de Phlo Daemon. Eén Node-proces beheert de socketverbindingen over elke vhost en voert elk evenement op je PHP-app uit via de eigen worker pool van de daemon. Je app implementeert vier hook-functies en zendt uit vanuit PHP met wsCast().
14.1: Streaming eerst
Niet alles dat realtime is, heeft een socket nodig. Voor eenrichtingsupdates via een enkele aanvraag opent chunk() (de chunk resource) een streamingresponse en verzendt elke oproep onmiddellijk als één JSON-regel via gewone HTTP; phlo.js past de commando's toe zodra ze binnenkomen:
route async POST report::generate {
foreach ($this->steps AS $i => $step){
$step->run
chunk(inner: arr('#progress' => $i + 1 .'/'. count($this->steps)))
}
apply(toast: 'Done')
}
%res->streaming = true geeft een route hetzelfde gedrag met gewone apply() aanroepen, en klassieke SSE (text/event-stream) vult dezelfde eenrichtingsslot. Al deze houden een PHP worker vast zolang de respons streamt: precies goed voor eindig werk dat één client bekijkt (AI token streams, imports, rapporten), en precies verkeerd voor een permanente verbinding die wacht op gebeurtenissen. Dat is waar Phlo Realtime voor is: de daemon houdt de open sockets vast, jouw PHP draait alleen wanneer er een gebeurtenis aankomt, en een uitzending bereikt elke verbonden client in plaats van alleen degene die vroeg. Streaming volgt de aanvraag; broadcasting volgt de vloot van verbindingen.
14.2: Wat Phlo Realtime is
Phlo Realtime is de WebSocket-laag van de daemon (gebouwd op de ws bibliotheek), niet een apart proces. De enkele daemon op poort 3001 bedient de hele stack: hij accepteert de socket-upgrades, routeert op basis van de Host header van de handshake, houdt het clientregister bij, draait de /message broadcast bridge en dispatcht elk socket-evenement naar jouw PHP op dezelfde worker pool die de daemon al voor alles gebruikt.
Omdat die dispatch in-process is, is er niets te verbinden tussen "de socketlaag" en "de PHP-laag": ze zijn één proces. Elk evenement (auth, connect, receive, close) draait de bijpassende websocket::<hook> target, en de uitvoeringsmodus is wat de daemon al doet voor die host:
- One-shot (een
build: truehost, d.w.z. dev). Een nieuwe PHP-proces per evenement, de app wordt elke keer opgestart: doodsimpel, volledig geïsoleerd, hot-reload. Ideaal voor ontwikkeling en hosts met weinig verkeer. - Resident pool (een release host). De pool van de daemon houdt workers warm en beantwoordt evenementen via een pijp, geen opstart per evenement. De pool schaalt zichzelf op naar de vraag en weer terug naar beneden wanneer inactief, dus er is geen aantal workers dat geconfigureerd moet worden. Worker-veilige handlers zijn van toepassing, dezelfde discipline als de FrankenPHP worker modus: geen verzoek- of gebruikersstatus in
statics, altijd commit of rollback van DB-werk.
Hoe dan ook, elk evenement geeft de handler de volledige levenscyclus van het verzoek: DB, sessie, resources, alles is eenvoudig beschikbaar. De modus volgt de build-vlag van de host (gesteld in de entry van de host in config/daemon.js, zie hieronder); de handlercode is identiek.
14.3: Installatie
De daemon is een Node-service die buiten het Phlo-framework leeft. Voer het uit, wijs je reverse proxy erop aan, en dat is de hele realtime setup.
git clone https://github.com/q-ainl/phlo-daemon.git <daemon>
cd <daemon>
npm install
Het neemt een poort, de PHP-binaire en de hostmap:
// <daemon>/config.js
require('./phlo-daemon.js')(3001, '/usr/bin/php-zts', {
'demo.example.nl': { app: '/var/www/demo/www/app.php', build: true },
})
Het derde argument is de hostmap: het koppelt elke Host aan zijn app.php pad en een build vlag, en wordt gedeclareerd in config/daemon.js. De daemon laadt het bij het opstarten, zodat het altijd weet welke hosts bestaan en of elke host een one-shot of pooled is. Een host zonder invoer faalt de dispatch, wat de upgrade faalt.
Voer het uit onder een procesbeheerder (systemd / pm2 / supervisord); de phlo-daemon README beschrijft het pm2-patroon en het /message brugcontract:
node <daemon>/config.js
Voor productie, stuur wss:// via je reverse proxy (Caddy, Nginx, FrankenPHP) naar 127.0.0.1:3001 voor het pad /websocket. Je app declareert dezelfde poort als de daemon constante in www/app.php:
phlo_app(
app: __DIR__.'/../',
daemon: 3001,
)14.4: App hooks
In je app-bron definieer je vier functies; Phlo's websocket resource roept ze aan als ze bestaan. Plaats ze in een bestand zoals app.ws.phlo: noem het bestand niet websocket.phlo, omdat die klassenaam in conflict komt met de websocket resource van de engine wanneer deze wordt geladen.
function wsConnect($wsHost, $wsToken, $wsSocket){
%log->info('ws connect', socket: $wsSocket)
return true
}
function wsAuth($wsHost, $wsToken, $wsSocket){
$user = %user->byToken($wsToken)
if (!$user) error('unauthorized')
%session->user = $user
return true
}
function wsReceive($wsHost, $wsToken, $wsSocket, ...$data){
$type = $data['type'] ?? null
if ($type === 'ping') return wsCast(wsTarget: $wsSocket, pong: time())
if ($type === 'chat.send') chat::send($data['text'], from: %session->user->id)
}
function wsClose($wsHost, $wsToken, $wsSocket){
%log->info('ws close', socket: $wsSocket)
}
| Haak | Wanneer | Opmerkingen |
|---|---|---|
wsAuth |
Bij de handshake, voordat de socket wordt geaccepteerd | Valideer $wsToken; geef een foutmelding om de verbinding te weigeren |
wsConnect |
Direct nadat de socket is geaccepteerd | Opzetten (aanwezigheid, logging); uitzenden met wsCast() |
wsReceive |
Voor elk volgend bericht (JSON-gecodeerd en verspreid) | Reageer met wsCast(); afgedrukte regels worden teruggestuurd naar de afzender |
wsClose |
De verbinding sluit | Opruimen (aanwezigheid); uitzenden met wsCast() |
$wsSocket is een ondoorzichtige stringidentificator die je kunt gebruiken om precies naar deze client terug te zenden.
De verbinding-contextargumenten zijn ws-geprefixed uit convenie ($wsHost, $wsToken, $wsSocket), precies zoals wsCast. Dit is niet cosmetisch: wsReceive verspreidt de JSON-lading in benoemde argumenten (...$data), dus een ongeprefixed $host/$token/$socket parameter zou fataal botsen met een lading die een host, token of socket sleutel bevat. Houd de prefix en je payload-sleutels blijven vrij.
14.5: Auth flow
De daemon authenticates tijdens de handshake, voordat hij de socket accepteert:
- De browser opent
wss://<host>/websocket. De cookies voor die oorsprong, inclusief eentokencookie, worden meegestuurd met het upgradeverzoek. - De daemon leest de
tokencookie. Als deze ontbreekt, wordt de upgrade geweigerd met401. - De daemon voert
websocket::auth($wsHost, $wsToken, $wsSocket)uit op zijn pool, wat jouwwsAuthaanroept. wsAuthvalideert de token tegen%user,%session->tokenof een aangepaste lookup. Geef een foutmelding (error('unauthorized')) om te weigeren: een gegooide auth mislukt de upgrade. Bij succes opent de socket en wordtwsConnectuitgevoerd.
De token komt doorgaans van %user->token (per ingelogde gebruiker) of een API-sleutel, ingesteld als de token cookie wanneer de pagina wordt weergegeven. De browser stuurt deze automatisch mee tijdens de WS-handshake; de client stuurt geen aparte auth-bericht.
14.6: Uitzenden vanuit PHP
wsCast() is een reguliere functie (resource wsCast). Het doet een POST naar de interne /message brug van de daemon, die het naar de juiste sockets doorstuurt.
wsCast(wsTarget: 'all', toast: 'New message received')
wsCast(wsTarget: 'socket:'.$wsSocket, path: '/inbox')
wsCast(wsTarget: 'token:'.$token, inner: ['#count' => $newCount])
| Argument | Default | Betekenis |
|---|---|---|
wsTarget |
'all' |
'all', 'token:<id>', 'token:not:<id>' of 'socket:<id>' |
wsHost |
host |
Vhost waarop de uitzending van toepassing is (standaard: huidige host) |
wsPort |
daemon (constante uit app-configuratie) |
De poort van de daemon |
...$data |
geen | Genaamde argumenten worden de payload, meestal apply()-commando's |
De payload wordt automatisch naar de client doorgestuurd en toegepast op de DOM door phlo.js: hetzelfde apply()-protocol dat je kent van async routes.
Geen herhaling, geen dead-letter, geen ACK. Als de daemon niet werkt, mislukt de POST stilletjes. Voor gegarandeerde levering (financiële gebeurtenissen): combineer met een DB-queue.
14.7: Klantzijde
De client zelf doet niets bijzonders. Voeg DOM/websocket toe aan je resources in data/app.json:
{
"resources": [..., "DOM/websocket", "wsCast"]
}
DOM/websocket injecteert een script dat:
- automatisch verbinding maakt met
wss://<host>/websocket - binnenkomende berichten rechtstreeks doorstuurt via
apply(),inner:,outer:,class:,toast:,path:werken hetzelfde als bij async routes - opnieuw verbindt met exponentiële backoff (333 ms, 999 ms, ...)
Als je vanuit JS wilt verzenden: app.websocket.send({type: 'chat.send', text: 'hi'}).
14.8: Mini voorbeeld: aanwezigheid
Toon "wie online is" zonder polling.
function wsConnect($wsHost, $wsToken, $wsSocket){
%apcu->set("presence:$wsSocket", time(), 3600)
wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
return true
}
function wsClose($wsHost, $wsToken, $wsSocket){
%apcu->delete("presence:$wsSocket")
wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
}
static count(){
$keys = %apcu->keys('presence:')
return count($keys)
}
De server houdt geen staat bij; APCu telt sockets per host. Bij een PHP-herstart leegt de cache zichzelf, wat prima is, omdat een lege aanwezigheid een acceptabele gedegradeerde staat is.
14.9: Bekende beperkingen
- One-shot modus kost een PHP-opstart per gebeurtenis. Prima voor inbox, aanwezigheid en meldingen; voor hoge frequentie telemetry draai de host als een release build zodat de daemon het vanuit zijn resident pool serveert, wat die kosten verwijdert. Een gepoolde worker behandelt één gebeurtenis tegelijk en de pool past zich aan de vraag aan, dus houd langlopende handlers uit het hete pad (herstart de workers na een deploy zodat ze opnieuw laden).
- Geen versiebeheer op payloads, bij refactoring: migreer alle clients in één keer.
- Één proces voor de stack. De daemon beheert de sockets en draait de PHP; als het crasht, is realtime (en elke gepoolde dispatch) down totdat het opnieuw wordt opgestart. Draai het onder een proces supervisor.
- Geen ingebouwde encryptie, gebruik je reverse proxy voor TLS-terminatie (
wss://).