ohffs / simple-reactphp-router
Simple attribute-based router wrapper for ReactPHP HTTP server
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/ohffs/simple-reactphp-router
Requires
- nikic/fast-route: ^1.3
- react/http: ^1.11
Requires (Dev)
- pestphp/pest: ^4.1
This package is auto-updated.
Last update: 2025-12-13 13:06:59 UTC
README
A minimal, attribute-based HTTP routing wrapper for ReactPHP. Inspired by FastAPI's decorator syntax, optimised for the common case of simple API endpoints.
Installation
composer require ohffs/simple-reactphp-router
Quick Start
<?php require __DIR__ . '/vendor/autoload.php'; use Ohffs\SimpleReactphpRouter\Attributes\Get; use Ohffs\SimpleReactphpRouter\Attributes\Post; use Ohffs\SimpleReactphpRouter\Request; use Ohffs\SimpleReactphpRouter\Response; use Ohffs\SimpleReactphpRouter\Server; use Psr\Http\Message\ResponseInterface; class ApiController { #[Get('/health')] public function health(Request $request): ResponseInterface { return Response::json(['status' => 'ok']); } #[Get('/hello')] public function hello(Request $request): ResponseInterface { $name = $request->query('name', 'World'); return Response::json(['message' => "Hello, {$name}!"]); } #[Post('/users')] public function createUser(Request $request): ResponseInterface { $data = $request->json(); if ($data === null) { return Response::badRequest('Invalid JSON'); } // Create user... return Response::json(['created' => true], 201); } } $server = new Server(new ApiController()); $server->run('127.0.0.1', 8000);
Run your server:
php server.php
Test it:
curl http://localhost:8000/health # {"status":"ok"} curl http://localhost:8000/hello?name=Jenny # {"message":"Hello, Jenny!"} curl -X POST http://localhost:8000/users \ -H "Content-Type: application/json" \ -d '{"name":"John"}' # {"created":true}
Features
- Simple - Minimal API, no magic
- Attribute-based routing - Use PHP 8 attributes to define routes
- Route parameters - Support for
{id}style parameters with optional regex constraints - Lightweight - Just ReactPHP and FastRoute, nothing else
- Familiar - If you've used FastAPI or similar frameworks, you'll feel at home
- Optional authentication - Simple bearer token auth built-in
Route Attributes
use Ohffs\SimpleReactphpRouter\Attributes\Get; use Ohffs\SimpleReactphpRouter\Attributes\Post; use Ohffs\SimpleReactphpRouter\Attributes\Put; use Ohffs\SimpleReactphpRouter\Attributes\Patch; use Ohffs\SimpleReactphpRouter\Attributes\Delete; class MyController { #[Get('/items')] public function list(Request $request): ResponseInterface { } #[Post('/items')] public function create(Request $request): ResponseInterface { } #[Put('/items/{id}')] public function replace(Request $request): ResponseInterface { } #[Patch('/items/{id}')] public function update(Request $request): ResponseInterface { } #[Delete('/items/{id}')] public function delete(Request $request): ResponseInterface { } }
Route Parameters
Routes can include dynamic parameters using {name} syntax:
class UserController { #[Get('/users/{id}')] public function show(Request $request): ResponseInterface { $id = $request->param('id'); return Response::json(['user_id' => $id]); } #[Get('/users/{userId}/posts/{postId}')] public function showPost(Request $request): ResponseInterface { $userId = $request->param('userId'); $postId = $request->param('postId'); return Response::json(['user' => $userId, 'post' => $postId]); } }
Regex Constraints
You can add regex constraints to parameters using {name:pattern} syntax:
class ItemController { // Only matches numeric IDs #[Get('/items/{id:\d+}')] public function show(Request $request): ResponseInterface { $id = $request->param('id'); return Response::json(['item_id' => $id]); } // Only matches slugs (lowercase letters and hyphens) #[Get('/posts/{slug:[a-z\-]+}')] public function showBySlug(Request $request): ResponseInterface { $slug = $request->param('slug'); return Response::json(['slug' => $slug]); } }
Requests that don't match the constraint will return a 404 response.
Request Helper
The Request class wraps PSR-7's ServerRequestInterface with convenience methods:
// Route parameters: /users/{id} matched against /users/123 $request->param('id'); // '123' $request->param('missing', 'default'); // 'default' $request->params(); // ['id' => '123'] // Query parameters: /path?foo=bar&baz=qux $request->query('foo'); // 'bar' $request->query('missing', 'default'); // 'default' $request->queryAll(); // ['foo' => 'bar', 'baz' => 'qux'] // JSON body (decoded) $request->json(); // ['key' => 'value'] or null if invalid // Raw body $request->body(); // '{"key":"value"}' // Headers (case-insensitive) $request->header('Content-Type'); // 'application/json' $request->header('X-Missing', 'default'); // 'default' // Bearer token from Authorization header $request->bearerToken(); // 'your-token' or null // HTTP method and path $request->method(); // 'POST' $request->path(); // '/users' // Access underlying PSR-7 request $request->psrRequest();
Response Helper
The Response class provides static factory methods for common responses:
// JSON responses Response::json(['status' => 'ok']); // 200 Response::json(['created' => true], 201); // 201 // No content Response::noContent(); // 204 // Plain text Response::text('Hello, World!'); // 200 text/plain Response::text('Accepted', 202); // 202 // HTML Response::html('<h1>Hello</h1>'); // 200 text/html // Generic response with custom headers Response::make('body', 200, ['X-Custom' => 'header']); // Error responses Response::notFound(); // 404 {"error":"Not found"} Response::notFound('User not found'); // 404 {"error":"User not found"} Response::badRequest(); // 400 {"error":"Bad request"} Response::badRequest('Invalid email'); // 400 {"error":"Invalid email"} Response::error(); // 500 {"error":"Internal server error"} Response::error('Service unavailable', 503); // 503
Multiple Controllers
You can register multiple controller classes:
$server = new Server([ new UserController(), new ProductController(), new HealthController(), ]); $server->run();
Authentication
You can protect your API with a bearer token:
$server = new Server(new ApiController(), bearerToken: 'your-secret-token'); $server->run();
When a bearer token is set, all requests must include a valid Authorization header:
curl http://localhost:8000/health \
-H "Authorization: Bearer your-secret-token"
Requests without a valid token receive a 401 Unauthorized response:
{"error": "Unauthorized"}
If no bearer token is configured, all requests are allowed (the default behaviour).
Error Handling
- 404 Not Found - Returned when no route matches the path
- 405 Method Not Allowed - Returned when the path exists but the HTTP method doesn't match
- 500 Internal Server Error - Returned when an exception is thrown in a handler (exception is logged to stderr)
Configuration
The run() method accepts host and port:
$server->run(); // 127.0.0.1:8000 (default) $server->run('0.0.0.0', 8080); // Listen on all interfaces, port 8080
Requirements
- PHP 8.1+
- ReactPHP HTTP ^1.11
- FastRoute ^1.3
Development
Clone the repository:
git clone https://github.com/ohnotnow/simple-reactphp-router.git
cd simple-reactphp-router
composer install
Run tests:
./vendor/bin/pest
Run the example server:
php examples/simple-api.php
Design Decisions
- Duplicate routes throw exceptions - Fail fast rather than silently overwriting
- Trailing slashes are normalised -
/usersand/users/are treated the same - Paths are case-sensitive -
/Usersand/usersare different routes - Handlers must return ResponseInterface - Explicit over implicit
License
MIT License. See LICENSE for details.