Download the PHP package polidog/use-php without Composer
On this page you can find all versions of the php package polidog/use-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download polidog/use-php
More information about polidog/use-php
Files in polidog/use-php
Package use-php
Short Description React Hooks-like PHP components with server-side state management
License MIT
Informations about the package use-php
usePHP
A framework that delivers server-driven UI with minimal JavaScript, using a React Hooks-like API.
Features
- React Hooks-like API - Simple state management with
useState - Function Components (Recommended) - Lightweight components using simple PHP callables
- Built-in Router - Simple, swappable router with snapshot state preservation across pages
- Minimal JS (~40 lines) - Smooth UX with partial updates, graceful fallback without JS
- Pure PHP by default - No transpilation needed for
H::xxx()style components - PSX (optional) - TSX-like template syntax compiled by
usephp compilefor HTML-heavy UIs (see PSX section) - Configurable State Storage - Choose between session (persistent) or memory (per-request) storage
- Progressive Enhancement - Works even with JavaScript disabled
- Framework Integration - Works with Laravel, Symfony, and other frameworks
Installation
Quick Start
1. Create a Function Component
2. Create an Entry Point with Router
When you render your own layout, prefer UsePHP::renderClientScript() over a
hand-written <script src="/usephp.js">. It loads the published asset with
defer and includes a tiny inline fallback that hydrates deferred components
if the asset cannot be read; partial form updates still require the full
usephp.js file.
3. Start the Server
Open http://localhost:8000 in your browser.
Router
usePHP includes a built-in router that can be swapped or disabled for framework integration.
Basic Usage
URL Generation
useRouter Hook
Access router functionality within components:
The useRouter() hook returns:
navigate(routeName, params)- Generate URL for a named routecurrentUrl- Current request URLparams- Route parameters from current matchisActive(routeName)- Check if a route is currently active
Snapshot Behavior
Control how state is preserved across page navigations:
StorageType vs SnapshotBehavior
These two concepts control state at different levels:
| StorageType (Component) | SnapshotBehavior (Router) | |
|---|---|---|
| Scope | Individual component | Route/page transitions |
| Configuration | #[Component(storage: '...')] |
$router->get(...)->sessionSnapshot() |
| Purpose | How a component's state is stored | How snapshots are handled across routes |
Example: A TodoList component with storage: 'session' stores its own state in the session. Meanwhile, SnapshotBehavior::Persistent on a route controls whether the entire page snapshot is passed via URL when navigating to another route.
Framework Integration
When using usePHP within Laravel, Symfony, or other frameworks:
Architecture
With JavaScript (Partial Updates)
Without JavaScript (Fallback)
API
Component Definition
Function Components (Recommended)
Function components are simple PHP callables that return Elements. They are the recommended way to build components in usePHP.
Using function components:
fc() Storage Types:
The fc() function accepts an optional third parameter to specify the storage type:
| Storage Type | Description | Use Case |
|---|---|---|
Session |
State stored in PHP session | Default. Forms, shopping carts |
Memory |
State reset per request | Temporary UI state, modals |
Snapshot |
State embedded in HTML | Stateless server, shareable URLs |
Class-based Components
For more complex components that need lifecycle methods or dependency injection, you can use class-based components:
Component Storage Types
The #[Component] attribute accepts a storage parameter to control how component state is persisted:
| Storage Type | Description | Use Case |
|---|---|---|
session |
State stored in PHP session | Default. Forms, shopping carts, user preferences |
memory |
State reset per request | Temporary UI state, modals |
snapshot |
State embedded in HTML | Stateless server, shareable URLs |
useState
HTML Elements
Composing Components
PSX (optional, TSX-like syntax)
PSX is an opt-in alternative way to author components: you write HTML directly and usephp compile lowers it to the same H::xxx() calls shown above. The runtime is unchanged — PSX is purely a syntax layer.
When to use it
- Components with deeply nested HTML where
H::div(children: [...])becomes hard to read - Teams used to JSX/TSX who want familiar template syntax
If your components are simple, sticking with H::xxx() is fine — PSX adds a build step, while plain PHP doesn't.
Syntax at a glance
Lowers to (cached file inside var/cache/psx/):
Compile workflow
compile writes its output to var/cache/psx/ (configurable with --cache=PATH). Each .psx source file produces a sha1-named .php companion in that directory, plus a single manifest.php (FQCN → compiled-path map). The .psx source files in your project tree are the only PSX-related files you commit — the cache directory is intended to be ignored:
Loading PSX components at runtime
Composing PSX components
<Counter /> (PascalCase) inside another .psx resolves through PHP's use statements to a fully qualified class name, just like a regular class import:
If a component is registered at runtime instead of being defined in a .psx file, declare it with a comment so the compiler doesn't fail validation:
For the full spec — Fragment syntax, attribute dispatch, manifest format, edge cases — see docs/PSX.md.
Editor support
Syntax highlighting for .psx is bundled in editors/:
- Neovim / Vim —
editors/nvim/(lazy.nvim / packer / vim-plug / manual install) - VS Code —
editors/vscode/(.vsixbuild or local symlink)
Each editor's README walks through install + verify steps. An LSP / tree-sitter grammar / PHPStan extension are intentionally out of scope here and will land in dedicated repositories.
Deferred rendering (CDN-friendly partial hydration)
Some components depend on per-user state (logged-in name, cart count, A/B bucket) and would otherwise force the entire page out of the CDN cache. The fix is to split such a component into two pieces:
- A base component that does the actual rendering.
- A deferred wrapper that carries the defer config —
fc(..., defer: new Defer(...))for closure components, or#[Defer(...)]for class components.
The page references the wrapper, which renders only a fallback in the cacheable
HTML. The real component is fetched after page load via a separate GET to a
dedicated endpoint /_defer/{name}.
usephp compile discovers Defer configs and writes a sidecar
deferred-manifest.php next to the regular manifest — loadComponentManifest()
picks it up and auto-calls registerDeferred() for each entry, so the manual
wiring goes away. Class components use the same flow:
What happens at runtime:
- SSR invokes the wrapper, which (on the page-render path) emits
<div data-usephp-defer-url="/_defer/user-header">…fallback…</div>. - The main HTML is independent of the user — safe to cache at the CDN edge.
usephp.jsfinds all[data-usephp-defer-url]elements afterDOMContentLoadedandGETs each URL. The framework flips an internal flag, re-invokes the wrapper in endpoint mode (so it returns the real content instead of the placeholder), and responds with the configuredCache-Control.- The placeholder is replaced in place with the real component.
- Because each deferred endpoint has its own URL, each can carry its own cache policy —
public, s-maxage=60for a shared announcement bar,private, no-storefor a session-coupledUserHeader, etc.
Passing data from the parent
Any prop besides fallback is forwarded as a query parameter on the deferred
fetch:
This becomes GET /_defer/post-comments?post_id=123&sort=new. The wrapper's
inner closure receives them as $props['post_id']. Values must be scalar
(int/string/float/bool) — arrays, Elements, and Closures cannot cross
the URL boundary. All values arrive as strings on the server.
Client-side cache & forced reset
usephp.js puts a two-tier cache in front of every deferred fetch:
- L1 — in-memory
Map<URL, DocumentFragment>, per page lifetime. Always on; a full reload clears it. Identical to the previous behaviour. - L2 —
localStorage, surviving reloads and shared across tabs. Strictly opt-in, and the component decides —usephp.jsnever looks at the HTTPCache-Controlheader for this. A fragment is persisted only when the component setsDefer::$localCache = true; without it the fragment stays memory-only, so a session-coupled component (the default) can't leak one user's content to the next on a shared terminal. By default there is no time expiry — a persisted entry lives until aDEFER_CACHE_VERSIONbump orclearDeferCache()removes it. SetDefer::$localCacheTtl(seconds) to additionally bound it by age (see below). The endpoint'scacheControlis a separate concern (server/CDN caching) and is intentionally decoupled from this client decision.
Read order is L1 → L2 → network; L2 is consulted only for components that opted in. An L2 hit is promoted into L1. Both tiers are keyed by URL and bounded at 64 entries (L1 LRU, L2 oldest-first by insertion), so a list page that defers per-row fragments can't grow them without limit.
Make a deferred component client-cacheable by opting in — on the
#[Defer] attribute or the Defer value object:
This renders <div data-usephp-defer-url="…" data-usephp-defer-cache>;
usephp.js keys off that bare attribute's presence (and nothing else) to
decide persistence. localCache and cacheControl are independent: you
can ship cacheControl: 'private, no-store' for the endpoint while still
allowing the client cache, or vice versa. Because there's no expiry by
default, invalidate via DEFER_CACHE_VERSION (deploy) or
clearDeferCache() (runtime).
Optional time bound — Defer::$localCacheTtl
If a fragment should simply go stale after N seconds, set
localCacheTtl (seconds) alongside localCache: true:
This adds a separate data-usephp-defer-cache-ttl="60" attribute next
to the bare opt-in. Once the persisted entry is older than that, the next
read discards it and re-fetches from the network — the fallback shows
briefly, then the fresh fragment. It is a hard discard, not
stale-while-revalidate (no stale paint, no background swap). The bound
applies to the L2 localStorage tier only; L1 is per-page anyway.
Any localCacheTtl <= 0 (the 0 default included) means no time bound —
byte-identical markup and behaviour to a plain localCache: true. A
negative is normalised to 0 (read as "no bound", not an error), so only
a genuinely positive value ever takes effect. A separate attribute
(rather than a value on the bare opt-in) keeps that default, and every
non-opted-in placeholder, byte-identical to the pre-feature markup. A
positive TTL without localCache: true still throws (it would bound an
entry that's never written).
Forced reset is handled in JS:
Name matching is prefix-agnostic, so a custom setDeferPrefix('/api/_d')
still resolves. If localStorage is unavailable (Safari private mode, quota
exceeded, disabled) the L2 tier silently no-ops and L1 keeps serving the
page.
Explicit reload
By default a deferred fragment is fetched once: usephp.js replaces the
placeholder with the response and the wrapper is gone, so there is nothing
to re-target. Opt into Defer::$reloadable to keep a re-fetchable wrapper
in the DOM:
This renders the placeholder with data-usephp-defer-name="todo-list".
usephp.js then swaps content inside that wrapper instead of replacing it
away, so the region can be re-fetched later. Every reload busts both
cache tiers for that URL first, so it always reflects current server
state. Three ways to trigger it, all over one core API:
The attribute value is a space/comma-separated list of defer names (or
exact URLs); an empty value reloads every reloadable region. Reload is a
distinct concern from clearDeferCache() — the latter only invalidates,
it never re-fetches. Components that don't set reloadable are unchanged:
replaced away on resolve, byte-identical markup.
Requirements & limitations
- One component per mode. Use a base component for inline rendering and a wrapper carrying
Deferfor the deferred endpoint. Don't try to switch modes on the same component at the call site. - Defer names are URL-safe and unique. Pattern:
[A-Za-z0-9_-]+. Two wrappers attempting to register the same name fail at compile time. - Class-based defer targets are supported. Annotate the class with
#[Defer(name: '...')];register()auto-wires the endpoint. The class itself decides whatrender()produces in endpoint mode. - Params must be scalar. They travel through the URL query string, so
int/string/float/boolonly (bools are coerced to'1'/'0'). Arrays, Elements, Closures, and resources are rejected at render time. - Authorization is the component's responsibility. The name and params are visible in the URL — for endpoints that surface sensitive data, check session/permissions inside the component. (HMAC signing is no longer needed; the name is a public entry-point identifier.)
- Nested defer works. A deferred component's output may itself contain
<...Deferred />placeholders;usephp.jsrecursively hydrates them. - Two-tier defer cache (L1 in-memory + opt-in L2
localStorage). Component-decided persistence (Defer::$localCache, not the HTTPCache-Control; no time expiry by default, optionally bounded byDefer::$localCacheTtlseconds) and a JS forced-reset API — see Client-side cache & forced reset above. Components that don't opt in behave exactly like the previous in-memory-only cache. - Opt-in explicit reload (
Defer::$reloadable). Keeps a re-targetable wrapper so a deferred region can be re-fetched viawindow.usePHP.reloadDefer(), a form'sdata-usephp-reload-defer, or a click on any element — see Explicit reload above. Each reload busts both cache tiers for that URL first. Components that don't opt in are replaced away on resolve, byte-identical markup. - No-JS users see the fallback only. If JavaScript runs but the published
usephp.jsasset cannot be read,UsePHP::renderClientScript()includes a tiny inline fallback that still fetches deferred fragments once. The full asset is still required for partial form updates, defer caching, and explicit reload APIs. - Framework integration: call
UsePHP::handleDeferred()from your controller; it returns the rendered HTML forGET /_defer/...requests, ornullotherwise. MirrorshandleAction(). The prefix is configurable viasetDeferPrefix('/api/_d').
Generated HTML
Transforms to:
data-usephp-form- Form intercepted by JS- Works as a regular form submission without JS
CLI
Security
usePHP ships with several built-in defenses. The notes below cover the parts an application author still has to wire up correctly.
Snapshot HMAC secret (required for Snapshot storage)
SnapshotBehavior::Persistent|Session|Shared and components declared with #[Component(storage: 'snapshot')] round-trip state through the client, so the framework HMAC-signs every snapshot. You must configure a high-entropy secret before rendering a snapshot-using component or you'll get a LogicException at render time.
Generate the key once and load it from configuration:
The key must be stable across requests and across worker processes (PHP-FPM, multi-server). If different workers have different keys, snapshots produced by one worker will fail verification on the next. Rotating the key invalidates every outstanding snapshot.
Never commit the secret to git. The placeholders used in examples/ ('your-secret-key-here', 'phase-1-demo-secret') are deliberately weak — replace them before deploying anything.
CSRF protection
UsePHP::run() and UsePHP::handleAction() enforce two layers on every POST:
- Origin / Referer same-origin check (always). A request with neither header, or with an origin that doesn't match the current
Host, is rejected with403 Forbidden. - Session-bound synchronizer token (when a session is active).
Renderer::renderWithFormembeds a per-session token as<input type="hidden" name="_usephp_csrf" value="...">;doHandleActionvalidates it withhash_equals.
The token is exposed via UsePHP::getCsrfToken() if you render forms yourself.
If the surrounding framework (Laravel VerifyCsrfToken, Symfony's CSRF component, etc.) already enforces CSRF on POST handlers, opt out of usePHP's check so the two layers don't double-validate:
Behind a TLS-terminating proxy
When usePHP runs behind a reverse proxy that terminates TLS (nginx, an ALB, Cloudflare, etc.), $_SERVER['HTTPS'] is unset on the PHP-FPM side even though the browser sees https://. The expected origin would then be computed as http://... and every legitimate POST would fail the same-origin check with a 403.
You have two options:
-
Pass the scheme into
$_SERVERbefore usePHP runs, e.g. in nginx: -
Opt into proxy-header trust — usePHP will honor
X-Forwarded-Proto,X-Forwarded-Host, andX-Forwarded-Port:Only enable this when every request reaches PHP through a proxy that you control and that strips or overwrites these headers from the client side. Otherwise an attacker can spoof them and bypass the origin check.
Session cookie hardening
usePHP uses $_SESSION for the Session and Shared snapshot behaviors and for its CSRF token. The library does not set cookie flags for you — configure them in php.ini or session_start() options:
Call session_regenerate_id(true) after authentication to defeat session fixation.
Same-origin redirects
UsePHP::redirect($url) and SimpleRouter::createRedirectUrl() reject absolute URLs (https://...), protocol-relative URLs (//host/path), and scheme-prefixed paths (javascript:...). Pass only same-origin paths starting with /. If you need to redirect off-site, write the Location header yourself after running the target through your own allow-list.
URL-attribute XSS guard
href, src, action, formaction, srcdoc, data, poster, background, and xlink:href are URL-context attributes. The renderer drops any value whose scheme resolves to javascript:, vbscript:, or data: (after stripping leading whitespace and control characters, the way browsers parse URLs). Regular relative or absolute HTTP(S) URLs pass through unchanged.
PSX compilation cache
usephp compile writes generated PHP files into var/cache/psx/ and UsePHP::loadComponentManifest() requires them. The cache directory must be writable only by the build/deployment role, not by HTTP request handlers. Never call loadComponentManifest() with a request-derived path.
Reporting issues
If you find a security issue, please email the maintainer rather than opening a public issue.
Requirements
- PHP 8.5+
- Sessions enabled
Development
License
MIT