Skip to content

Releases: mbolli/php-via

v0.8.0 (2026-05-04)

04 May 14:58

Choose a tag to compare

Breaking Changes

  • Config::withTrustProxy() removed: the dynamic per-request base-path detection
    mechanism (detectBasePathFromRequest(), withTrustProxy(), getTrustProxy()) has been
    removed entirely. The base path must now be known at startup and set via
    Config::withBasePath(string $basePath), which throws \InvalidArgumentException for
    invalid values (absolute URLs, protocol-relative paths, backslashes, etc.).
    Migration: remove any ->withTrustProxy(true) call; if your app is mounted at a sub-path,
    set it explicitly: ->withBasePath('/myapp').

New Features

  • Multi-worker support: php-via can now run with multiple OpenSwoole workers sharing a
    single port, enabling CPU parallelism on multi-core hosts.

    • Config::withWorkerNum(int $n): set the number of worker processes (default 1). Requires
      a multi-worker-capable broker.
    • Config::withGlobalStateTableSize(int $maxRows, int $maxValueBytes): tune the shared
      memory table used for GlobalState in multi-worker mode (defaults: 1024 rows × 4096 B).
    • SwooleBroker: new broker that uses OpenSwoole's inter-worker IPC pipe to fan-out
      scope invalidations across all worker processes on the same machine. No external
      infrastructure required. InMemoryBroker is rejected at startup when worker_num > 1.
    • SharedTable: wraps OpenSwoole\Table (shared memory, mmap'd into all workers on
      fork) as the GlobalState backend when worker_num > 1. Values are PHP-serialized;
      throws \OverflowException if a value exceeds the configured column size.
    • Session-affinity dispatch: when worker_num > 1, a custom dispatch_func routes
      every request from the same browser session (page load, SSE, action POSTs) to the same
      worker via SessionManager::workerForRequest(). No external load balancer required; all
      Context lookups and sessionData() calls always hit the correct process.
  • POOL_MODE + USR1 graceful worker reload: the server now runs in POOL_MODE.
    Sending SIGUSR1 to the master process triggers graceful worker rotation without dropping
    active connections; the fresh worker re-includes route definitions, picking up new class
    definitions from disk. The master PID is written to sys_get_temp_dir()/php-via-master.pid
    in dev mode.

  • composer run dev / scripts/dev.sh hot-reload workflow: an entr-based file
    watcher that sends SIGUSR1 on every .php or .twig change, plus an optional pnpm CSS
    watcher for the website. Run composer run dev from the project root; requires entr
    (apt install entr / brew install entr).

  • File upload + SharedWorker Upload demos: two new website examples: a streaming
    file-upload progress example and a SharedWorker-based upload demo that maintains upload
    state across multiple tabs.

  • Auto-inject signals and actions into Twig: Context::render() (and $c->view() with
    a template string) now automatically injects all registered signals and actions as named
    variables into every Twig render. No manual data-array passing required:

    • Signals are keyed by their user-supplied baseName (e.g. $c->signal(0, 'count'){{ count }})
    • Actions are keyed by the camelCase form of their registration name
      (e.g. 'refresh-graphs'{{ refreshGraphs }})
    • Explicit entries in the $data array still win on conflict
    • Result is memoized after the first call; zero overhead on SSE ticks
    • A _via debug key is injected in dev mode listing all injected signal and action names
  • Context::getSignal(string $name): ?Signal: retrieve a registered signal by its
    user-supplied name. Works for all scopes (TAB, ROUTE, SESSION, GLOBAL, custom).

  • Context::getAction(string $name): ?Action: retrieve a registered action by its
    user-supplied name. Useful in action bodies that need sibling actions without captured vars.

Security

  • Session ownership enforced on /_action and /_sse: both handlers now verify that
    the caller's session cookie matches the session that originally created the context.
    A mismatch or absent cookie returns HTTP 403. Contexts with no session binding (e.g.
    GLOBAL-scoped pages) remain openly accessible.
  • CSRF hardening on /_action: three weaknesses addressed:
    1. GET requests to /_action/{id} now return HTTP 405 (Method Not Allowed), preventing
      top-level cross-site navigation CSRF.
    2. When no withTrustedOrigins() allowlist is configured, the framework no longer allows
      all origins by default; it falls back to a same-host check derived from the Host
      header. Absent Origin is only permitted in dev mode (curl/local tools); in production
      it is denied.
    3. website/app.php now enables withSecureCookie(true) and withTrustedOrigins() in
      non-dev mode when CORS_ORIGIN is set to a concrete origin.

Bug Fixes

  • Broadcast context count: logBroadcast for route:/path scopes always logged 0
    contexts (hardcoded). syncContextsOnRoute() now returns the actual count of synced
    contexts and it is passed to the logger.
  • Shutdown signal loop: workers no longer call $server->shutdown() on SIGINT/SIGTERM,
    which was sending SIGTERM back to the master, causing a cascading loop that left orphaned
    processes holding the port. Workers now run cleanup callbacks and exit(0). The
    master-process onStart handler is the sole driver of $server->shutdown().
  • Website examples: various fixes including chat-room typing indicator (hidden from the
    typing user; stale user list on disconnect), file-upload and login template variable
    alignment with signal baseNames, real IP extraction from proxy headers in SseHandler,
    and data-ignore-morph on the file-upload nav-confirm dialog.

Refactoring

  • All 15 website example files updated to use auto-inject: signal and action variables
    are no longer passed explicitly to render() / view() calls. Action closures now
    receive the Context as a parameter and call $ctx->getSignal() instead of capturing
    variables from the outer scope.

Documentation

  • Twig docs: rewrote "Passing data to templates" section; updated reactive-text,
    two-way binding, and action examples to use auto-injected Signal/Action objects
    ({{ count.int }}, {{ inc.url }}).
  • Views docs: removed manual signal/action passing from "Defining a view" and
    "Partial updates" examples; simplified component view example.
  • Components docs: removed manual count_id/count_val/inc_url passing from the
    "Creating a component" example.
  • API docs: documented auto-inject behaviour on view(); added getSignal() and
    getAction() method entries; updated component example.

0.7.1 (2026-04-22)

22 Apr 06:29

Choose a tag to compare

Bug Fixes

  • Signal native storageSignal::setValue() no longer pre-encodes arrays/objects as
    JSON strings. Values are stored as their native PHP type and serialized once by the Datastar
    SDK when building the SSE payload. Previously, arrays were double-encoded and arrived at the
    client as a JSON string instead of an array.
  • Signal::string() / Signal::bool() — both methods now guard against array/object
    values to avoid PHP "Array to string conversion" notices.

New API

  • Signal::array(): array — convenience accessor that returns the signal value cast to
    a PHP array, consistent with the existing int(), float(), string(), and bool() casts.

Dependencies

  • Upgraded Datastar JS client to v1.0.1 (public/datastar.js).
  • Upgraded Datastar PHP SDK to v1 final; pinned starfederation/datastar-php to stable ^1.0 (resolved to 1.0.0) in root and website lockfiles.
  • Removed src/Via.php from PHPStan analysis (OpenSwoole stub gap for Event::EVENT_READ); suppressed false-positive alwaysTrue warning in SseHandler.
  • CI: install website/vendor before running PHPStan.

v0.7.0 (2026-04-10)

10 Apr 15:10

Choose a tag to compare

New Features

  • Proactive GC timerVia now runs gc_collect_cycles() on a configurable periodic timer (default 30 s) to prevent PHP's cycle collector from causing unpredictable mid-request pauses as circular references accumulate in the long-running process.

    • Config::withGcInterval(int $ms) — set interval in milliseconds; pass 0 to disable and rely on PHP's automatic trigger
    • Via::runGcCycle() — the GC tick body, public so it can be called from tests or triggered manually
    • Each run logs at debug level: GC: N cycles freed, mem=X MB peak=Y MB
    • Stats::getAll() now includes gc_runs and gc_cycles_freed counters
  • Via::setInterval(callable $callback, int $ms): void — Process-wide recurring timer. Started automatically when the server starts, cleared on shutdown. No manual onStart/onShutdown wiring needed.

  • Via::group(string|callable $prefixOrFn, ?callable $fn): RouteGroup — Register a group of routes with an optional URL prefix and/or shared middleware.

    // With prefix — routes declared with short paths, prefix prepended automatically
    $app->group('/admin', function (Via $app): void {
        $app->page('/', fn(Context $c) => ...);      // → /admin
        $app->page('/users', fn(Context $c) => ...); // → /admin/users
    })->middleware(new AuthMiddleware());
    
    // Middleware-only (no prefix)
    $app->group(function (Via $app): void {
        $app->page('/login/dashboard', fn(Context $c) => ...);
        $app->page('/login/profile', fn(Context $c) => ...);
    })->middleware(new AuthMiddleware());
  • MessageBroker — pluggable multi-node broadcastingbroadcast() now propagates across workers/servers via a swappable broker. Ships with InMemoryBroker (default, single-node), RedisBroker, and NatsBroker.

    • Both brokers support auth ($password/$authToken, #[\SensitiveParameter]), TLS ($tls, $tlsCaFile), and a custom channel/subject param to isolate traffic
    • Auto-reconnect with exponential backoff (1 s base, 30 s cap); Config::onBrokerError() for observability
    • Config::withBroker() / Config::getBroker() for configuration
    • TAB scope skips broker publish (no cross-node recipients possible)
  • GET /_health — JSON health endpoint: {"status":"ok"|"degraded","broker":{…},"connections":{…}}. HTTP 503 when broker is in reconnect backoff.

  • Scope injection protection — broker wire scopes are validated via Scope::isValidWireScope() before syncLocally(); invalid scopes are logged and dropped.

v0.6.0 (2026-04-08)

08 Apr 06:24

Choose a tag to compare

New Features

  • Cookie helpers — Safe, coroutine-friendly cookie access on Context:

    • $c->cookie(string $name): ?string — read a request cookie (replaces $_COOKIE, which is unsafe in OpenSwoole)
    • $c->setCookie(string $name, string $value, ...) — queue a cookie for the response; safe defaults: secure: true, httpOnly: true, sameSite: 'Lax'
    • $c->deleteCookie(string $name, string $path = '/') — expire a cookie (sets expires=1, empty value)
    • Queued cookies are flushed to the HTTP response by RequestHandler (page load) and ActionHandler (action). SSE streams seal their headers after the first write, so cookies must be set before or via an action response.
  • File upload support$c->file(string $name): ?array returns the upload array (name, type, tmp_name, size) for multipart form submissions, or null if missing or errored. Use Datastar's contentType: 'form' modifier to submit a <form enctype="multipart/form-data"> as a real multipart POST.

  • Multipart signal parsingVia::parseSignals() (public static) handles three signal sources: GET ?datastar=<json>, JSON body (standard actions), and $post['datastar'] field (urlencoded forms). The existing readSignals() is now a thin wrapper. via_ctx fallback extracted from $request->post for multipart actions where Datastar sends no signals.

  • Brotli compression — Native PHP brotli compression via ext-brotli, served over HTTPS/HTTP2 from OpenSwoole directly (no proxy required). Requires either withCertificate() or withH2c().

    • Config::withBrotli(bool $enabled, int $dynamicLevel = 4, int $staticLevel = 11) — enable brotli with configurable levels. Dynamic level (4) is used for pages and SSE streams (hot path, low CPU). Static level (11 = max ratio) is used for static assets, lazy-compressed once and cached in memory per process.
    • Config::withCertificate(string $certFile, string $keyFile) — direct TLS termination in OpenSwoole. Enables HTTP/2 automatically.
    • Config::withH2c(string $enabled) — h2c (cleartext HTTP/2) for proxy scenarios where Caddy/Nginx handles TLS and proxies to OpenSwoole via h2c. Satisfies the brotli HTTPS requirement without needing a cert on the PHP side.
    • BrotliMiddleware — PSR-15 setWriter-style middleware. Attaches brotli_write and brotli_finish callables as PSR-7 request attributes before delegating. Implements SseAwareMiddleware so it also runs on SSE handshake requests. Auto-registered as the outermost global middleware when brotli is enabled.
    • Hard error at start() if ext-brotli is missing or HTTPS/h2c is not configured.

Improvements

  • SSE: removed unnecessary 30-second keepalive comment (not needed with HTTP/2 or Caddy; was corrupting brotli streams).
  • Contact Form example — Demonstrates multipart file upload, server-side per-field validation, and block re-rendering via SSE. State shared through PHP reference captures (no client-reactive signals needed).

Tests

  • Unit tests now run by defaultphpunit.xml updated to include tests/Unit/ in the default test suite. vendor/bin/pest now runs all 236 tests (Feature + Unit).
  • SignalFactory test semantics corrected — Scoped signals intentionally do NOT update their value on re-registration (only the first registration uses $initialValue). This prevents a re-render or a second joining context from overwriting live shared state with a stale initial value. Test expectations updated to match; setValue() is the documented mutation path.

v0.5.0 (2026-03-25)

25 Mar 19:44

Choose a tag to compare

New Features

  • PSR-15 Middleware — Full middleware support with global and per-route registration. Global middleware runs on all page/action requests via $app->middleware(). Per-route middleware via $app->page('/admin', ...)->middleware(new AuthMiddleware()). Implements the onion model with zero overhead when no middleware is registered. OpenSwoole requests are converted to PSR-7 at the boundary and back.
    • SseAwareMiddleware marker interface for middleware that should also run on SSE handshake requests
    • MiddlewareDispatcher — onion-style PSR-15 pipeline executor
    • PsrRequestFactory / PsrResponseEmitter — OpenSwoole ↔ PSR-7 adapters
    • RouteDefinition — fluent API for route + handler + middleware
    • Middleware attributes bridged to Context::getRequestAttribute() / Context::getRequestAttributes()
  • Per-session data storageContext::sessionData(), setSessionData(), clearSessionData() for server-side per-session state keyed on session cookie. Survives page refreshes and context destruction (unlike signals).
  • Context::input(string $name, mixed $default) — Safe replacement for $_GET/$_POST access. Checks POST first, then query string. Coroutine-safe in OpenSwoole (superglobals are not).
  • CSRF protectionActionHandler validates the Origin header against a configurable allowlist (Config::withTrustedOrigins()). null = no restriction (dev), [] = block all, ['https://...'] = strict allowlist.
  • Secure session cookiesConfig::withSecureCookie(true) enables __Host- cookie prefix (enforces HTTPS, Path=/, no Domain), SameSite=Lax.
  • Action rate limitingConfig::withActionRateLimit(int $max, int $window) enables per-IP sliding-window rate limiting on action endpoints (returns 429 + Retry-After).
  • Proxy trustConfig::withTrustProxy(bool) gates X-Base-Path header processing behind an explicit opt-in.
  • NATS Visualizer example — JetStream, durable consumers, KV heartbeats with OpenSwoole-native NatsClient.
  • Login Flow example — Now demonstrates PSR-15 AuthMiddleware with a public login form and a middleware-protected dashboard route. Auth data flows via request attributes.
  • Middleware docs page — Full documentation covering global/per-route middleware, writing middleware, SSE-aware middleware, request attributes, and built-in security features.

Security

  • Superglobal elimination — Removed all $_GET/$_POST/$_FILES/$_SESSION writes from RequestHandler. Migrated 7 example files (12 call sites) from superglobals to Context::input() / Context::sessionData().
  • XSS fixdump() Twig function output is now HTML-escaped (htmlspecialchars). Previously marked is_safe => ['html'] without escaping.
  • /_stats endpoint — Now gated behind devMode. Previously exposed client IPs and memory usage to unauthenticated requests.

Improvements

  • Shopping Cart — Migrated cart storage to sessionData API.
  • Wizard example — Wizard state persists across page refreshes via sessionData.
  • Updated API docs with middleware, sessionData, input(), and security Config options.
  • Updated comparisons table: Auth/middleware marked as ✓ (was "coming soon").

Dependencies

  • Added psr/http-server-middleware ^1.0 and nyholm/psr7 ^1.8
  • Added tuupola/cors-middleware ^1.5 (website project)

Chore

  • Added .gitattributes to exclude dev directories from Composer distribution.
  • Removed legacy examples-source/ folder and stale via:cut template markers.

v0.4.3 (2026-03-23)

23 Mar 16:27

Choose a tag to compare

New Features

  • Context::removeScope(string $scope) — Remove a scope from a live context so it no longer receives broadcasts targeting that scope. TAB scope is protected. Backed by new Via::unregisterContextInScope().
  • Live Search example — Instant client-side filtering with debounced signal updates. Demonstrates TAB-scoped input signals and conditional rendering.
  • Shopping Cart example — Multi-item cart with quantity controls, subtotals, and a running total. Demonstrates multiple TAB-scoped signals and computed view state.
  • Theme Builder example — Live colour/font customiser with full undo/redo history. Demonstrates TAB-scoped signal stacks and action composition.
  • Multi-step Wizard example — Guided form with step validation, progress indicator, and review step. Demonstrates TAB-scoped step state and conditional block rendering.
  • Live Auction example — Real-time shared auction with countdown clock, anti-snipe bid extension, bid history, and sold state. Demonstrates ROUTE scope + timer broadcasting with cacheUpdates: false.
  • Type Race example — Multiplayer typing race with custom per-room scope, live progress bars, WPM tracking, 3-second countdown, and "Race Again" that resets in-place and absorbs lone waiters from other rooms via live scope migration (removeScope / addScope).

Improvements

  • Chat Room messages persisted to SQLite (last 50 per room).

v0.4.2 (2026-03-19)

19 Mar 20:45

Choose a tag to compare

Bug Fixes

  • SSE reconnect race — UI hangs with HTTP 400 after a few minutes — When a
    client's SSE connection drops and immediately reconnects, two coroutines
    briefly overlap. The old coroutine's exit path unconditionally called
    scheduleContextCleanup(), firing a 5 s timer that destroyed the still-live
    context. The new SSE loop then polled a closed channel indefinitely, and every
    subsequent action returned 400. Fixed by tracking active SSE coroutine count
    per context (Via::$activeSseCount) and only scheduling cleanup when the last
    coroutine exits. A safety-valve reload is also sent if the context is found
    destroyed mid-loop.

Improvements

  • Debuggable TAB-scoped action IDs — TAB-scoped actions now prefix their
    random hex ID with the action name when one is provided (e.g.
    resize-3df6c542507ab8e1 instead of 3df6c542507ab8e1), making request logs
    readable without affecting uniqueness or security.

v0.4.1 (2026-03-19)

19 Mar 07:24

Choose a tag to compare

Bug Fixes

  • Timer leak via Context::interval()interval() called Timer::tick() directly, bypassing
    ContextLifecycle. Timers were never recorded and therefore never cancelled on context cleanup.
    After extended uptime, leaked timers accumulated
    and eventually drove the process to 100% CPU. Fixed by delegating to lifecycle->registerTimer()
    so every timer is tracked and cleared on cleanup, consistent with setInterval().

  • Callback accumulation in scheduleContextCleanup() — the method was called on every SSE
    disconnect, including reconnections, and each call unconditionally appended a new closure to
    ContextLifecycle::$cleanupCallbacks. A tab reconnecting N times accumulated N closures, growing
    without bound for the context's lifetime and contributing to memory pressure and GC load under
    prolonged uptime. Fixed by tracking registration state in $viaUnsetCallbackRegistered and
    skipping duplicate registrations.

v0.4.0 (2026-03-18)

18 Mar 12:26

Choose a tag to compare

Features

  • Auto-block view renderingview() now accepts a block: named parameter. On SSE updates the
    named Twig block is extracted and sent instead of the full page, eliminating the need to manually
    thread $isUpdate through view callables.

    // Before
    $c->view(fn (bool $isUpdate) => $c->render('todo.html.twig', $data, $isUpdate ? 'demo' : null));
    
    // After
    $c->view(fn () => $c->render('todo.html.twig', $data), block: 'demo');

    The block content must have a root element with a unique id — Datastar uses it to find the morph
    target in the DOM.

  • clientWritable flag for scoped signals — Scoped signals (ROUTE / SESSION / GLOBAL / custom)
    are now server-authoritative by default: client-sent values are silently ignored, preventing
    arbitrary clients from overwriting shared state. Pass clientWritable: true to opt a scoped signal
    in to client writes:

    // Server-authoritative (default) — client cannot overwrite
    $counter = $c->signal(0, 'count', Scope::ROUTE);
    
    // Collaborative — client may push values (e.g. data-bind on a shared input)
    $note = $c->signal('', 'note', Scope::ROUTE, clientWritable: true);

    TAB-scoped signals (the default) are always client-writable and unaffected by this change.

Bug Fixes

  • Fixed examples (GameOfLife, ChatRoom, ClientMonitor) that were sending full-page HTML patches
    instead of only the dynamic block. All three now use block: 'demo' and emit partial patches.

Website

  • Applied block: to all examples with a named update block (GameOfLife, ChatRoom, ClientMonitor,
    Todo, Components)
  • Added Signal Injection and Block Rendering test suites (17 new tests)
  • Updated docs: views, FAQ, and API reference reflect block: convention and clientWritable flag

v0.3.0 (2026-03-17)

17 Mar 08:52

Choose a tag to compare

Features

  • App-level hooksonClientConnect() / onClientDisconnect() callbacks fire when SSE connections open or close
  • Per-context overrides — custom shell template, head/foot HTML per page via Context API
  • Static file serving — built-in for development; serve CSS/JS/images without a reverse proxy
  • TUI request logger — colorful structured terminal output with method/status/timing glyphs
  • Component re-rendering — components participate in broadcast sync; dirty components re-render on page-level broadcasts
  • Performance — skip re-rendering clean components during page sync, reducing unnecessary SSE patches

Bug Fixes

  • fix: Coroutine::sleep() TypeError on OpenSwoole — use usleep() with SWOOLE_HOOK_ALL
  • fix: broken signal approach in live-poll replaced with patchElements
  • fix: component re-rendering wired into broadcast sync correctly
  • fix: shell template path co-located with HtmlBuilder (no more ../../templates/ relative path)

Website

  • Consolidated examples — 11 standalone example apps merged into the website's single Via server under /examples/{name}
  • Tabbed source panel — each example shows PHP handler and Twig template in switchable tabs (CSS-only, no JS)
  • Example summaries — 3–6 paragraph descriptions per example explaining the concepts demonstrated
  • Examples-source accuracy — all source display files updated to match actual handler logic (board model, scope prefixes, signal names, template variables)
  • Client Monitor revamp — replaced timer-driven polling with onClientConnect/onClientDisconnect hooks
  • Removed Global Notifications example (concepts merged into All Scopes)
  • All Scopes redesign — CSS class-based cards replacing inline styles, reduced emoji usage
  • Docs section — FAQ entries for PatchElementsNoTargetsFound and duplicate ID collision pitfalls; design philosophy page; comparison table with Phoenix LiveView column
  • Home page overhaul — tabbed code demos, glass UI, scope badges, animations
  • Twig {% code %} tag — syntax highlighting via mbolli/tempest-highlight-datastar package