polidog/usephp-approuter

Next.js App Router style file-based routing for usePHP

Maintainers

Package info

github.com/polidog/usePHP-AppRouter

pkg:composer/polidog/usephp-approuter

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-05-09 22:07 UTC

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

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