Build Real-Time, Multi-User PHP Apps Without JavaScript Using PHP-VIA
PHP‑VIA transforms PHP into a responsive, multi‑user server that delivers real‑time web interfaces without any JavaScript, leveraging OpenSwoole, Datastar, and Twig; the guide covers installation, core concepts such as signals, actions, scopes, views, components, and deployment tips.
Overview
PHP‑VIA converts PHP code into a responsive multi‑user server application using only PHP and OpenSwoole. It relies on Datastar for client‑side reactivity, Server‑Sent Events (SSE) and DOM morphing, and Twig for templating.
System requirements
PHP 8.4 or higher
OpenSwoole extension
Composer
Installation
composer require mbolli/php-viaQuick start
<?php
require 'vendor/autoload.php';
use Mbolli\PhpVia\Via;
use Mbolli\PhpVia\Config;
use Mbolli\PhpVia\Context;
$config = new Config();
$config->withTemplateDir(__DIR__ . '/templates');
$app = new Via($config);
$app->page('/', function (Context $c): void {
$count = $c->signal(0, 'count');
$step = $c->signal(1, 'step');
$increment = $c->action(function () use ($count, $step, $c): void {
$count->setValue($count->int() + $step->int());
$c->syncSignals();
}, 'increment');
$c->view('counter.html.twig', [
'count' => $count,
'step' => $step,
'increment' => $increment,
]);
});
$app->start();counter.html.twig template:
<div id="counter">
<p>Count: <span data-text="${{ count.id }}">{{ count.int }}</span></p>
<label>Step: <input type="number" data-bind="{{ step.id }}"></label>
<button data-on-click="@post('{{ increment.url }}')">Increment</button>
</div>Signals – real‑time synchronized state
$name = $c->signal('Alice', 'name');
$name->string(); // read current value
$name->setValue('Bob'); // update and push to browserTemplate binding examples:
<input data-bind="{{ name.id }}">
<span data-text="${{ name.id }}">{{ name.string }}</span>Actions – server functions triggered by client events
$save = $c->action(function () use ($c): void {
// custom logic …
$c->sync();
}, 'save');Template usage:
<button data-on-click="@post('{{ save.url }}')">Save</button>Scopes – controlling state sharing
Scope::TAB – isolated per browser tab (default).
Scope::ROUTE – shared among all users on the same route.
Scope::SESSION – shared across tabs of the same session.
Scope::GLOBAL – visible to every connected user.
Custom scopes (e.g., room:lobby) – shared among connections that join the same custom group.
Views
Render a Twig file or an inline string:
$c->view('dashboard.html.twig', ['user' => $user]);Path parameters (automatic injection)
$app->page('/blog/{year}/{slug}', function (Context $c, string $year, string $slug): void {
// $year and $slug are injected automatically
});Components – reusable sub‑contexts with independent state
$a = $c->component($counterWidget, 'a');
$b = $c->component($counterWidget, 'b');Lifecycle hooks
$c->onDisconnect(fn() => /* clean up */);
$c->setInterval(fn() => $c->sync(), 2000); // sync every 2 seconds
$app->onClientConnect(fn(string $id) => /* … */);Broadcasting
$c->broadcast(); // current scope
$app->broadcast(Scope::GLOBAL); // all users
$app->broadcast('room:lobby'); // custom scopeHow it works (request‑response flow)
Browser requests a page → server renders full HTML and opens an SSE stream.
User interaction (click, input) → Datastar sends a POST with signal values and action identifier.
Server executes the corresponding action and updates signals.
Server pushes HTML patches and signal updates via SSE.
Datastar applies DOM morphs on the client, producing instant UI updates without a full page reload.
Real‑time example demos (source code available)
Counter (TAB scope)
Greeting form
Shared to‑do list (ROUTE)
Independent component demo
Path‑parameter demo
Stock ticker (custom scope + timer)
Multi‑room chat
Client‑connection dashboard
Conway's Game of Life (multi‑user)
Mixed‑scope showcase
Server‑streamed DOOM game via SSE
Demo site: https://via.zweiundeins.gmbh/examples
Development environment setup
git clone https://github.com/mbolli/php-via.git
cd php-via && composer install
cd website && php app.php # start demo on port 3000
vendor/bin/pest # 85 tests, 240+ assertions
composer phpstan # static analysis (level 6)
composer cs-fix # code‑style fixesDeployment recommendations
Run a single OpenSwoole process behind a reverse proxy (e.g., Caddy). The repository’s deploy/ folder contains a systemd service file and example Caddy configuration.
Browser → Caddy (TLS + Brotli) → OpenSwoole :3000Repository
Source code: https://github.com/mbolli/php-via.git
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
