polidog/usephp-router

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

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/polidog/usephp-router

v0.0.4 2026-01-27 06:46 UTC

This package is auto-updated.

Last update: 2026-01-27 15:40:41 UTC


README

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

Installation

composer require polidog/usephp-router

Quick Start

Directory Structure

app/
├── layout.php              # Root layout (wraps all pages)
├── page.php                # Home page (/)
├── about/
│   └── page.php            # About page (/about)
├── blog/
│   ├── layout.php          # Blog layout (wraps blog pages)
│   ├── page.php            # Blog index (/blog)
│   └── [slug]/
│       └── page.php        # Blog post (/blog/:slug)
└── error.php               # Error page (404)

Entry Point

<?php
// public/index.php
require 'vendor/autoload.php';

use UsePhp\Router\AppRouter;

AppRouter::create(__DIR__ . '/../app')
    ->setJsPath('/usephp.js')
    ->run();

Tailwind CSS

To use Tailwind, include the compiled CSS and disable the default inline styles:

<?php
// public/index.php
require 'vendor/autoload.php';

use UsePhp\Router\AppRouter;

AppRouter::create(__DIR__ . '/../app')
    ->addCssPath('/assets/app.css')
    ->disableDefaultStyles()
    ->setJsPath('/usephp.js')
    ->run();

Custom Document (HTML Wrapper)

If you want to control the base HTML structure (e.g., OG tags), inject a document renderer:

<?php
// public/index.php
require 'vendor/autoload.php';

use UsePhp\Router\AppRouter;
use UsePhp\Router\Document\HtmlDocument;

$document = HtmlDocument::create()
    ->setTitle('My App')
    ->addHeadHtml('<meta property="og:title" content="My App">');

AppRouter::create(__DIR__ . '/../app')
    ->setDocument($document)
    ->setJsPath('/usephp.js')
    ->run();

Build example (use your project's public directory as the output):

npm init -y
npm install -D tailwindcss
npx tailwindcss init

# assets/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;

npx tailwindcss -i ./assets/tailwind.css -o ./public/assets/app.css --minify

Creating Pages

Pages extend PageComponent and must implement render():

<?php
// app/page.php
use UsePhp\Router\Component\PageComponent;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsePhp\Html\H;

class HomePage extends PageComponent
{
    public function render(): Element
    {
        return H::div(children: [
            H::h1(children: 'Welcome'),
            H::p(children: 'This is the home page.'),
        ]);
    }
}

Metadata

Pages can set metadata via setMetadata() and it becomes <title> and <meta> tags:

<?php
// app/about/page.php
use UsePhp\Router\Component\PageComponent;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsePhp\Html\H;

class AboutPage extends PageComponent
{
    public function render(): Element
    {
        $this->setMetadata([
            'title' => 'About',
            'description' => 'About our project',
            'og:title' => 'About',
            'og:image' => 'https://example.com/og.png',
        ]);

        return H::div(children: [
            H::h1(children: 'About'),
        ]);
    }
}

Dynamic Routes

Use [param] syntax in directory names for dynamic segments:

<?php
// app/blog/[slug]/page.php
use UsePhp\Router\Component\PageComponent;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsePhp\Html\H;

class BlogPostPage extends PageComponent
{
    public function render(): Element
    {
        $slug = $this->getParam('slug');

        return H::article(children: [
            H::h1(children: "Blog Post: {$slug}"),
        ]);
    }
}

Form Actions (with CSRF)

Use action([$this, 'method']) in PageComponent to bind a POST handler. The router injects hidden fields automatically (including CSRF token).

<?php
// app/contact/page.php
use UsePhp\Router\Component\PageComponent;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsePhp\Html\H;

class ContactPage extends PageComponent
{
    public function render(): Element
    {
        return H::form(
            method: 'post',
            action: $this->action([$this, 'handlePost'], ['source' => 'contact']),
            children: [
                H::input(type: 'text', name: 'name', required: true),
                H::button(type: 'submit', children: 'Send'),
            ]
        );
    }

    /**
     * @param array<string, mixed> $formData
     */
    protected function handlePost(array $formData, string $source): void
    {
        // handle $formData here
    }
}

Layouts

Layouts extend LayoutComponent and wrap child content:

<?php
// app/layout.php
use UsePhp\Router\Layout\LayoutComponent;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsePhp\Html\H;

class RootLayout extends LayoutComponent
{
    public function render(): Element
    {
        return H::div(children: [
            H::nav(children: [
                H::a(href: '/', children: 'Home'),
                H::a(href: '/blog', children: 'Blog'),
            ]),
            H::main(children: [$this->getChildren()]),
        ]);
    }
}

Layouts are automatically nested from root to the page's directory.

Error Pages

Create app/error.php for custom 404 handling:

<?php
// app/error.php
use UsePhp\Router\Component\ErrorPageComponent;
use Polidog\UsePhp\Runtime\Element;
use Polidog\UsePhp\Html\H;

class ErrorPage extends ErrorPageComponent
{
    public function render(): Element
    {
        return H::div(children: [
            H::h1(children: (string) $this->getStatusCode()),
            H::p(children: $this->getMessage()),
        ]);
    }
}

Route Matching Priority

  1. Static routes (more segments = higher priority)
  2. Dynamic routes (more static segments = higher priority)

For example, given routes /blog/featured and /blog/[slug]:

  • /blog/featured matches the static route
  • /blog/anything-else matches the dynamic route

Running the Example

php -S localhost:8080 -t examples/public

Then visit:

Testing

./vendor/bin/phpunit

License

MIT