Skip to content

mbolli/php-via

Repository files navigation

php-via

Latest Version on Packagist Total Downloads License PHP Version CI PHPStan Docs

php-via

Real-time reactive web framework for PHP. Server-side reactive UIs with zero JavaScript, using OpenSwoole for async PHP, Datastar (RC.8) for SSE + DOM morphing, and Twig for templating.

Documentation & Live Examples

Why php-via?

  • No JavaScript to write — Datastar handles client-side reactivity, SSE, and DOM morphing
  • Twig templates — familiar, powerful server-side templating
  • No build step — no transpilation, no bundling, no node_modules
  • Real-time by default — every page gets a live SSE connection
  • Scoped state — TAB, ROUTE, SESSION, GLOBAL, and custom scopes control who shares what
  • Single SSE stream — extremely efficient with Brotli compression

Requirements

  • PHP 8.4+
  • OpenSwoole PHP extension
  • Composer
  • Brotli PHP extension (optional, required for Config::withBrotli())

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:

<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>
php app.php
# → http://localhost:3000

Core Concepts

Full documentation at via.zweiundeins.gmbh/docs

Signals — reactive state that syncs between server and client

$name = $c->signal('Alice', 'name');
$name->string();          // read
$name->setValue('Bob');    // write → auto-pushes to browser
<input data-bind="{{ name.id }}">
<span data-text="${{ name.id }}">{{ name.string }}</span>

Actions — server-side functions triggered by client events

$save = $c->action(function () use ($c): void {
    $c->sync();
}, 'save');
<button data-on:click="@post('{{ save.url }}')">Save</button>

Scopes — control who shares state and receives broadcasts

Scope Sharing Use Case
Scope::TAB Isolated per tab (default) Personal forms, settings
Scope::ROUTE All users on same route Shared boards, multiplayer
Scope::SESSION All tabs in same session Cross-tab state
Scope::GLOBAL All users everywhere Notifications, announcements
Custom ("room:lobby") All contexts in that scope Chat rooms, game lobbies

Views — Twig template files or inline strings

$c->view('dashboard.html.twig', ['user' => $user]);

Path Parameters — auto-injected by name

$app->page('/blog/{year}/{slug}', function (Context $c, string $year, string $slug): void {
    // ...
});

Components — reusable sub-contexts with isolated state

$a = $c->component($counterWidget, 'a');
$b = $c->component($counterWidget, 'b');

Lifecycle Hooks

$c->onDisconnect(fn() => /* cleanup */);
$c->setInterval(fn() => $c->sync(), 2000);  // auto-cleaned on disconnect
$app->onClientConnect(fn(string $id) => /* ... */);
$app->setInterval(fn() => $app->broadcast(Scope::GLOBAL), 5000); // process-wide timer

Route Groups — shared prefix and/or middleware

$app->group('/admin', function (Via $app): void {
    $app->page('/', fn(Context $c) => ...);      // → /admin
    $app->page('/users', fn(Context $c) => ...); // → /admin/users
})->middleware(new AuthMiddleware());

Broadcasting — push updates to other connected clients

$c->broadcast();                    // same scope
$app->broadcast(Scope::GLOBAL);     // all contexts
$app->broadcast('room:lobby');      // custom scope

Multi-node broadcasting — Redis and NATS brokers

By default php-via uses an InMemoryBroker that is correct for single-process deployments. To fan out broadcast() calls across multiple servers or containers, swap in RedisBroker or NatsBroker:

use Mbolli\PhpVia\Broker\RedisBroker;
use Mbolli\PhpVia\Broker\NatsBroker;

// Redis (requires ext-redis + SWOOLE_HOOK_ALL)
$config->withBroker(new RedisBroker('127.0.0.1', 6379));

// Redis with auth and TLS
$config->withBroker(new RedisBroker(
    host: 'redis.internal',
    password: $_ENV['REDIS_PASSWORD'],
    tls: true,
));

// NATS (raw OpenSwoole socket — no extra extension)
$config->withBroker(new NatsBroker('127.0.0.1', 4222));

// NATS with token auth and TLS
$config->withBroker(new NatsBroker(
    host: 'nats.internal',
    authToken: $_ENV['NATS_TOKEN'],
    tls: true,
));

// Error observability — called on every connection drop
$config->onBrokerError(fn(\Throwable $e) => error_log('Broker: ' . $e->getMessage()));

Both brokers reconnect automatically with exponential backoff (1 s → 30 s cap).

A GET /_health endpoint is available on every php-via server — no configuration needed:

{"status":"ok","version":"0.7.0","broker":{"driver":"RedisBroker","connected":true},"connections":{"contexts":42,"sse":38}}

Returns HTTP 503 when the broker is in the reconnect backoff window.

How it Works

1. Browser requests page     →  Server renders HTML, opens SSE stream
2. User clicks button        →  Datastar POSTs signal values + action ID
3. Server executes action    →  Modifies signals / state
4. Server pushes patches     →  HTML fragments + signal updates via SSE
5. Datastar morphs DOM       →  UI updates without page reload

Development

git clone https://github.com/mbolli/php-via.git
cd php-via && composer install

cd website && php app.php    # run website + examples on :3000

vendor/bin/pest              # run tests
composer phpstan             # PHPStan level 6
composer cs-fix              # code style

Credits

License

MIT

About

Real-time engine for building reactive web applications in PHP with Swoole.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors