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:

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:

  1. De browser opent wss://<host>/websocket. De cookies voor die oorsprong, inclusief een token cookie, worden meegestuurd met het upgradeverzoek.
  2. De daemon leest de token cookie. Als deze ontbreekt, wordt de upgrade geweigerd met 401.
  3. De daemon voert websocket::auth($wsHost, $wsToken, $wsSocket) uit op zijn pool, wat jouw wsAuth aanroept.
  4. wsAuth valideert de token tegen %user, %session->token of een aangepaste lookup. Geef een foutmelding (error('unauthorized')) om te weigeren: een gegooide auth mislukt de upgrade. Bij succes opent de socket en wordt wsConnect uitgevoerd.

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:

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

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