PHPackages                             pauldeano/dromos - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. pauldeano/dromos

ActiveLibrary[Framework](/categories/framework)

pauldeano/dromos
================

Lightweight PHP Router

v1.1.0(2mo ago)011[8 issues](https://github.com/p4u1d34n0/Dromos/issues)[6 PRs](https://github.com/p4u1d34n0/Dromos/pulls)MITPHPPHP &gt;=8.2

Since Oct 9Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/p4u1d34n0/Dromos)[ Packagist](https://packagist.org/packages/pauldeano/dromos)[ Fund](https://ko-fi.com/pauldean)[ RSS](/packages/pauldeano-dromos/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)DependenciesVersions (15)Used By (0)

[![Dromos - Superlight PHP Router](https://camo.githubusercontent.com/670abd35caec953d0cb0da0107f758f7961750c7c3f2c95eab23b428b01709c6/687474703a2f2f64726f6d6f732e7061756c6465616e2e6d652f44726f6d6f732e706e67)](https://camo.githubusercontent.com/670abd35caec953d0cb0da0107f758f7961750c7c3f2c95eab23b428b01709c6/687474703a2f2f64726f6d6f732e7061756c6465616e2e6d652f44726f6d6f732e706e67)

Why Choose Dromos?
==================

[](#why-choose-dromos)

> Dromos is a lightweight PHP micro-service library with PSR-7 and PSR-15 inspired interfaces, designed for maximum flexibility and performance. It offers fast routing, route caching, middleware support, and a minimal emitter layer to send responses to any PHP SAPI or server environment.

Key Features
------------

[](#key-features)

- **PSR-7 / PSR-15 Inspired** — Native HTTP message and middleware implementations with PSR-compatible APIs. No external libraries required.
- **Fast Routing** — Expressive routes with parameters, wildcards, route groups, and HTTP method support.
- **Route Groups** — Group routes under a shared prefix with per-group middleware.
- **Route Caching** — Serialize and load route trees to eliminate route parsing overhead in production.
- **Middleware Pipeline** — Global and per-route middleware with PSR-15 style handler chains. Ships with CORS, Auth, and Rate Limiting middleware.
- **Input Validation** — Built-in validator with pipe-delimited rules for API input.
- **JSON-First API Design** — JSON request body parsing, JSON error responses, and response helpers.
- **Minimal Emitter Layer** — Built-in `Emitter` handles status line, headers, and body output. Implement `EmitterInterface` to target other runtimes like OpenSwoole.
- **Micro-Service Ready** — Perfect for REST, RPC, or event-driven micro-services with zero framework magic.

Frameworks like Laravel and Symfony excel at monolithic apps but introduce significant overhead:

- **Lean Footprint** — Core library &lt; 100 KB.
- **High Concurrency** — Custom emitters + OpenSwoole handle 10k+ req/s.
- **Zero War Story** — No imposed folder structure. Organise your code your way.

Implement OpenSwoole and you can start building micro-services that can rival Node.js or Go in performance, with PHP's robust ecosystem.

Installation
------------

[](#installation)

```
composer require pauldeano/dromos

```

Requires **PHP 8.2+** with zero external dependencies.

---

Routing
=======

[](#routing)

Basic Routes
------------

[](#basic-routes)

```
use Dromos\Router;
use Dromos\Http\Request;
use Dromos\Http\Response;

Router::Get("/users", function (Request $request, Response $response) {
    return $response->json(['users' => []]);
});

Router::Post("/users", function (Request $request, Response $response) {
    $body = $request->getParsedBody();
    return $response->created(['id' => 1, 'name' => $body['name']]);
});
```

All HTTP methods are supported:

```
Router::Get("/path",     $target);
Router::Post("/path",    $target);
Router::Put("/path",     $target);
Router::Patch("/path",   $target);
Router::Delete("/path",  $target);
Router::Head("/path",    $target);
Router::Options("/path", $target);
```

Route Parameters
----------------

[](#route-parameters)

```
Router::Get("/users/{id}", function (Request $request, Response $response) {
    $id = $request->getAttribute('id');
    return $response->json(['id' => $id]);
});
```

Using Controllers
-----------------

[](#using-controllers)

```
Router::Get("/users",       [UserController::class, 'index']);
Router::Get("/users/{id}",  [UserController::class, 'show']);
Router::Post("/users",      [UserController::class, 'store']);
Router::Put("/users/{id}",  [UserController::class, 'update']);
Router::Patch("/users/{id}",[UserController::class, 'patch']);
Router::Delete("/users/{id}",[UserController::class, 'destroy']);
```

Resource Routes
---------------

[](#resource-routes)

Auto-register all HTTP methods for a controller:

```
Router::Resource("/users/{id}", UserController::class);
```

By default, the controller must have public methods named `get`, `post`, `put`, `patch`, `delete`, `options`, `head`. Routes are registered when the `RouteResource` object goes out of scope, so chained methods are applied before registration.

### Customising Resource Methods

[](#customising-resource-methods)

```
// API resource (GET, POST, PUT, PATCH, DELETE only — excludes OPTIONS and HEAD)
Router::Resource("/users/{id}", UserController::class)->apiResource();

// Exclude specific methods
Router::Resource("/users/{id}", UserController::class)
    ->exceptMethods(["HEAD", "OPTIONS", "DELETE"]);

// Only specific methods
Router::Resource("/users/{id}", UserController::class)
    ->onlyMethods(["GET", "POST"]);
```

Route Groups
------------

[](#route-groups)

Group routes under a shared prefix with optional per-group middleware:

```
use Dromos\Middleware\AuthMiddleware;

Router::group('/api/v1', function ($group) {
    // Public routes
    $group->get('/status', function (Request $request, Response $response) {
        return $response->json(['status' => 'ok']);
    });

    // Protected routes with auth middleware — define middleware BEFORE nested groups
    $group->group('/users', function ($users) {
        $users->middleware(new AuthMiddleware(function ($token) {
            return $token === 'valid-token' ? ['user_id' => 1] : false;
        }));

        $users->get('/list', [UserController::class, 'index']);
        $users->post('/create', [UserController::class, 'store']);
        $users->get('/{id}', [UserController::class, 'show']);
        $users->put('/{id}', [UserController::class, 'update']);
        $users->delete('/{id}', [UserController::class, 'destroy']);
    });
});
```

Nested groups inherit the parent's prefix and middleware stack at the point of creation. Add middleware to a group **before** defining nested groups to ensure inheritance.

---

Middleware
==========

[](#middleware)

Dromos uses PSR-15 style middleware. Middleware can be applied globally or per-route/group.

Global Middleware
-----------------

[](#global-middleware)

```
$router = new Router();
$router->addMiddleware(new CorsMiddleware());
$router->addMiddleware(new RateLimitMiddleware(100, 60));
```

Per-Route / Per-Group Middleware
--------------------------------

[](#per-route--per-group-middleware)

See [Route Groups](#route-groups) above for per-group middleware.

Built-in Middleware
-------------------

[](#built-in-middleware)

### CORS Middleware

[](#cors-middleware)

```
use Dromos\Middleware\CorsMiddleware;

$cors = new CorsMiddleware([
    'allowed_origins'   => ['https://example.com', 'https://app.example.com'],
    'allowed_methods'   => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    'allowed_headers'   => ['Content-Type', 'Authorization', 'X-Requested-With'],
    'max_age'           => 86400,
    'allow_credentials' => true,
]);

$router->addMiddleware($cors);
```

All config keys are optional. Defaults to `allowed_origins: ['*']`.

Handles OPTIONS preflight requests automatically with a 204 response. Returns 403 for disallowed origins on preflight.

**Note:** `allow_credentials: true` cannot be used with wildcard `allowed_origins: ['*']`. Specify explicit origins when using credentials. An `InvalidArgumentException` is thrown if this combination is detected.

### Auth Middleware

[](#auth-middleware)

Supports Bearer tokens and API keys:

```
use Dromos\Middleware\AuthMiddleware;

$auth = new AuthMiddleware(function (string $token) {
    // Your authentication logic here.
    // Return a truthy value (user array/object) on success, or false/null on failure.
    // IMPORTANT: For API key comparison, use hash_equals() to prevent timing attacks.
    $user = MyUserService::validateToken($token);
    return $user ?: false;
});
```

- Extracts `Bearer ` from the `Authorization` header
- Falls back to the `X-API-Key` header
- On success, stores the result as `$request->getAttribute('auth_user')`
- On failure, returns a 401 JSON response

### Rate Limit Middleware

[](#rate-limit-middleware)

```
use Dromos\Middleware\RateLimitMiddleware;

// 100 requests per 60-second window
$rateLimiter = new RateLimitMiddleware(100, 60);

$router->addMiddleware($rateLimiter);
```

Adds `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers to all responses. Returns 429 with `Retry-After` header when exceeded.

**Important:** Uses in-memory per-IP tracking. This is only effective in long-running processes (OpenSwoole, ReactPHP). In standard PHP-FPM deployments, each request runs in isolated memory and counters are never shared. For FPM, implement your own middleware backed by Redis or similar.

### Custom Middleware

[](#custom-middleware)

Implement `MiddlewareInterface`:

```
use Dromos\Http\Middleware\MiddlewareInterface;
use Dromos\Http\Middleware\RequestHandlerInterface;
use Dromos\Http\Message\ServerRequestInterface;
use Dromos\Http\Message\ResponseInterface;

class MyMiddleware implements MiddlewareInterface
{
    public function handle(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Before the route handler
        $request = $request->withAttribute('started_at', microtime(true));

        // Call the next handler
        $response = $handler->handle($request);

        // After the route handler
        return $response->withHeader('X-Response-Time', '42ms');
    }
}
```

---

Request &amp; Response
======================

[](#request--response)

JSON Request Body Parsing
-------------------------

[](#json-request-body-parsing)

JSON request bodies (`Content-Type: application/json`) are automatically parsed and available via `getParsedBody()`. Request bodies are limited to 1MB by default with a max JSON nesting depth of 64.

```
Router::Post("/users", function (Request $request, Response $response) {
    $data = $request->getParsedBody();
    // $data is the decoded JSON array
    return $response->created(['id' => 1, 'name' => $data['name']]);
});
```

Response Helpers
----------------

[](#response-helpers)

All response helpers return a new immutable Response instance:

```
// JSON response
$response->json(['key' => 'value'], 200);

// Plain text
$response->text('Hello World', 200);

// HTML (writes raw unescaped content — sanitise user input before passing)
$response->html('Hello', 200);

// 201 Created with JSON body
$response->created(['id' => 1]);

// 204 No Content
$response->noContent();

// Redirect (URL is sanitised against header injection)
$response->redirect('/new-location', 302);
```

Error Responses
---------------

[](#error-responses)

Routing errors return structured JSON. For `RouterException` errors (404, 405), the exception message is included. For unexpected errors, a generic "Internal Server Error" message is returned to prevent leaking internal details.

```
{
    "error": "Not Found",
    "message": "Route not found: /unknown",
    "status": 404
}
```

---

Input Validation
================

[](#input-validation)

```
use Dromos\Validation\Validator;

Router::Post("/users", function (Request $request, Response $response) {
    $validator = new Validator($request->getParsedBody(), [
        'name'  => 'required|string|min:2|max:100',
        'email' => 'required|email',
        'age'   => 'integer|min:0|max:150',
        'role'  => 'in:admin,user,editor',
    ]);

    if ($validator->fails()) {
        return $response->json(['errors' => $validator->errors()], 422);
    }

    $clean = $validator->validated(); // Only validated fields, unknown keys stripped
    return $response->created($clean);
});
```

**Note:** `validated()` throws `ValidationException` if validation has not passed. Always check `fails()` first, or wrap in a try/catch. `validated()` returns the original values filtered by key — it does not sanitise or type-cast data.

Unknown rule names throw `InvalidArgumentException` to catch typos early.

### Available Rules

[](#available-rules)

RuleDescription`required`Field must be present and non-empty`string`Must be a string`integer`Must be an integer`numeric`Must be numeric`email`Must be a valid email address`url`Must be a valid URL`boolean`Must be a boolean value`array`Must be an array`min:n`Minimum length (string), value (numeric), or count (array)`max:n`Maximum length (string), value (numeric), or count (array)`in:a,b,c`Must be one of the listed values`regex:/pattern/`Must match the regex pattern (pipe characters inside regex are handled correctly)Rules are pipe-delimited: `'required|string|min:2|max:100'`

---

Emitter
=======

[](#emitter)

Send a response to the client:

```
use Dromos\Router;
use Dromos\Http\Request;
use Dromos\Http\Emitter\Emitter;

$router = new Router();
$response = $router->handle(new Request());

$emitter = new Emitter();
$emitter->emit($response);
```

### Custom Emitters

[](#custom-emitters)

Implement `EmitterInterface` to target non-SAPI environments (e.g., OpenSwoole):

```
use Dromos\Http\Emitter\EmitterInterface;
use Dromos\Http\Message\ResponseInterface;

class SwooleEmitter implements EmitterInterface
{
    public function emit(ResponseInterface $response): void
    {
        // Your OpenSwoole response logic here
    }
}
```

---

Environment Variables
=====================

[](#environment-variables)

```
use Dromos\Env\EnvLoader;

EnvLoader::load(__DIR__ . '/.env');

$dbHost = EnvLoader::get('DB_HOST', 'localhost');
```

Loads `.env` files into `$_ENV` and `putenv()`. Skips comments (`#`) and malformed lines. Falsy values like `"0"` are correctly preserved.

---

Route Caching
=============

[](#route-caching)

For production, cache compiled routes to skip parsing:

```
ROUTER_CACHE_FILE=/tmp/dromos_routes.cache
```

```
Router::initialize(); // Loads cache if ROUTER_CACHE_FILE is set
```

Or manage manually:

```
Router::enableCache('/tmp/routes.cache');
Router::clearCache();
Router::disableCache();
```

**Note:** Route caching uses `json_encode()`. Routes using Closure handlers cannot be cached — use controller targets for cacheable routes.

---

Full Example
============

[](#full-example)

```
