polidog / usephp-approuter
Next.js App Router style file-based routing for usePHP
Requires
- php: >=8.5
- polidog/use-php: ^0.1.0
- psr/container: ^2.0
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-09 22:07:29 UTC
README
Next.js App Router style file-based routing for PHP, built on usePHP.
Requirements
- PHP >= 8.5
- polidog/use-php ^0.1.0
Installation
composer require polidog/usephp-approuter
Quick Start
<?php // public/index.php require_once __DIR__ . '/../vendor/autoload.php'; use Polidog\UsephpApprouter\AppRouter; $app = AppRouter::create(__DIR__ . '/../src/app'); $app->run();
Directory Structure
app/
layout.php -> Root layout (wraps all pages)
page.psx -> /
error.php -> Error page (404 etc.)
about/
page.psx -> /about
counter/
page.psx -> /counter
todo/
page.psx -> /todo
form/
page.psx -> /form
blog/
[slug]/
page.psx -> /blog/:slug (dynamic route)
Page Types
Function Page (closure-based)
Pages return a closure that receives a PageContext. The closure returns a render function. Use page.psx to write the inner render in TSX-like markup.
<?php // app/about/page.psx declare(strict_types=1); use Polidog\UsePhp\Runtime\Element; use Polidog\UsephpApprouter\Component\PageContext; return function (PageContext $ctx) { $ctx->metadata(['title' => 'About']); return function (): Element { return ( <div className="container"> <h1>About</h1> <p>Written in PSX.</p> </div> ); }; };
Helpers stay encapsulated inside the outer closure:
return function (PageContext $ctx) { $ctx->metadata(['title' => 'About']); $renderCard = function (string $title): Element { return ( <div> <h3>{$title}</h3> </div> ); }; return function () use ($renderCard): Element { return ( <div> {$renderCard('Hello')} </div> ); }; };
Dynamic Routes
Directory names wrapped in brackets (e.g. [slug]) become URL parameters, accessible via $ctx->params:
// app/blog/[slug]/page.psx return function (PageContext $ctx) { $slug = $ctx->params['slug'] ?? ''; $ctx->metadata(['title' => ucwords($slug) . ' - Blog']); return function () use ($slug): Element { return ( <div>Blog post: {$slug}</div> ); }; };
useState Hook
use function Polidog\UsePhp\Runtime\useState; return function (PageContext $ctx) { return function (): Element { [$count, $setCount] = useState(0); return ( <div> <span>{(string) $count}</span> <button onClick={fn() => $setCount($count + 1)}>+</button> </div> ); }; };
Class Page
Class-based pages are also supported by extending PageComponent. Use page.psx to write the render method in TSX-like markup:
<?php // app/form/page.psx declare(strict_types=1); namespace App\Form; use Polidog\UsePhp\Runtime\Element; use Polidog\UsephpApprouter\Component\PageComponent; class FormPage extends PageComponent { public function render(): Element { $this->setMetadata(['title' => 'Form']); [$data, $setData] = $this->useState([]); $action = $this->action([$this, 'handleSubmit']); return ( <form action={$action}> <input type="text" name="name" /> <button type="submit">Send</button> </form> ); } protected function handleSubmit(array $formData): void { // handle form submission } }
Compiling .psx files
.psx files must be compiled. Output goes to var/cache/psx/ by default (sha1-named files plus manifest.php); the source tree only ever contains .psx.
# Production / CI: pre-compile once ./vendor/bin/usephp compile src/app ./vendor/bin/usephp compile src/app --check # CI guard # Dev loop: watch and recompile on save ./vendor/bin/usephp compile src/app --watch # Override the cache location ./vendor/bin/usephp compile src/app --cache=build/psx
Or let AppRouter compile on demand during development:
// Default cache: <appDir>/../var/cache/psx $app = AppRouter::create(__DIR__ . '/../src/app', autoCompilePsx: true); // Or pass an explicit cache directory (must match the CLI if you also // use `vendor/bin/usephp compile`): $app = AppRouter::create( __DIR__ . '/../src/app', autoCompilePsx: true, psxCacheDir: __DIR__ . '/../build/psx', );
page.psx and page.php cannot coexist in the same directory — the scanner errors if both are present.
Add the cache directory to .gitignore:
/var/cache/psx/
Layouts
Each directory can have a layout.php that wraps all pages beneath it. Layouts implement LayoutInterface.
Configuration
$app = AppRouter::create(__DIR__ . '/../src/app'); $app->setJsPath('/assets/app.js'); $app->addCssPath('/assets/style.css'); $app->setContainer($container); // PSR-11 container (optional) $app->run();
Running Tests
vendor/bin/phpunit
License
MIT