8: Realtime

Er blijft één gat over: wanneer iemand anders stemt, beweegt jouw tabblad niet. In dit laatste hoofdstuk duwt de server resultaten naar elke open browser via phloWS, en dan verzend je het geheel. Het realtime-gedeelte is optioneel: het vereist de aparte phlo-websocket Node-service, en de peiling is compleet zonder dit. Het verzendgedeelte is niet optioneel.

8.1: phloWS, de optionele Node-service

WebSockets in Phlo draaien via phloWS, een kleine Node.js broker die buiten het framework leeft. Eén phloWS-proces bedient al je apps op één poort en routeert verbindingen op basis van de Host header. Voor elk binnenkomend bericht maakt het een eenmalige PHP-aanroep naar je app, zodat je handlers de volledige levenscyclus van het verzoek krijgen: database, sessie, alles.

git clone https://github.com/q-ainl/phlo-websocket.git <ws>
cd <ws>
npm install

In <ws>/websocket.js, koppel je je vhost aan het app-entrypoint en voer het dan uit:

require('./phloWS.js')(3001, 'php', {
    'localhost': '<app>/www/app.php',
})
node <ws>/websocket.js

Aan de app-kant geef je de runtime zijn WebSocket-poort in www/app.php en voeg je de realtime resources toe aan data/app.json:

phlo_app(
    id:    'Poll',
    host:  'localhost',
    build: true,
    debug: true,
    app:   dirname(__DIR__).'/',
    langs: dirname(__DIR__).'/langs/',
    websocket: 3001,
);
{
    "resources": ["...", "websocket", "DOM/websocket", "wsCast", "HTTP"]
}

(... staat voor alles wat al in je lijst staat.) websocket dispatcht binnenkomende evenementen naar je hooks, DOM/websocket is de browserclient die automatisch verbinding maakt en binnenkomende berichten doorstuurt via dezelfde apply()-mechanismen die je in hoofdstuk 6 hebt gebruikt, en wsCast plus HTTP laten PHP broadcasten. In productie geef je wss://.../websocket door aan je reverse proxy naar poort 3001. Herbouw; lint blijft [].

8.2: De hooks

Je app reageert op socket-evenementen via gewone functies. Maak poll.ws.phlo aan:

function wsConnect($host, $token, $socket){
    return true
}

function wsReceive($host, $token, $socket, ...$data){
    if (($data['type'] ?? null) === 'ping') return wsCast(wsTarget: $socket, pong: time())
}

wsConnect wordt uitgevoerd na de handshake; retourneer true om te accepteren. wsReceive wordt uitgevoerd voor elk clientbericht, JSON-gedecodeerd en verspreid in $data. Er zijn nog twee hooks (wsAuth voor tokenauthenticatie, wsClose voor opruimen); de poll heeft deze niet nodig, omdat de resultaten openbaar zijn. $socket is een ondoorzichtige id die je direct kunt targeten, zoals de ping-antwoord laat zien.

Een naamgevingsvalkuil: noem dit bestand niet websocket.phlo. Die klassenaam is in gebruik door de engine's websocket resource en de build faalt met een foutmelding over een dubbele klasse. poll.ws.phlo compileert de functies prima in de app.

8.3: Uitzenden bij stem

wsCast() plaatst berichten naar phloWS, dat deze naar verbonden browsers duwt. De payload is dezelfde commando-taal als apply(). Eén regel in de vote route:

route both POST poll vote $id {
    if (!$option = type_poll::record(id: (int)$id)) return false
    type_poll::change('id=?', (int)$id, votes: $option->votes + 1)
    wsCast(wsTarget: 'all', outer: ['#results' => $this->results])
    if (%req->async) return apply(
        outer: ['#results' => $this->results],
    )
    location('/poll')
}

wsTarget: 'all' bereikt elke verbonden client; een socket id of een array van ids verkleint het bereik. De browsers ontvangen outer: {'#results': ...} en werken de balken bij, precies alsof ze zelf hadden gestemd.

Open http://localhost/poll in twee vensters en stem in één: beide bewegen. En als phloWS niet werkt? wsCast() faalt stilletjes, stemmers krijgen nog steeds hun apply() antwoord, en de poll blijft werken. Realtime is hier een laag, geen afhankelijkheid.

8.4: Verzend het

Tijd om een deploybare build te produceren. Voeg een release-doel toe aan data/app.json:

{
    "resources": ["..."],
    "release": "%app/release/"
}

Bouw het vervolgens:

php www/app.php build::run
php www/app.php build::lint
php www/app.php build::release

build::release compileert een geminimaliseerde productie-build: PHP in release/, webassets in release/www/. Geef de release zijn eigen entrypoint, release/www/app.php, met de buildmodus uit en de paden gericht op de release-output:

<?php
require '/phlo/phlo.php';

phlo_app(
    id:    'Poll',
    host:  'poll.example.com',
    build: false,
    app:   dirname(__DIR__, 2).'/',
    php:   dirname(__DIR__).'/',
    www:   __DIR__.'/',
    data:  dirname(__DIR__, 2).'/data/',
    langs: dirname(__DIR__, 2).'/langs/',
    websocket: 3001,
);

data en langs wijzen naar de gedeelde app-mappen: de stemmen en vertalingen zijn status, geen build-output. Deploy met dezelfde Docker-image waarmee je begonnen bent, nu met release/www als de webroot en automatische HTTPS:

docker run -v $(pwd)/app:/app -p 80:80 -p 443:443 -p 443:443/udp \
    -e SERVER_NAME=poll.example.com ghcr.io/q-ainl/phlo

8.5: Waar nu?

Je hebt een full-stack app gebouwd in acht korte bestanden: een model, een controller, een stylesheet, een hooks-bestand en vier configuratiebestanden. Routing, views, CSS, ORM, async DOM-updates, vertalingen en realtime, allemaal in één taal.

Van hieruit:

Verzend iets.

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