Rammewerk Router takes a fresh approach to PHP routing. Built for PHP 8.4, it’s lightweight, flexible, and built around class-based routing.
With features like type-safe parameters, dependency injection support, and middleware, it gives you powerful tools without unnecessary complexity. Minimal code, minimal configuration - just check out the code yourself.
- Class-Based Routing: Organize routes cleanly and intuitively.
- Type-Safe Parameters: Let the router handle types and dependencies for you.
- Middleware Support: Add functionality like authentication or logging.
- Minimal & Focused: Designed to do one thing well without unnecessary complexity.
- Project Goals
- Getting started
- Basic Routing
- Class-based Routing
- Dependency Injection
- Middleware
- Dispatching and Response
- Method-Specific Request Handling
- Powerful Parameter Handling
- Performance and Speed
- Closure-Based Routes
- PSR-7 & PSR-15 Support
These goals reflect what Rammewerk strives to achieve across all its components:
- Lightweight & Fast: Small, focused and compact library with zero bloat, built for speed.
- Plug-and-Play: Works out of the box with minimal configuration.
- Minimal & Understandable: Simple code that’s easy to read, adapt, and even rewrite for your own projects.
- Flexible by Design: Add your own implementations and customize it to suit your needs.
- Open for Collaboration: Fork it, explore it, and contribute back with pull requests!
By using Rammewerk, you get a minimal yet powerful foundation that’s easy to build on and improve. Let’s dive in! 🔧✨
Install Rammewerk Router via composer:
composer require rammewerk/router
- Requires PHP 8.4+.
- Server must route all requests to a single PHP file (e.g., index.php) using Caddy, Nginx, or Apache.
- Use a Dependency Injection container like Rammewerk Container for managing class instances.
use Rammewerk\Router\Router;
// Create Router with a closure to handle construction of class instances
$router = new Router( static fn( string $class) => $container->get($class) );
// Define routes
// ...
// Go!
$router->dispatch();
While the Rammewerk Router is designed for class-based routing, it also supports closures.
Here’s a simple example:
$router->add('/hello', function() {
return 'Hello World!';
});
This matches /hello
, triggers the closure, and returns “Hello World!”
Class-based routing is the core feature of Rammewerk Router. It maps paths and their nested routes directly to a class, making it both powerful and flexible.
Here’s how it works:
$router->add('/profile', ProfileRoute::class);
With this setup, the ProfileRoute
class will handle all requests to /profile
(and its sub-paths, unless overridden
by other routes).
Here’s an example of a simple class for the /profile
path:
namespace Routes;
class ProfileRoute {
public function index(): string {
return 'You visited /profile';
}
}
The index()
method is the default handler for the base path of a class-based route. In this case, accessing /profile
triggers the index()
method.
If needed, you can set a change default method during initialization or on a per-route basis:
// Global override for all class-based routes
$router = new Router( default_method: 'show' );
// Override for a single route
$roter->add(...)->defaultMethod('show');
In this case, a show()
method will handle base path requests. If default method isn’t defined in class, accessing
/profile
will throw an InvalidRoute
exception.
You can also define class routes with a single __invoke()
method. This will be called if no other method matches or is
defined. The __invoke()
method is best used when the class doesn’t have additional route methods, keeping it simple
and focused.
To handle a path like /profile/settings/notifications
, simply add a method to your class matching the subpath
structure:
class ProfileRoute {
// Previous methods
public function settings_notifications(): string {
return 'You visited /profile/settings/notifications';
}
}
Each segment after the base path (/profile
) maps to a method, with subpath segments replaced by underscores (_
).
You can define dynamic subpaths by adding parameters to your method:
class ProfileRoute {
public function edit( int $id ): string {
return "You visited /profile/edit/$id";
}
}
Accessing /profile/edit/123
triggers the edit()
method with the parameter $id
= 123
.
Additionally, you can use wildcard parameters (*
) to map subpaths to parameters. For example, handling
/profile/123/edit
can be done like this:
$router->add('/profile/*/edit', ProfileEditRoute::class);
Wildcard parameters are mapped in order, alongside subpaths. For example, /profile/123/edit/notification
results in
parameters 123
and notification
.
Notes:
- Parameter names don’t matter, but their order does.
- If a parameter isn’t in the path, it must be optional or nullable to match.
- Type hints are supported, and path segments are converted to match (
int
,float
,bool
,string
). Undefined or mixed defaults to string. - Parameters that can't convert to defined type are rejected, and route won't match.
- Paths must match exactly. For example,
/profile/edit/123
won’t match/profile/edit/123/something
. - Use a variadic parameter (
...$args
) to allow extra subpaths to match.
You can use classes as parameters, and the router will resolve them via the dependency handler set during initialization:
class ProfileRoute {
public function edit( Profile $profile, Template $template, int $id ): Response {
$user = $profile->find($id);
return $template->render('profile/edit', [$user]);
}
}
The order of class dependencies doesn’t matter, but parameters extracted from the path must be in the correct order.
You can bind a route to a specific method in its class by defining the method in the route definition:
$router->add(...)->method('edit');
This ensures the edit()
method of the ProfileRoute
class is always called when /profile/settings
is accessed.
Less configuration? Checked! 🎉 Class-based routing keeps things straightforward. Adding new routes is as easy as defining methods in your handler classes. With type safety, support for required and optional parameters, and the flexibility of wildcards, you can build routes that adapt to your needs without unnecessary complexity.
To manage dependencies for class-based and closure-based routes, as well as middleware, the router requires a dependency resolver. You must set this up during initialization by passing a closure to the constructor. This closure receives a class name and returns an instance of that class.
For a simple and efficient solution, check out Rammewerk Container.
$router = new Router( static fn( string $class_string ) => $container->create($class_string) );
This approach keeps your routes clean and ensures seamless dependency handling.
Middleware adds functionality to your routes, like authentication, logging, or caching, without cluttering your route classes. It acts as a layer that processes requests before they reach your route handler or modifies responses afterward.
Here’s how to add middleware to a route:
$router->add('/', HomeRoutes::class)->middleware([
AuthMiddleware::class,
LoggerMiddleware::class,
]);
Middleware runs in the order it’s defined. Each middleware must have a handle method that processes the request and calls the next closure to continue:
class AuthMiddleware {
public function handle(Request $request, \Closure $next) {
// Do auth stuff
return $next($request);
}
}
Note: Even though the request object is optional (object|null
), your middleware must define it as the first
parameter of the handle()
method. The handle()
method must always receive two arguments: the given request
object (or
null) and the next closure to call.
Rammewerk Router also supports PSR-15 MiddlewareInterface. See PSR-15 Support for more information.
The router passes a request object to each middleware and the route handler. The request type is flexible; you can pass any object during dispatch:
$router->dispatch('/profile', new ServerRequest());
While optional, it’s good practice to type-hint the request in your handle method and ensure it matches the request class passed during dispatch.
Rammewerk Router also supports PSR-7 ServerRequestInterface. See PSR-7 Support for more
Use the group()
method to apply middleware to multiple routes at once. This keeps your code clean and avoids
repetitive middleware declarations.
Here’s an example:
$router->group(function(Router $r) {
$r->add('/products', ProductRoutes::class);
$r->add('/users', fn() => 'Users listing');
})->middleware([
AuthMiddleware::class,
LoggerMiddleware::class
]);
In this example, both /products
and /users
routes share the same middleware (AuthMiddleware
and
LoggerMiddleware
), applied in the defined order.
If you define middleware on a route inside the group, it will run before the group’s middleware:
$r->group(function (Router $r) {
$r->add('/products', ProductRoutes::class)->middleware([AuthMiddleware::class]);
// More routes
})->middleware([LoggerMiddleware::class]);
Here, AuthMiddleware
runs first for /products
, followed by LoggerMiddleware
from the group. This lets you control
the middleware order for each route.
Little configuration? Checked! 🎉 Middleware in the router is flexible and straightforward, letting you add layers to your routes without overcomplicating things. You’re free to implement middleware however you like — no restrictions on which request class to use or how to handle it.
Dispatching routes is simple: just call the dispatch method on your router instance.
try {
$response = $router->dispatch( path: '/', serverRequest: $request );
// Handle response
} catch (InvalidRoute $e) {
// Handle 404 errors or log unmatched paths
} catch (Throwable $e) {
// Handle other application errors, or let it bubble up
}
- The path parameter is matched against your routes. If no match is found, an InvalidRoute exception is thrown, letting you handle 404s or similar responses.
- The request parameter is optional and passes a request object to middleware and route handlers for processing.
- The response is returned from the dispatch method, which can be any type. It’s up to you to handle it in your application.
This router doesn’t predefine request types like GET
or POST
. It simply passes any request to the route. If you want
to implement a get()
/post()
/delete()
style structure, you can achieve it by adding a wrapper or using middleware
to handle specific request methods. This gives you full flexibility to define request handling as you see fit.
This router shines with its robust and flexible parameter system:
- Type-safe & Intuitive: Supports type hints, union types, and automatic conversion for seamless parameter handling.
- Dependency-Friendly: Reflects parameters in methods and closures, allowing seamless integration with your own DI container for maximum flexibility and adaptability.
- Wildcard Simplicity: Use * to capture dynamic segments - no regex needed, and parameter types ensure effortless refactoring and clarity. -** No Dictated Names**: Parameters don’t rely on specific names, giving you freedom and flexibility.
This level of type safety, combined with flexible wildcards, is rare in other routers. It’s designed to make routing both powerful and effortless! 🚀
Rammewerk Router is designed to stay lean and move fast.
- It uses simple arrays to store routes and quickly narrows down matching paths by comparing only relevant segments.
- Regex patterns are sorted by length so more specific routes are tested first.
- Reflection is only performed once a route is confirmed, so there’s no overhead for routes that don’t match
- With minimal internal complexity, no bulky dependencies, and a single-file core, the router focuses on doing one job well without slowing you down in production.
Integrating Rammewerk Container can boost speed even more, thanks to its lazy-loading approach. It’s also one of the fastest DI containers out there, as shown in benchmarks.
While class-based routing is the core feature of Rammewerk Router, closure-based routes can be useful for simple, standalone handlers or quick prototypes.
These routes still benefit from the same powerful parameter handling as class-based routes, including dependency injection of classes and type-hinted subpath parameters. Middleware can also be applied seamlessly to closure-based routes, ensuring consistent behavior across your application.
Here’s an example:
// Define a closure-based route with parameter handling and middleware
$router->add('/greet', function (string $name, Logger $logger): string {
$logger->info("Greeting user: $name");
return "Hello, $name!";
})->middleware([
AuthMiddleware::class,
]);
// Dispatch the router
$router->dispatch('/greet/John', new Request());
Key Points:
- Parameter Handling: Subpath parameters like {name} are automatically resolved and type-checked.
- Dependency Injection: Classes like Logger are injected via the resolver.
- Middleware: Layers such as AuthMiddleware can be applied, ensuring functionality like authentication or logging is handled consistently.
Closure-based routes provide a lightweight yet flexible alternative when you don’t need a dedicated class handler.
The Rammewerk Router includes an extended class, PsrRouter
, designed specifically for applications requiring
PSR-7 (HTTP Message Interface) and PSR-15 (Middleware and Request Handlers) compliance. Use PsrRouter as a
drop-in replacement for the default Router when working with PSR-compliant middleware and request handlers.
- PSR-7: Pass compliant ServerRequestInterface objects to handlers and middleware.
- PSR-15: Add reusable, standards-based MiddlewareInterface layers.
- Pipeline: Middleware is executed sequentially, ensuring proper request and response processing.
Here's an example of PSR-7 & PSR-15 Usage:
use Rammewerk\Router\PsrRouter;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
$router = new PsrRouter(static fn(string $class) => $container->get($class));
// Add PSR middleware and routes
// HomeRoute handle method must return a PSR-7 ResponseInterface
$router->add('/home', HomeRoute::class)->middleware([
AuthMiddleware::class, // Implements PSR-15 MiddlewareInterface
]);
// $serverRequest is a PSR-7 ServerRequestInterface
$response = $router->dispatch('/', $serverRequest);
header('Content-Type: ' . $response->getHeaderLine('Content-Type'));
echo $response->getBody();
The PsrRouter not only provides PSR-7 and PSR-15 support but also serves as an example of how to extend the Rammewerk Router to implement custom solutions tailored to specific application needs. This showcases the flexibility of the Rammewerk Router’s architecture, enabling developers to adapt it to various standards or unique requirements.
NOTE: If your project requires
getAttribute()
or similar functionality to handle parameters directly, the Rammewerk Router might not be the ideal solution for your needs. This router is designed for flexibility and handles parameters differently, with logic tailored to its specific architecture. If you’re looking for a router that supports named parameters or simpler routing logic, you may want to consider alternatives that are more closely aligned with your project’s requirements.