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.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Build Real-Time, Multi-User PHP Apps Without JavaScript Using PHP-VIA

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-via

Quick 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 browser

Template 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 scope

How 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 fixes

Deployment 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 :3000

Repository

Source code: https://github.com/mbolli/php-via.git

real-timePHPSSETwigOpenSwoole
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.