Real-time engine for building reactive web applications in PHP with Swoole.
Inspired by go-via/via, this library brings the same reactive programming model to PHP using Swoole's async capabilities. Datastar (RC.7) acts as the glue between server and client, handling DOM morphing and SSE communication.
๐ฎ Try Live Examples - See php-via in action with interactive demos
- Datastar-powered - Reactive hypermedia framework handles client-side reactivity
- Twig templates - Familiar, powerful templating with Twig
- No JavaScript - Write server-side code only, Datastar handles the rest
- No build step - No transpilation or bundling
- Full reactivity - Real-time updates via SSE
- Single SSE stream - Efficient communication (very efficient with Brotli)
- Pure PHP - Leveraging Swoole's coroutines
- PHP 8.4+
- Swoole extension
- Composer
composer require mbolli/php-via<?php
require 'vendor/autoload.php';
use Mbolli\PhpVia\Via;
use Mbolli\PhpVia\Config;
$config = new Config();
$config->withTemplateDir(__DIR__ . '/templates');
$app = new Via($config);
$app->page('/', function (Context $c) {
$count = 0;
$step = $c->signal(1);
$increment = $c->action(function () use (&$count, $step, $c): void {
$count += $step->int();
$c->sync();
});
$c->view(function () use (&$count, $step, $increment, $c): string {
return $c->renderString('
<div id="counter"><!-- id is used to morph -->
<p>Count: {{ count }}</p>
<label>
Step:
<input type="number" data-bind="{{ step.id }}">
</label>
<button data-on:click="@post(\'{{ increment.url() }}\')">
Increment
</button>
</div>
', [
'count' => $count,
'step' => $step,
'increment' => $increment
]);
});
});
$app->start();Run the server:
php counter.phpThen open your browser to http://localhost:3000
The main application class that manages routing and the Swoole HTTP server.
use Mbolli\PhpVia\Config;
$config = new Config();
$config->withHost('0.0.0.0')
->withPort(3000)
->withDevMode(true)
->withLogLevel('debug');
$app = new Via($config);A Context represents a living connection between the server and browser. Each page gets its own context.
$app->page('/dashboard', function (Context $c) {
// Define signals, actions, and views here
});Routes can include dynamic path parameters using curly braces. Parameters are automatically injected into your callable by matching parameter names:
// Automatic parameter injection - matched by name, not order
$app->page('/blog/{year}/{month}/{slug}', function (Context $c, string $year, string $month, string $slug) {
// All parameters are automatically injected
echo "Blog post: $year/$month/$slug";
});
// Alternative: Manual parameter retrieval
$app->page('/users/{username}', function (Context $c) {
$username = $c->getPathParam('username');
// Both methods work - choose your preference
});How it works:
- Parameters are matched by name from your function signature
- Order doesn't matter -
function($c, $slug, $year)works the same - Missing parameters get their default value, or empty string if no default
- The Context
$cis always passed first - Both new (auto-injection) and old (
getPathParam()) methods work
Signals are reactive values that sync between server and client automatically using Datastar's data model.
$name = $c->signal('Alice');
// In your Twig template:
<input type="text" data-bind="{{ name.id }}">
<span data-text="${{ name.id }}"></span>
// Access the value in PHP:
$name->string() // Get as string
$name->int() // Get as integer
$name->bool() // Get as boolean
$name->float() // Get as floatActions are server-side functions triggered by client events via Datastar.
$saveAction = $c->action(function () use ($c): void {
// Do something
$c->sync(); // Push updates to browser
});
// In your Twig template:
<button data-on:click="@post('{{ saveAction.url }}')">Save</button>Views can use Twig templates (inline or from files) or plain PHP functions. php-via automatically registers a @via Twig namespace for accessing templates.
Inline Twig:
$c->view(function () use ($data, $c): string {
return $c->renderString('<h1>Hello, {{ name }}!</h1>', [
'name' => $data->name
]);
});php-via uses Datastar attributes for reactivity:
{# Two-way data binding with signals - use the bind() Twig function #}
<input type="text" data-bind="{{ nameSignal.id() }}">
{# Display signal value (note the $ sign to access the signal) #}
<span data-text="${{ nameSignal.id() }}"></span>
{# Trigger actions on events - actions use @get() #}
<button data-on:click="@get('{{ saveAction.url() }}')">Save</button>
{# Actions with specific keys #}
<input data-on:keydown="evt.key == 'Enter' && @get('{{ submitAction.url() }}')">
{# Change events #}
<select data-on:change="@get('{{ updateAction.url() }}')">...</select>See Datastar documentation for more attributes and patterns.
php-via automatically detects the scope of each page and optimizes rendering accordingly:
Global Scope (app-wide state):
- Pages using only global actions (no route actions, no signals)
- State is shared across ALL routes and users
- View is rendered once and cached globally (maximum performance)
- Example: Notification system visible on every page
Route Scope (shared state):
- Pages using only route actions (no global actions, no signals)
- State is shared across all users/tabs on the same route
- View is rendered once per route and cached
- Example: Game of Life with global board state
Custom Scope (domain-specific state):
- Define custom scopes for specific resources (e.g.,
stock:NFLX,chat:room123) - State is shared across all users/tabs accessing the same resource
- Example: Stock ticker showing real-time data for a specific stock symbol
Tab Scope (per-user state):
- Pages using signals (personal state) or mixing scopes
- Each user/tab has independent state
- View renders fresh for each context
- Example: User profile, shopping cart
// Global scope (cached app-wide):
$app->page('/anywhere', function (Context $c) use ($app) {
$c->scope(Scope::GLOBAL);
$notify = $c->action(function (Context $c) use ($app): void {
$count = $app->globalState('notifications', 0);
$app->setGlobalState('notifications', $count + 1);
$app->broadcast(Scope::GLOBAL); // Updates ALL pages
}, 'notify');
// No signals, no route actions = Global scope
});
// Route scope (cached per route):
$app->page('/game', function (Context $c) {
$c->scope(Scope::ROUTE);
$toggle = $c->action(function (Context $c): void {
GameState::toggle();
$c->broadcast();
}, 'toggle');
// No signals, no global actions = Route scope
});
// Tab scope (not cached):
$app->page('/profile', function (Context $c) {
$name = $c->signal('Alice');
// Uses signals = Tab scope
});The scope is detected automatically - no manual configuration needed!
Components are reusable sub-contexts with their own state and actions:
$counterComponent = function (Context $c) {
$count = 0;
$increment = $c->action(function () use (&$count, $c): void {
$count++;
$c->sync();
});
$c->view(function () use (&$count, $increment, $c): string {
return $c->renderString('
<div>
<p>Count: {{ count }}</p>
<button data-on:click="@post(\'{{ increment.url() }}\')">Increment</button>
</div>
', [
'count' => $count,
'increment' => $increment
]);
});
};
$app->page('/', function (Context $c) use ($counterComponent) {
$counter1 = $c->component($counterComponent);
$counter2 = $c->component($counterComponent);
$c->view(function () use ($counter1, $counter2): string {
return <<<HTML
<div>
<h1>Counter 1</h1>
{$counter1()}
<h1>Counter 2</h1>
{$counter2()}
</div>
HTML;
});
});php-via fundamentally relies on:
- Long-lived event loop - Swoole provides coroutines and async I/O, similar to Go's goroutines
- Reactive state on the server - Signals track changes and sync with the browser
- Server-side actions - Client events trigger server-side PHP functions
- UI defined on the server - Views are rendered with PHP, not templates
- Single SSE channel - Efficient real-time communication via Server-Sent Events with Brotli compression
- Even large updates (e.g., 2500 divs in Game of Life) compress to ~0.1-0.2 KB per update
- Repetitive HTML structure compresses extremely well
- Initial Page Load: Server renders HTML and establishes an SSE connection
- SSE Connection: Browser maintains an open connection to receive live updates
- User Interaction: User clicks button โ Browser sends signal values + action ID
- Action Execution: Server executes the action with current signal values
- Sync Changes: Server pushes HTML patches and signal updates via SSE
- DOM Merge: Datastar merges the patches into the DOM reactively
- Swoole: Provides async I/O, coroutines, and HTTP server
- Datastar: The glue between server and client - handles SSE communication, DOM merging, and reactive data binding
- Twig: Server-side templating with Datastar attributes
- Server-Sent Events: Unidirectional stream from server to browser for real-time updates
๐ฎ Try Live Examples - Interactive demos running in production
Check the examples/ directory for more examples:
counter_basic.php- Simple counter with reactive signalscounter.php- Counter with step control and componentsgreeter.php- Form handling with multiple inputspath_params.php- Dynamic path parameters demonstrationcomponents.php- Reusable component patternstodo.php- Todo list (multiplayer)game_of_life.php- Conway's Game of Life with real-time updates (multiplayer)global_notifications.php- Global scope notification systemstock_ticker.php- Real-time stock ticker with custom scopes and Apache EChartschat_room.php- Multi-room chat applicationall_scopes.php- Demonstrates all scope types in one exampleclient_monitor.php- Live client monitoring with server stats
php-via follows Semantic Versioning 2.0.0. This means:
- Major version (e.g., 2.0.0) - Breaking changes to the public API
- Minor version (e.g., 1.1.0) - New features, backward compatible
- Patch version (e.g., 1.0.1) - Bug fixes, backward compatible
You can safely upgrade within the same major version without breaking your code.
This library is inspired by and builds upon:
- ๐ go-via/via - The original Go implementation
- ๐ Datastar - The reactive hypermedia framework
- ๐ Swoole - PHP async/coroutine framework
MIT
Contributions are welcome! Please feel free to submit pull requests or open issues.
# Clone the repository
git clone https://github.com/mbolli/php-via.git
cd php-via
# Install dependencies
composer install
# Run the counter example
php examples/counter.php
# Code quality tools
composer phpstan # Run static analysis
composer cs-fix # Fix code style- Core Via class with routing
- Context management
- Reactive signals
- Action triggers
- SSE support
- Path parameters with auto-injection
- Automatic scope detection and caching
- Custom scopes (resource-specific state)
- Component system
- Testing suite (46+ comprehensive tests)
- HEAD method support
- Session management
- More examples and documentation
- Documentation site
