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-02-23 02:58 UTC

This package is auto-updated.

Last update: 2026-03-01 08:45:47 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.php            -> /
  error.php           -> Error page (404 etc.)
  about/
    page.php          -> /about
  counter/
    page.php          -> /counter
  todo/
    page.php          -> /todo
  form/
    page.php          -> /form
  blog/
    [slug]/
      page.php        -> /blog/:slug (dynamic route)

Page Types

Function Page (closure-based)

Pages return a closure that receives a PageContext. The closure returns a render function.

<?php
declare(strict_types=1);

use Polidog\UsePhp\Html\H;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsephpApprouter\Component\PageContext;

return function (PageContext $ctx) {
    $ctx->metadata(['title' => 'About']);

    return function (): Element {
        return H::div(children: 'Hello from About page');
    };
};

Helpers stay encapsulated inside the outer closure:

return function (PageContext $ctx) {
    $ctx->metadata(['title' => 'About']);

    $renderCard = function (string $title): Element {
        return H::div(children: H::h3(children: $title));
    };

    return function () use ($renderCard): Element {
        return H::div(children: [$renderCard('Hello')]);
    };
};

Dynamic Routes

Directory names wrapped in brackets (e.g. [slug]) become URL parameters, accessible via $ctx->params:

// app/blog/[slug]/page.php
return function (PageContext $ctx) {
    $slug = $ctx->params['slug'] ?? '';
    $ctx->metadata(['title' => ucwords($slug) . ' - Blog']);

    return function () use ($slug): Element {
        return H::div(children: "Blog post: {$slug}");
    };
};

useState Hook

use function Polidog\UsePhp\Runtime\useState;

return function (PageContext $ctx) {
    return function (): Element {
        [$count, $setCount] = useState(0);

        return H::div(children: [
            H::span(children: (string) $count),
            H::button(
                onClick: fn() => $setCount($count + 1),
                children: '+',
            ),
        ]);
    };
};

Class Page

Class-based pages are also supported by extending PageComponent:

<?php
declare(strict_types=1);

namespace App\Form;

use Polidog\UsePhp\Html\H;
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 H::form(action: $action, children: [
            H::input(type: 'text', name: 'name'),
            H::button(type: 'submit', children: 'Send'),
        ]);
    }

    protected function handleSubmit(array $formData): void
    {
        // handle form submission
    }
}

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