14: WebSocket
Realtime in Phlo draait via phloWS, een aparte Node.js server (phloWS.js) die WebSocket-verbindingen multiplexeert over meerdere vhosts en elk binnenkomend bericht doorgeeft als een eenmalige PHP CLI-aanroep. Je app implementeert vier hook-functies en zendt uit vanuit PHP met wsCast().
14.1: Wat phloWS is
phloWS is een lichte broker geschreven in Node.js (ws bibliotheek, ~9 KB aan code). Eén proces op poort 3001 bedient de hele stack: een enkele phloWS routeert naar de juiste app via de Host header van de WS handshake.
Voor elk binnenkomend bericht start phloWS een one-shot PHP proces (php-zts <app>/www/app.php ws::<event>). Dat kost ~50-100 ms per bericht, maar geeft elke handler de volledige levenscyclus van het verzoek: DB, sessie, resources, alles is eenvoudig beschikbaar.
Geen persistente worker status tussen berichten. Als je status wilt delen, doe dat dan via je database of apcu.
14.2: Installatie
phloWS leeft buiten het Phlo-framework. Kies een pad, /srv/websocket, /opt/phloWS, ~/code/phloWS, het maakt niet uit. We gebruiken <ws> als een tijdelijke aanduiding:
git clone https://github.com/q-ainl/phlo-websocket.git <ws>
cd <ws>
npm install
In <ws>/websocket.js koppel 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',
})
Voer het uit als een service (systemd / pm2 / supervisord); de phlo-websocket README beschrijft het pm2 uitvoeringspatroon en het /message brugcontract:
node <ws>/websocket.js
Voor productie: geef wss:// door via je reverse proxy (Caddy, Nginx, FrankenPHP) naar 127.0.0.1:3001 voor het pad /websocket.
14.3: 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) return false
%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)
}
| Hook | Wanneer | Retourneer |
|---|---|---|
wsConnect |
Direct na de WS-handshake | true accepteert, false sluit |
wsAuth |
Eerste auth-bericht van de client | true authenticates, false sluit |
wsReceive |
Voor elk volgend bericht (JSON-gecodeerd en verspreid) | irrelevant, gebruik wsCast() om te reageren |
wsClose |
Verbinding sluit | irrelevant |
$wsSocket is een ondoorzichtige string-identificator die je kunt gebruiken om terug te zenden naar precies deze client.
De verbinding-contextargumenten zijn ws-geprefixed volgens afspraak ($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 in conflict komen met een payload die een host, token of socket sleutel bevat. Houd de prefix en je payload-sleutels blijven vrij.
14.4: Auth flow
phloWS implementeert een tweestaps-handshake:
- De browser opent
wss://<host>/websocket. - phloWS roept
wsConnectaan. Bijfalse: sluiten. - Het eerste binnenkomende bericht moet de auth payload zijn (typisch
{type: 'auth', token: '<string>'}). - phloWS roept
wsAuth($wsHost, $wsToken, $wsSocket)aan. De app valideert tegen%user,%session->tokenof een aangepaste lookup. - Bij
false: sluiten. Bijtrue: de socket wordt gemarkeerd als geauthenticeerd; alles daarna gaat viawsReceive.
De token komt typisch van %user->token (per ingelogde gebruiker) of een API-sleutel. De client kan deze verzenden via een cookie of als het eerste WS-bericht.
14.5: Uitzenden vanuit PHP
wsCast() is een reguliere functie (resource wsCast). Het doet een POST naar de interne HTTP-brug van phloWS, die het naar de juiste sockets duwt.
wsCast(wsTarget: 'all', toast: 'Nieuwe boodschap ontvangen')
wsCast(wsTarget: $wsSocket, path: '/inbox')
wsCast(wsTarget: ['s1', 's2'], inner: ['#count' => $newCount])
| Argument | Standaard | Betekenis |
|---|---|---|
wsTarget |
'all' |
'all', 'token:<id>', 'token:not:<id>', een enkele socket id, of een array van socket ids |
wsHost |
host |
Vhost waarop de uitzending van toepassing is (standaard: huidige host) |
wsPort |
websocket (constante uit app-configuratie) |
phloWS-poort |
...$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 dode brief, geen ACK. Als phloWS niet werkt, mislukt de POST stilletjes. Voor gegarandeerde levering (financiële gebeurtenissen): combineer met een DB-queue.
14.6: 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.7: Mini voorbeeld: aanwezigheid
Toon "wie is online" 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 verlaagde staat is.
14.8: Bekende beperkingen
- One-shot CLI per event, elke boodschap kost een PHP-opstart. Geschikt voor inbox, aanwezigheid en meldingen; ongeschikt voor hoge frequentie telemetrie of realtime trading.
- Geen versiebeheer op payloads, bij refactoring: migreer alle clients in één keer.
- Enkele foutpunt, één phloWS-proces voor de hele stack. Bij een crash: zijn alle realtime functies uitgeschakeld totdat de herstart. Voer phloWS uit onder een procesbeheerder.
- Geen ingebouwde encryptie, gebruik je reverse proxy voor TLS-terminatie (
wss://).
14.9: Streaming reacties: realtime-light zonder phloWS
Niet elke progressieve update heeft een WebSocket nodig. Stel %res->streaming = true in een route in en elke daaropvolgende apply(...) wordt onmiddellijk afgedrukt en geflusht als één JSON-regel over dezelfde HTTP-respons; phlo.js blijft de opdrachten toepassen naarmate ze binnenkomen:
route async POST report::generate {
%res->streaming = true
foreach ($this->steps AS $i => $step){
$step->run
apply(inner: arr('#progress' => $i + 1 .'/'. count($this->steps)))
}
apply(toast: 'Klaar')
}
Gebruik streaming wanneer één client het werk heeft getriggerd en zijn eigen voortgang bekijkt (AI-tokenstreams, imports, batchtaken). Gebruik phloWS wanneer ANDERE clients de update ook moeten ontvangen: streaming volgt de aanvraag, broadcasting volgt de vloot van verbindingen.