polidog / use-php
React Hooks-like PHP components with server-side state management
Installs: 19
Dependents: 1
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/polidog/use-php
Requires
- php: >=8.5
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.93
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
README
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 - Minimal JS (~40 lines) - Smooth UX with partial updates, graceful fallback without JS
- Pure PHP - No transpilation needed, PHP code runs directly on the server
- Configurable State Storage - Choose between session (persistent) or memory (per-request) storage per component
- Component-oriented - Reusable component classes
- Progressive Enhancement - Works even with JavaScript disabled
Installation
composer require polidog/use-php
# Copy JS file to public directory (required for partial updates)
./vendor/bin/usephp publish
Quick Start
1. Create a Component
<?php // components/Counter.php namespace App\Components; use Polidog\UsePhp\Component\BaseComponent; use Polidog\UsePhp\Component\Component; use Polidog\UsePhp\Html\H; use Polidog\UsePhp\Runtime\Element; #[Component] class Counter extends BaseComponent { public function render(): Element { [$count, $setCount] = $this->useState(0); return H::div( className: 'counter', children: [ H::span(children: "Count: {$count}"), H::button( onClick: fn() => $setCount($count + 1), children: '+' ), H::button( onClick: fn() => $setCount($count - 1), children: '-' ), ] ); } }
2. Create an Entry Point
<?php // public/index.php require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../components/Counter.php'; use App\Components\Counter; use Polidog\UsePhp\UsePHP; // Serve usephp.js (for partial updates) if ($_SERVER['REQUEST_URI'] === '/usephp.js') { header('Content-Type: application/javascript'); readfile(__DIR__ . '/usephp.js'); exit; } // Register component UsePHP::register(Counter::class); // Handle POST actions (for partial updates) $actionResult = UsePHP::handleAction(); if ($actionResult !== null) { echo $actionResult; exit; } // Render component $content = UsePHP::render(Counter::class); ?> <!DOCTYPE html> <html> <head> <title>Counter - usePHP</title> </head> <body> <?= $content ?> <script src="/usephp.js"></script> </body> </html>
3. Start the Server
php -S localhost:8000 public/index.php
Open http://localhost:8000 in your browser.
Architecture
With JavaScript (Partial Updates)
[Browser] [PHP Server]
| |
| GET / |
| -------------------------------->|
| | Counter::render() executes
| <html>Count: 0</html> | useState → saves to session
| <--------------------------------|
| |
| POST + X-UsePHP-Partial header |
| -------------------------------->|
| | State update
| <partial>Count: 1</partial> | Re-render component only
| <--------------------------------|
| (innerHTML partial update) |
Without JavaScript (Fallback)
[Browser] [PHP Server]
| |
| <form> POST (button click) |
| -------------------------------->|
| | State update
| 303 Redirect |
| <--------------------------------|
| |
| GET / |
| -------------------------------->|
| <html>Count: 1</html> | Full page re-render
| <--------------------------------|
API
Component Definition
use Polidog\UsePhp\Component\BaseComponent; use Polidog\UsePhp\Component\Component; #[Component] class MyComponent extends BaseComponent { public function render(): Element { // ... } }
The component name defaults to the FQCN (e.g., App\Components\MyComponent). You can override it explicitly:
#[Component(name: 'custom-name')] class MyComponent extends BaseComponent { /* ... */ }
useState
[$state, $setState] = $this->useState($initialValue); // Examples [$count, $setCount] = $this->useState(0); [$todos, $setTodos] = $this->useState([]); [$user, $setUser] = $this->useState(['name' => 'John']);
State Storage
By default, component state is stored in PHP sessions and persists across page navigations. You can configure this behavior per component using the storage parameter:
use Polidog\UsePhp\Component\BaseComponent; use Polidog\UsePhp\Component\Component; use Polidog\UsePhp\Storage\StorageType; // Session storage (default) - state persists across page navigations #[Component] class Counter extends BaseComponent { public function render(): Element { [$count, $setCount] = $this->useState(0); // $count persists when user navigates away and comes back // ... } } // Memory storage - state resets on each page load #[Component(storage: StorageType::Memory)] class SearchForm extends BaseComponent { public function render(): Element { [$query, $setQuery] = $this->useState(''); // $query resets to '' on page reload/navigation // ... } } // You can also use string values #[Component(storage: 'memory')] class Wizard extends BaseComponent { /* ... */ }
Storage Types:
| Type | Behavior | Use Case |
|---|---|---|
session (default) |
State persists across page navigations | Counters, shopping carts, user preferences |
memory |
State resets on each page load | Search forms, temporary UI state, wizards that should reset |
HTML Elements
use Polidog\UsePhp\Html\H; // Basic usage H::div( className: 'container', id: 'main', children: [ H::h1(children: 'Title'), H::button( onClick: fn() => $setCount($count + 1), children: 'Click' ), ] ); // Conditional rendering H::div(children: [ $isLoggedIn ? H::span(children: 'Welcome') : null, $count > 0 ? H::ul(children: $items) : H::p(children: 'No items'), ]); // All HTML elements are supported H::article(className: 'post', children: [...]); H::table(children: [H::tr(children: [H::td(children: 'Cell')])]); H::video(src: 'movie.mp4', controls: true);
Multiple Components + Routing
<?php // public/index.php use App\Components\{Counter, TodoList}; use Polidog\UsePhp\UsePHP; // Serve usephp.js if ($_SERVER['REQUEST_URI'] === '/usephp.js') { header('Content-Type: application/javascript'); readfile(__DIR__ . '/usephp.js'); exit; } // Register components UsePHP::register(Counter::class); UsePHP::register(TodoList::class); // Handle POST actions $actionResult = UsePHP::handleAction(); if ($actionResult !== null) { echo $actionResult; exit; } // Routing $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $component = match ($path) { '/', '/counter' => Counter::class, '/todo' => TodoList::class, default => Counter::class, }; // Render $content = UsePHP::render($component); ?> <!DOCTYPE html> <html> <head> <title>usePHP Example</title> </head> <body> <?= $content ?> <script src="/usephp.js"></script> </body> </html>
Generated HTML
H::button(onClick: fn() => $setCount($count + 1), children: '+')
Transforms to:
<form method="post" data-usephp-form style="display:inline;"> <input type="hidden" name="_usephp_component" value="counter#0" /> <input type="hidden" name="_usephp_action" value='{"type":"setState","payload":{"index":0,"value":1}}' /> <button type="submit">+</button> </form>
data-usephp-form- Form intercepted by JS- Works as a regular form submission without JS
CLI
./vendor/bin/usephp publish # Copy usephp.js to public/ ./vendor/bin/usephp help # Show help
Requirements
- PHP 8.2+
- Sessions enabled
Development
# Run tests ./vendor/bin/phpunit # Start example server php -S localhost:8000 examples/index.php
License
MIT