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:

  1. De browser opent wss://<host>/websocket.
  2. phloWS roept wsConnect aan. Bij false: sluiten.
  3. Het eerste binnenkomende bericht moet de auth payload zijn (typisch {type: 'auth', token: '<string>'}).
  4. phloWS roept wsAuth($wsHost, $wsToken, $wsSocket) aan. De app valideert tegen %user, %session->token of een aangepaste lookup.
  5. Bij false: sluiten. Bij true: de socket wordt gemarkeerd als geauthenticeerd; alles daarna gaat via wsReceive.

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:

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

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.

We gebruiken essentiële cookies om deze site te laten werken. Met uw toestemming gebruiken we ook analytics om de site te verbeteren.