Skip to content

Routing

Precious Lyson edited this page Nov 11, 2026 · 2 revisions

Table of Contents

  1. Introduction
  2. Installation
  3. Quick Start (Beginner)
  4. Basic Routing
  5. Route Parameters
  6. Named Routes
  7. Route Groups
  8. Middleware
  9. Controllers
  10. Attribute-Based Routing
  11. Route Model Binding
  12. Rate Limiting
  13. CORS Support
  14. AJAX & Traditional Routes
  15. Resource Routes
  16. Error Handling
  17. Route Caching
  18. Advanced Topics
  19. API Reference

Introduction

The Machinjiri Routing System is a powerful, flexible routing library for PHP applications. It provides:

  • Fluent API for defining routes
  • Attribute-based routing for controllers
  • Middleware pipeline for request filtering
  • Rate limiting to protect your endpoints
  • CORS support for cross-origin requests
  • Route model binding for automatic model resolution
  • Route caching for production performance
  • Named routes and URL generation
  • Resource routing for CRUD operations
  • AJAX detection for API vs web requests

Installation

The Routing system is already bundled in the framework. no need to Install separatrly.

Basic Setup

Create an web.php in the routes/ folder:

<?php

use Mlangeni\Machinjiri\Core\Routing\Router;

// Define routes...
Router::get('/', function () {
    return 'Hello, World!';
});

// Dispatch the request
Router::dispatch();

Quick Start (Beginner)

1. Define Your First Route

use Mlangeni\Machinjiri\Core\Routing\Router;

// Simple GET route
Router::get('/', function () {
    return 'Welcome to my website!';
});

// Route with a view
Router::get('/about', function () {
    return '<h1>About Us</h1><p>We build great things.</p>';
});

2. Handle Different HTTP Methods

Router::get('/users', function () {
    return 'List of users';
});

Router::post('/users', function () {
    return 'Create a new user';
});

Router::put('/users/{id}', function ($id) {
    return "Update user {$id}";
});

Router::delete('/users/{id}', function ($id) {
    return "Delete user {$id}";
});

3. Access Route Parameters

Router::get('/hello/{name}', function ($name) {
    return "Hello, {$name}!";
});

// Visit /hello/John → Returns "Hello, John!"

4. Run Your Application

Router::dispatch();

Basic Routing

Available HTTP Method Shortcuts

Method Description
Router::get() GET requests
Router::post() POST requests
Router::put() PUT requests
Router::delete() DELETE requests
Router::any() All common methods
Router::ajax() AJAX-only routes
Router::traditional() Non-AJAX routes

Example: Multiple Methods

// Respond to multiple methods
Router::any('/api/data', function () {
    return json_encode(['status' => 'ok']);
});

// AJAX only (returns 404 for regular browser requests)
Router::ajax('/api/notifications', function () {
    return json_encode(['notifications' => []]);
});

// Traditional only (returns 404 for AJAX requests)
Router::traditional('/contact', function () {
    return '<form>...</form>';
});

Route with Name

Router::get('/users/{id}', function ($id) {
    return "User profile: {$id}";
}, 'user.profile');

Route Parameters

Basic Parameters

Router::get('/posts/{slug}', function ($slug) {
    return "Post: {$slug}";
});

Multiple Parameters

Router::get('/users/{userId}/posts/{postId}', function ($userId, $postId) {
    return "User {$userId}, Post {$postId}";
});

Parameter Constraints

Use where() to restrict parameter values:

Router::get('/user/{id}', function ($id) {
    return "User ID: {$id}";
})->where('id', '[0-9]+');

// Or multiple constraints
Router::get('/post/{year}/{month}', function ($year, $month) {
    return "Posts from {$year}-{$month}";
})->where([
    'year' => '[0-9]{4}',
    'month' => '[0-9]{2}'
]);

Optional Parameters

Router::get('/archive/{year}/{month?}', function ($year, $month = null) {
    if ($month) {
        return "Archive: {$year}-{$month}";
    }
    return "Archive: {$year}";
});

Named Routes

Named routes allow you to generate URLs dynamically:

// Define a named route
Router::get('/users/{id}/profile', function ($id) {
    return "Profile for user {$id}";
}, 'user.profile');

// Generate URL
$url = Router::route('user.profile', ['id' => 42]);
// Returns: /users/42/profile

// Generate absolute URL
$absoluteUrl = Router::absoluteRoute('user.profile', ['id' => 42]);
// Returns: http://example.com/users/42/profile

// Get base URL
$baseUrl = Router::baseUrl();

Route Groups

Group routes to share attributes like prefixes and middleware:

Basic Group

Router::group(['prefix' => '/admin'], function () {
    Router::get('/dashboard', function () {
        return 'Admin Dashboard';
    });
    // URL: /admin/dashboard

    Router::get('/users', function () {
        return 'Manage Users';
    });
    // URL: /admin/users
});

Group with Middleware

Router::group(['prefix' => '/api', 'middleware' => ['Auth', 'Throttle']], function () {
    Router::get('/users', function () {
        return json_encode(['users' => []]);
    });
});

Nested Groups

Router::group(['prefix' => '/admin'], function () {
    Router::group(['prefix' => '/users'], function () {
        Router::get('/', function () {
            return 'User list';
        });
        // URL: /admin/users

        Router::get('/{id}', function ($id) {
            return "User {$id}";
        });
        // URL: /admin/users/{id}
    });
});

Middleware

Middleware filters requests before they reach your route handler.

Defining Middleware

Create a middleware class in App\Middleware:

<?php

namespace App\Middleware;

use Mlangeni\Machinjiri\Core\Http\HttpRequest;
use Mlangeni\Machinjiri\Core\Http\HttpResponse;

class Auth
{
    public function handle(HttpRequest $request, HttpResponse $response, callable $next, array $params)
    {
        if (!$request->getSession('user_id')) {
            $response->setStatusCode(401);
            $response->setBody('Unauthorized');
            $response->send();
            return;
        }

        return $next($params);
    }
}

Applying Middleware to Routes

Router::get('/dashboard', function () {
    return 'Dashboard';
}, null, ['middleware' => ['Auth']]);

Middleware with Groups

Router::group(['middleware' => ['Auth', 'LogRequests']], function () {
    Router::get('/profile', function () {
        return 'User Profile';
    });
});

Middleware Execution Order

Middleware executes in the order defined:

// LogRequests runs first, then Auth
Router::group(['middleware' => ['LogRequests', 'Auth']], function () {
    // Routes here
});

Controllers

Using Controller Strings

Router::get('/users', 'UserController@index');
Router::post('/users', 'UserController@store');
Router::get('/users/{id}', 'UserController@show');
Router::put('/users/{id}', 'UserController@update');
Router::delete('/users/{id}', 'UserController@destroy');

Using Fully-Qualified Class Names

Router::get('/users', 'App\\Controllers\\UserController@index');

Controller Example

<?php

namespace App\Controllers;

use Mlangeni\Machinjiri\Core\Http\HttpRequest;
use Mlangeni\Machinjiri\Core\Http\HttpResponse;

class UserController
{
    public function index(HttpRequest $request, HttpResponse $response)
    {
        return '<h1>All Users</h1>';
    }

    public function show(HttpRequest $request, HttpResponse $response, $id)
    {
        return "<h1>User: {$id}</h1>";
    }

    public function store(HttpRequest $request, HttpResponse $response)
    {
        $data = $request->getPostParams();
        return json_encode(['created' => true, 'data' => $data]);
    }
}

Using Array Handlers

Router::get('/users', [UserController::class, 'index']);

// With instance
$controller = new UserController();
Router::get('/users', [$controller, 'index']);

Attribute-Based Routing

Define routes directly on controller methods using PHP 8 attributes.

Basic Attribute Routing

<?php

namespace App\Controllers;

use Mlangeni\Machinjiri\Core\Routing\Attributes\Get;
use Mlangeni\Machinjiri\Core\Routing\Attributes\Post;
use Mlangeni\Machinjiri\Core\Routing\Attributes\Put;
use Mlangeni\Machinjiri\Core\Routing\Attributes\Delete;
use Mlangeni\Machinjiri\Core\Routing\Attributes\Prefix;

#[Prefix('/api/users')]
class UserController
{
    #[Get('', 'users.index')]
    public function index()
    {
        return json_encode(['users' => []]);
    }

    #[Get('/{id}', 'users.show')]
    public function show($id)
    {
        return json_encode(['user' => $id]);
    }

    #[Post('', 'users.store')]
    public function store()
    {
        return json_encode(['created' => true]);
    }

    #[Put('/{id}', 'users.update')]
    public function update($id)
    {
        return json_encode(['updated' => $id]);
    }

    #[Delete('/{id}', 'users.destroy')]
    public function destroy($id)
    {
        return json_encode(['deleted' => $id]);
    }
}

Registering Attribute Controllers

// Register a single controller
Router::controller(UserController::class);

// Register multiple controllers
Router::controllers([
    UserController::class,
    PostController::class,
    CommentController::class,
]);

Available Route Attributes

Attribute HTTP Method(s)
#[Get] GET
#[Post] POST
#[Put] PUT
#[Delete] DELETE
#[Patch] PATCH
#[Options] OPTIONS
#[Head] HEAD
#[Any] All methods
#[Route] Custom methods

Class-Level Attributes

#[Prefix('/api/v1')]
#[Middleware(['Auth', 'JsonResponse'])]
#[Cors(allowedOrigins: ['https://example.com'])]
#[RateLimit('api')]
class ApiController
{
    #[Get('/status')]
    public function status()
    {
        return json_encode(['status' => 'ok']);
    }

    #[Get('/data')]
    #[Middleware(['Cache'])]  // Merged with class middleware
    #[Where('id', '[0-9]+')]
    public function data()
    {
        return json_encode(['data' => []]);
    }
}

Method-Level Attributes

class SecureController
{
    #[Get('/public')]
    public function publicEndpoint()
    {
        return 'Public data';
    }

    #[Get('/private')]
    #[Middleware(['Auth'])]
    #[RateLimit('sensitive')]
    public function privateEndpoint()
    {
        return 'Private data';
    }

    #[Get('/admin')]
    #[Middleware(['Auth', 'Admin'])]
    #[Ajax]  // AJAX only
    public function adminEndpoint()
    {
        return json_encode(['admin' => true]);
    }
}

Repeatable Attributes

class MultiRouteController
{
    #[Get('/posts', 'posts.list')]
    #[Get('/articles', 'articles.list')]
    public function list()
    {
        return 'Content list';
    }

    #[Middleware(['Auth'])]
    #[Middleware(['LogAccess'])]
    public function secured()
    {
        return 'Secured content';
    }
}

Complete Attribute Reference

#[Prefix]

#[Prefix('/api/v1')]
class ApiController {}

#[Middleware]

#[Middleware(['Auth', 'Throttle'])]
#[Middleware('LogRequests')]  // Single middleware
class Controller {}

#[Cors]

#[Cors(
    allowedOrigins: ['https://app.example.com'],
    allowedMethods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    maxAge: 3600
)]
class ApiController {}

#[RateLimit]

#[RateLimit('api')]  // Uses 'api' limiter config
class ApiController {}

#[Ajax] / #[Traditional]

class Controller
{
    #[Get('/api/data')]
    #[Ajax]  // Only responds to AJAX requests
    public function ajaxOnly() {}

    #[Get('/page')]
    #[Traditional]  // Only responds to non-AJAX requests
    public function traditionalOnly() {}
}

#[Where]

class Controller
{
    #[Get('/user/{id}')]
    #[Where('id', '[0-9]+')]
    public function show($id) {}
}

#[Bind]

class Controller
{
    #[Get('/user/{user}')]
    #[Bind('user', User::class)]  // Auto-resolve User model
    public function show(User $user) {}
}

Route Model Binding

Automatically resolve route parameters to model instances.

Class-Based Binding

Router::get('/users/{user}', function ($user) {
    // $user is now a User instance
    return "User: {$user->name}";
}, null, ['bindings' => ['user' => User::class]]);

The User class must have a findOrFail() method:

class User
{
    public static function findOrFail($id)
    {
        $user = static::find($id);
        if (!$user) {
            throw new Exception("User not found");
        }
        return $user;
    }
}

Callable Binding

Router::get('/posts/{slug}', function ($post) {
    return "Post: {$post->title}";
}, null, [
    'bindings' => [
        'slug' => function ($slug) {
            return Post::where('slug', $slug)->firstOrFail();
        }
    ]
]);

Global Binding

Router::bind('user', User::class);
Router::bind('post', function ($id) {
    return Post::findOrFail($id);
});

Attribute-Based Binding

class UserController
{
    #[Get('/users/{user}')]
    #[Bind('user', User::class)]
    public function show($user)
    {
        return "User: {$user->name}";
    }
}

Rate Limiting

Protect your routes from abuse with rate limiting.

Configuration

Define limiters in RoutingConfig:

use Mlangeni\Machinjiri\Core\Routing\RoutingConfig;

$config = new RoutingConfig(
    rateLimiters: [
        'api' => [
            'max_requests' => 60,
            'period' => 60,  // seconds
        ],
        'login' => [
            'max_requests' => 5,
            'period' => 300,  // 5 attempts per 5 minutes
        ],
        'sensitive' => [
            'max_requests' => 10,
            'period' => 3600,  // 10 requests per hour
        ],
    ]
);

Applying Rate Limits

To Individual Routes

Router::post('/login', 'AuthController@login', null, ['rate_limit' => 'login']);

To Route Groups

Router::group(['prefix' => '/api', 'rate_limit' => 'api'], function () {
    Router::get('/users', 'UserController@index');
    Router::get('/posts', 'PostController@index');
});

Via Attributes

class ApiController
{
    #[Post('/login')]
    #[RateLimit('login')]
    public function login() {}

    #[Get('/sensitive-data')]
    #[RateLimit('sensitive')]
    public function sensitiveData() {}
}

Rate Limit Response

When rate limited, the router returns a 429 response:

{
    "error": "Rate Limit Exceeded",
    "message": "Too many requests. Please try again later.",
    "code": 429,
    "retry_after": 60,
    "timestamp": 1700000000
}

With header: Retry-After: 60


CORS Support

Handle Cross-Origin Resource Sharing automatically.

Configuring CORS

For Route Groups

Router::cors([
    'allowed_origins' => ['https://app.example.com', 'https://admin.example.com'],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
    'allowed_headers' => ['Content-Type', 'Authorization'],
    'max_age' => 86400,
], function () {
    Router::get('/api/data', 'ApiController@data');
    Router::post('/api/data', 'ApiController@store');
});

For Individual Routes

Router::get('/api/public', 'ApiController@public', null, [
    'cors' => [
        'allowed_origins' => ['*'],
        'allowed_methods' => ['GET'],
    ]
]);

Via Attributes

#[Cors(
    allowedOrigins: ['https://app.example.com'],
    allowedMethods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type']
)]
class ApiController
{
    #[Get('/data')]
    public function data() {}

    #[Post('/data')]
    #[Cors(allowedOrigins: ['*'])]  // Override for this method
    public function store() {}
}

CORS Preflight

The router automatically handles OPTIONS preflight requests for routes with CORS configuration.

CORS Defaults

Default CORS configuration in RoutingConfig:

public array $corsDefaults = [
    'allowed_origins' => ['*'],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    'allowed_headers' => ['*'],
    'max_age' => 86400,
];

AJAX & Traditional Routes

Control whether routes respond to AJAX or traditional browser requests.

AJAX-Only Routes

Router::ajax('/api/notifications', function () {
    return json_encode(['notifications' => []]);
});

Returns 404 for non-AJAX requests. AJAX detection uses the X-Requested-With: XMLHttpRequest header.

Traditional-Only Routes

Router::traditional('/contact', function () {
    return '<form>...</form>';
});

Returns 404 for AJAX requests.

Via Attributes

class Controller
{
    #[Get('/api/data')]
    #[Ajax]
    public function ajaxOnly() {}

    #[Get('/page')]
    #[Traditional]
    public function traditionalOnly() {}

    #[Get('/mixed')]
    public function mixed() {}  // Responds to both
}

JSON Response Detection

The router also checks the Accept header for application/json:

// Browser with Accept: application/json
// Returns JSON error responses

Resource Routes

Quickly generate CRUD routes for a resource.

Basic Resource

Router::resource('users', 'UserController');

This generates:

Method URI Action Route Name
GET /users index users.index
GET /users/create create users.create
POST /users store users.store
GET /users/{id} show users.show
GET /users/{id}/edit edit users.edit
PUT /users/{id} update users.update
DELETE /users/{id} destroy users.destroy

Limited Resources

// Only index and show
Router::resource('posts', 'PostController', ['only' => ['index', 'show']]);

// All except create and edit
Router::resource('posts', 'PostController', ['except' => ['create', 'edit']]);

Error Handling

404 Not Found

When no route matches, the router returns a 404 response:

  • JSON (for AJAX/API requests):

    {
        "error": "Not Found",
        "message": "The requested resource was not found",
        "code": 404,
        "timestamp": 1700000000
    }
  • HTML (for browser requests): Renders an error page

Custom Error Pages

Create error templates in resources/errors/:

resources/errors/
├── 404.php
├── 429.php
├── 500.php
└── 503.php

Example 404.php:

<!DOCTYPE html>
<html>
<head>
    <title>404 - Not Found</title>
</head>
<body>
    <h1>404 - Page Not Found</h1>
    <p><?= htmlspecialchars($message ?? 'The page you requested does not exist.') ?></p>
    <a href="/">Go Home</a>
</body>
</html>

Fallback Routes

Define a fallback for unmatched routes:

Router::fallback(function () {
    return 'Custom 404 page';
});

Maintenance Mode

The router automatically shows a maintenance page when the application is down:

// When container->isDownForMaintenance() returns true
// Returns 503 Service Unavailable

Route Caching

Improve production performance by caching routes.

Generating the Cache

use Mlangeni\Machinjiri\Core\Routing\RouteCache;
use Mlangeni\Machinjiri\Core\Routing\Router;

$router = Router::getInstance();
$collection = $router->getCollection();

$cache = new RouteCache('/path/to/cache/routes.cache', $collection);
$cache->generate();

Cache Configuration

Set the cache file path in RoutingConfig:

$config = new RoutingConfig(
    cacheFile: '/var/www/app/storage/cache/routes.cache'
);

Automatic Loading

The router automatically loads cached routes if the cache file exists:

// In Router::__construct()
$this->loadCachedRoutes();

Cache Limitations

  • Closure-based routes cannot be cached (handlers become null)
  • Only string and array handlers are fully cached
  • Clear the cache after route changes

Clearing the Cache

// Simply delete the cache file
unlink('/path/to/cache/routes.cache');

Advanced Topics

Method Spoofing

HTML forms can only submit GET or POST. Use method spoofing for PUT/DELETE:

<form method="POST" action="/users/42">
    <input type="hidden" name="_method" value="PUT">
    <!-- form fields -->
</form>

The router automatically detects _method and treats the request as PUT.

Base Path Detection

The router automatically detects the base path for applications in subdirectories:

https://example.com/myapp/users

Routes are defined as /users but matched correctly.

Custom Base Path

Override automatic detection:

// In Container
Container::setRoutingBase('/myapp');

Route Constraints (Dynamic)

Add constraints after defining a route:

Router::get('/post/{id}', 'PostController@show')
    ->where('id', '[0-9]+')
    ->where('slug', '[a-z0-9-]+');

Handler Resolution

The router supports multiple handler types:

// Closure
Router::get('/', function () { return 'Hello'; });

// Controller string
Router::get('/', 'HomeController@index');

// Fully qualified
Router::get('/', 'App\\Controllers\\HomeController@index');

// Array [class, method]
Router::get('/', [HomeController::class, 'index']);

// Array [instance, method]
Router::get('/', [$controller, 'index']);

Custom Response Handling

Handlers can return different types:

// String → HTML response
Router::get('/', function () {
    return '<h1>Hello</h1>';
});

// Array/Object → JSON (for AJAX/API)
Router::get('/api/data', function () {
    return ['users' => []];
});

// Using HttpResponse directly
Router::get('/custom', function ($request, $response) {
    $response->setStatusCode(201)
             ->setJsonBody(['created' => true])
             ->send();
});

Request & Response Objects

Handlers receive request and response objects:

Router::get('/info', function (HttpRequest $request, HttpResponse $response) {
    $method = $request->getMethod();
    $uri = $request->getUri();
    $isAjax = $request->isAjax();
    $ip = $request->getIp();

    return json_encode([
        'method' => $method,
        'uri' => $uri,
        'ajax' => $isAjax,
        'ip' => $ip,
    ]);
});

Middleware Pipeline

Middleware forms a pipeline:

Request → Middleware1 → Middleware2 → Handler → Response

Each middleware can:

  • Modify the request
  • Short-circuit (return early)
  • Pass to next middleware
  • Modify the response
class LogRequests
{
    public function handle($request, $response, $next, $params)
    {
        // Before handler
        error_log("Request: " . $request->getMethod() . " " . $request->getUri());

        // Call next middleware/handler
        $result = $next($params);

        // After handler
        error_log("Response sent");

        return $result;
    }
}

Dependency Injection in Controllers

The router instantiates controllers directly. For DI, use the container:

class UserController
{
    public function __construct()
    {
        $this->userService = Container::getInstance()->resolve(UserService::class);
    }
}

Or use a custom handler resolver:

class ContainerAwareResolver implements HandlerResolverInterface
{
    public function execute($handler, $params, $request, $response)
    {
        if (is_string($handler) && str_contains($handler, '@')) {
            [$class, $method] = explode('@', $handler);
            $instance = Container::getInstance()->resolve($class);
            return call_user_func_array([$instance, $method], [$request, $response, ...array_values($params)]);
        }
        // ... handle other types
    }
}

Route Groups with All Attributes

Router::group([
    'prefix' => '/api/v1',
    'middleware' => ['Auth', 'JsonResponse'],
    'cors' => [
        'allowed_origins' => ['https://app.example.com'],
        'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
    ],
    'rate_limit' => 'api',
], function () {
    Router::get('/users', 'UserController@index');
    Router::post('/users', 'UserController@store');
});

Testing Routes

// Reset router between tests
Router::resetInstance();

// Define test routes
Router::get('/test', function () {
    return 'test response';
});

// Simulate request (requires HttpRequest setup)

API Reference

Router Class

Static Methods

Method Description
Router::get($pattern, $handler, $name, $options) Define GET route
Router::post($pattern, $handler, $name, $options) Define POST route
Router::put($pattern, $handler, $name, $options) Define PUT route
Router::delete($pattern, $handler, $name, $options) Define DELETE route
Router::any($pattern, $handler, $name, $options) Define multi-method route
Router::ajax($pattern, $handler, $name, $options) Define AJAX-only route
Router::traditional($pattern, $handler, $name, $options) Define non-AJAX route
Router::group($attributes, $callback) Create route group
Router::middleware($middleware, $callback) Apply middleware group
Router::cors($config, $callback) Apply CORS group
Router::resource($name, $controller, $options) Generate resource routes
Router::fallback($handler) Set fallback handler
Router::bind($param, $resolver) Add global binding
Router::controller($class) Register attribute controller
Router::controllers($classes) Register multiple controllers
Router::route($name, $params) Generate URL for named route
Router::absoluteRoute($name, $params) Generate absolute URL
Router::baseUrl() Get base URL
Router::dispatch() Dispatch the current request
Router::getInstance() Get router instance
Router::resetInstance() Reset singleton

Instance Methods

Method Description
addRoute($methods, $pattern, $handler, $name, $options) Add route
addResource($name, $controller, $options) Add resource routes
setFallback($handler) Set fallback
addBinding($param, $resolver) Add binding
where($param, $regex) Add constraint to last route
instanceGroup($attributes, $callback) Create group
instanceDispatch() Dispatch request

Route Options

Option Type Description
middleware array Middleware to apply
cors array CORS configuration
rate_limit string Rate limiter name
ajax_only bool AJAX-only route
no_ajax bool Non-AJAX route
where array Parameter constraints
bindings array Model bindings

RoutingConfig

new RoutingConfig(
    cacheFile: '/path/to/routes.cache',
    errorsDir: '/path/to/errors',
    controllersNamespace: 'App\\Controllers',
    rateLimiters: [
        'api' => ['max_requests' => 60, 'period' => 60],
    ],
    viewsBasePath: '/path/to/views',
    viewsCachePath: '/path/to/views/cache',
    enableCsrf: true,
    corsDefaults: [
        'allowed_origins' => ['*'],
        'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
        'allowed_headers' => ['*'],
        'max_age' => 86400,
    ]
);

Route Attribute Reference

Attribute Target Parameters
#[Get] Method $pattern, $name, $options
#[Post] Method $pattern, $name, $options
#[Put] Method $pattern, $name, $options
#[Delete] Method $pattern, $name, $options
#[Patch] Method $pattern, $name, $options
#[Options] Method $pattern, $name, $options
#[Head] Method $pattern, $name, $options
#[Any] Method $pattern, $name, $options
#[Route] Method $methods, $pattern, $name, $options
#[Prefix] Class $prefix
#[Middleware] Class/Method $middleware
#[Cors] Class/Method $allowedOrigins, $allowedMethods, $allowedHeaders, $maxAge
#[RateLimit] Method $limiter
#[Ajax] Method (none)
#[Traditional] Method (none)
#[Where] Method $param, $regex
#[Bind] Method $param, $model

Examples

Complete API Setup

<?php

use Mlangeni\Machinjiri\Core\Routing\Router;
use Mlangeni\Machinjiri\Core\Routing\RoutingConfig;

// Configure
$config = new RoutingConfig(
    rateLimiters: [
        'api' => ['max_requests' => 100, 'period' => 60],
        'auth' => ['max_requests' => 5, 'period' => 300],
    ]
);

// Public API
Router::group(['prefix' => '/api', 'rate_limit' => 'api'], function () {
    Router::get('/status', function () {
        return ['status' => 'ok', 'version' => '1.0.0'];
    });

    Router::get('/posts', 'PostController@index', 'api.posts.index');
    Router::get('/posts/{id}', 'PostController@show', 'api.posts.show')
        ->where('id', '[0-9]+');
});

// Authenticated API
Router::group([
    'prefix' => '/api',
    'middleware' => ['Auth'],
    'cors' => ['allowed_origins' => ['https://app.example.com']],
], function () {
    Router::post('/posts', 'PostController@store', 'api.posts.store');
    Router::put('/posts/{id}', 'PostController@update', 'api.posts.update');
    Router::delete('/posts/{id}', 'PostController@destroy', 'api.posts.destroy');

    Router::get('/me', 'UserController@me', 'api.me');
});

// Auth endpoints (strict rate limit)
Router::group(['prefix' => '/auth', 'rate_limit' => 'auth'], function () {
    Router::post('/login', 'AuthController@login');
    Router::post('/register', 'AuthController@register');
    Router::post('/forgot-password', 'AuthController@forgotPassword');
});

// Web routes
Router::get('/', 'HomeController@index', 'home');
Router::get('/about', 'PageController@about', 'about');
Router::get('/contact', 'PageController@contact', 'contact')
    ->where('id', '[0-9]+');

// Fallback
Router::fallback(function () {
    return 'Page not found';
});

// Dispatch
Router::dispatch();

Attribute Controller Example

<?php

namespace App\Controllers;

use Mlangeni\Machinjiri\Core\Routing\Attributes\{
    Get, Post, Put, Delete, Prefix, Middleware, Cors, RateLimit, Where, Bind, Ajax
};

#[Prefix('/api/v1')]
#[Middleware(['JsonResponse'])]
#[Cors(allowedOrigins: ['https://app.example.com'])]
class ProductController
{
    #[Get('/products', 'api.products.index')]
    public function index()
    {
        return ['products' => []];
    }

    #[Get('/products/{id}', 'api.products.show')]
    #[Where('id', '[0-9]+')]
    public function show($id)
    {
        return ['product' => $id];
    }

    #[Post('/products', 'api.products.store')]
    #[Middleware(['Auth'])]
    #[RateLimit('api')]
    public function store()
    {
        return ['created' => true];
    }

    #[Put('/products/{id}', 'api.products.update')]
    #[Middleware(['Auth'])]
    #[Bind('id', Product::class)]
    public function update($product)
    {
        return ['updated' => $product->id];
    }

    #[Delete('/products/{id}', 'api.products.destroy')]
    #[Middleware(['Auth', 'Admin'])]
    public function destroy($id)
    {
        return ['deleted' => $id];
    }

    #[Get('/products/search')]
    #[Ajax]
    public function search()
    {
        return ['results' => []];
    }
}

Troubleshooting

Route Not Matching

  1. Check the HTTP method matches exactly
  2. Verify the URI path (trailing slashes are normalized)
  3. Check parameter constraints
  4. Ensure base path detection is correct

Middleware Not Running

  1. Verify middleware class exists in App\Middleware
  2. Check middleware implements handle() method
  3. Verify middleware name spelling in route definition

CORS Issues

  1. Ensure the route has CORS configuration
  2. Check preflight handling for OPTIONS requests
  3. Verify allowed origins include the requesting origin

Rate Limiting Not Working

  1. Check the limiter name matches configuration
  2. Verify cache manager is available
  3. Check the cache store is working

Attribute Routes Not Registering

  1. Ensure PHP 8+ is used
  2. Check attributes are properly imported
  3. Verify controller is registered with Router::controller()
  4. Check method is public and not static

Clone this wiki locally