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
Requires
- php: >=8.5
- polidog/use-php: 0.0.2
- ray/di: ^2.0
Requires (Dev)
- phpunit/phpunit: ^11.0
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
- Static routes (more segments = higher priority)
- Dynamic routes (more static segments = higher priority)
For example, given routes /blog/featured and /blog/[slug]:
/blog/featuredmatches the static route/blog/anything-elsematches the dynamic route
Running the Example
php -S localhost:8080 -t examples/public
Then visit:
- http://localhost:8080/ - Home page
- http://localhost:8080/about - About page
- http://localhost:8080/blog - Blog index
- http://localhost:8080/blog/hello-world - Blog post
Testing
./vendor/bin/phpunit
License
MIT