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

<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) => /* ... */);

Broadcasting — push updates to other connected clients

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

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              # 101 tests, 258 assertions
composer phpstan             # PHPStan level 6
composer cs-fix              # code style

Deployment

Single OpenSwoole process behind a reverse proxy. See deploy/ for systemd + Caddy configs.

Browser → Caddy (TLS + Brotli) → OpenSwoole :3000

Roadmap

  • Route groups ($app->group('/prefix', fn))
  • initAtBoot() — explicit hook for boot-time shared state initialisation
  • Global intervals ($app->setInterval() — one shared timer per server process)

Credits

License

MIT

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors