Codync
무료 오픈소스 1:1 대안, Grok Bot과 Muse 대신
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A small, fast PHP router for APIs: lean mode, an opcache route cache, and PSR-7, PSR-15 and PSR-17
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A PHP router for fast APIs: dromos.codenexis.com. In the router comparison it beats Symfony Routing, FastRoute, Laravel's router, Flight, Slim and Mezzio in every in-process test at 2,000 routes. It loads 5 files to route a request, and it depends only on the PSR interface packages.
Router::Get('/users/{id}', [UserController::class, 'show']);
$route = Router::match($_SERVER['REQUEST_METHOD'], $path); // [200, $handler, $params, $middleware]
Microseconds each router spends on a request to a route with a parameter, on an Apple M3 Pro with PHP 8.4.4 and opcache. Lower is better.
| Worker, 20 routes | Worker, 2,000 routes | PHP-FPM cached, 20 routes | PHP-FPM cached, 2,000 routes | Files loaded | |
|---|---|---|---|---|---|
| Dromos lean | 0.81 | 0.83 | 1.73 | 1.74 | 5 |
| Dromos | 2.40 | 2.43 | 4.46 | 4.45 | 25 |
| Symfony Routing | 0.87 | 1.36 | 1.66 | 2.16 | 10 |
| FastRoute | 0.61 | 16.7 | 2.21 | 20.5 | 10 |
| Laravel | 39.9 | 41.1 | 63.8 | 66.3 | 75 |
| Slim | 11.1 | 29.1 | 47.1 | 1,653 | 68 |
| Mezzio | 11.5 | 141 | 72.6 | 2,925 | 103 |
| Flight | 25.8 | 1,373 | 74.4* | 4,889* | 10 |
* Flight has no route cache. The router comparison has every test, including real HTTP requests, with the spread between runs and every sample.
GET /res{n}, GET /res{n}/{id}, GET /res{n}/{id}/items/{item} and POST /res{n}. The table requests a route of the last resource registered, which is the worst case for routers that try their routes in turn. Every handler returns a small JSON body.illuminate/routing on its own, without the rest of the framework. Both choices favour the other routers.hrtime(), after one untimed call per round. Each script runs 5 times in a fresh process, and a result is the median across runs of each run's median.To run it yourself (about 90 minutes, and it needs ab):
cd benchmark/compare
composer install
php run.php
php report.php results/<name>
Most of the routers we tested slow down as you add routes. FastRoute tries its routes in batches of about ten, so at 2,000 routes it is 27 times slower than at 20. Dromos finds exact paths with a hash lookup, and groups routes with placeholders by their static prefix, so /users/42 is only tested against the routes under /users. It takes the same time with 20 routes as with 2,000.
You can use it two ways. Router::match() only finds the route and leaves the rest to you, which is all many APIs need. Router::handle() gives you PSR-7 requests and responses and runs middleware, including any PSR-15 middleware. Both use the same routes and the same cache, so you can start lean and add middleware later.
Under PHP-FPM, Router::loadRoutes() keeps the compiled routes in a PHP file that opcache holds in memory, so a request touches no file and routing takes about 1.7 µs in lean mode, or 4.5 µs with middleware. The cache rebuilds itself when you change your routes. Under OpenSwoole, routes are built once per worker.
It's also small enough to read in an afternoon. Lean mode is 5 files and 834 lines; full mode is 25 files and 3,424 lines, a third of which are the PSR-7 interfaces, mostly docblocks. The messages pass the community PSR-7 and PSR-17 test suites.
With only a handful of routes, FastRoute and Symfony win some tests, by a fraction of a microsecond. If you want an ORM, templating and a container, use Laravel or Symfony; Dromos is a router with middleware, validation and response helpers. And routing is rarely the slow part of a request: one database query costs more than all of this. Dromos matters most for APIs that serve a lot of small requests.
composer require pauldeano/dromos
Requires PHP 8.2+. The only dependencies are the four PSR interface packages: psr/http-message, psr/http-server-handler, psr/http-server-middleware and psr/http-factory.
Allow header when only the method is wrongControllerResolverInterfaceRouter::match() returns [200, $handler, $params, $middleware], [404] or [405, $allowedMethods], like FastRouterequired|emailEmitterInterface for anything elseuse 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);
Router::Get("/users/{id}", function (Request $request, Response $response) {
$id = $request->getAttribute('id');
return $response->json(['id' => $id]);
});
A placeholder matches one path segment (anything except /). If a placeholder name appears twice, both segments must hold the same value.
Routes are matched in the order they are registered, and the first match wins. Register specific routes before general ones:
Router::Get("/users/me", [UserController::class, 'me']); // matches /users/me
Router::Get("/users/{id}", [UserController::class, 'show']); // matches everything else
If /users/{id} were registered first, it would also match /users/me.
Under the hood, the router builds a lookup table the first time it dispatches, and again whenever routes change. Exact paths are found with a single hash lookup. Parameterised routes are grouped by the static segments before their first placeholder (/users for /users/{id}), and each group is combined into one regex, so a request only tests the few routes that could match its path. In a long-running server such as OpenSwoole, the table is built once and reused for every request.
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']);
A controller is created the first time one of its routes is dispatched, then reused for every later request in the same process. There's one instance per class, shared by all of that class's routes. Under PHP-FPM that means at most one per request, and only for the controller that's actually used. Under OpenSwoole each worker keeps one instance per class, so don't keep per-request state in controller properties.
To build controllers some other way, such as from a dependency injection container, implement ControllerResolverInterface and register it:
use Dromos\Routing\ControllerResolverInterface;
final class ContainerControllerResolver implements ControllerResolverInterface
{
public function __construct(private ContainerInterface $container) {}
public function resolve(string $class): object
{
return $this->container->get($class);
}
}
Router::setControllerResolver(new ContainerControllerResolver($container));
A handler must return a ResponseInterface. Anything else becomes a 500 error.
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.
// 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"]);
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.
Router::handle() builds a request and a response, runs middleware and calls the handler for you. If you don't need those, Router::match() only finds the route, as a matcher-only router such as FastRoute does, and you call the handler yourself. In a worker it's three to six times faster per request; see the router comparison.
// public/index.php
Router::loadRoutes(__DIR__ . '/../routes.php', __DIR__ . '/../storage/cache/routes.php');
$path = (string) parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$route = Router::match($_SERVER['REQUEST_METHOD'], $path);
switch ($route[0]) {
case 200:
[, [$class, $action], $params] = $route;
header('Content-Type: application/json');
echo json_encode((new $class())->{$action}(...$params));
break;
case 405:
http_response_code(405);
header('Allow: ' . implode(', ', $route[1]));
break;
default:
http_response_code(404);
}
The result follows FastRoute's shape:
| Result | When |
|---|---|
[200, $handler, $params, $middleware] | A route matches. $handler is the closure or [Controller::class, 'method'] you registered, and $params holds the placeholder values by name. |
[404] | No route matches the path. |
[405, $allowedMethods] | Routes match the path, but only under other methods. |
handle(), including loadRoutes(), so you can switch between the two.$middleware, so check it if you rely on it, for example for authentication. Middleware added with addMiddleware() belongs to a Router instance and isn't seen by match().Dromos runs its own middleware and PSR-15 middleware. Either can be applied globally or per route or group.
$router = new Router();
$router->addMiddleware(new CorsMiddleware());
$router->addMiddleware(new RateLimitMiddleware(100, 60));
See Route Groups above for per-group middleware. Middleware can be an instance or a class name:
$group->middleware(new AuthMiddleware($authenticator)); // an instance, built now
$group->middleware(RequireApiKey::class); // a class name, built on first use
Router::setRouteMiddleware('GET', '/reports', [RequireApiKey::class]);
A class name is built by the controller resolver the first time a request reaches one of its routes, and reused after that, so a request never pays for middleware it doesn't use. Routes with class-name middleware can also be cached by Router::loadRoutes(). The default resolver calls the constructor with no arguments; for middleware that needs some, register a resolver backed by your container with Router::setControllerResolver().
Middleware that implements Psr\Http\Server\MiddlewareInterface works anywhere Dromos middleware does: globally, per route or group, as an instance or a class name, and from the route cache.
$router->addMiddleware(new SomePackage\Psr15Middleware());
$group->middleware(SomePackage\OtherPsr15Middleware::class);
Dromos\Http\Middleware\MiddlewareInterface, whose method is handle(). PSR-15's is process(). The two can be mixed in one chain.Psr\Http\Message\ServerRequestInterface gets the request, and one typed ResponseInterface gets a new response, as the Dromos types do.To use the router where a PSR-15 request handler is expected, for example by a PSR-15 server, wrap it. The wrapper accepts any PSR-7 server request:
$handler = new Dromos\Http\PsrRequestHandler(new Router());
Dromos\Http\HttpFactory implements every PSR-17 factory: requests, responses, server requests, streams, uploaded files and URIs. Give it to PSR-15 middleware that asks for a factory, so the responses it builds are Dromos responses:
$factory = new Dromos\Http\HttpFactory();
$router->addMiddleware(new SomePackage\Psr15Middleware(responseFactory: $factory, streamFactory: $factory));
Dromos's messages and factories pass the community compliance suites, php-http/psr7-integration-tests and http-interop/http-factory-tests, which run with the rest of the tests.
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.
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;
});
Bearer <token> from the Authorization headerX-API-Key header$request->getAttribute('auth_user')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.
Counters are kept in a RateLimitStore. By default every RateLimitMiddleware shares one in-memory store, which only works in long-running processes (OpenSwoole, RoadRunner, ReactPHP). It tracks up to 10,000 client IPs and drops the oldest window beyond that. In tests, RateLimitMiddleware::resetStore() clears it.
PHP-FPM: each request has its own memory, so in-memory counters are never shared. Pass a store backed by shared storage instead. RateLimitStore has one method; this one uses APCu:
use Dromos\Middleware\RateLimitEntry;
use Dromos\Middleware\RateLimitStore;
final class ApcuRateLimitStore implements RateLimitStore
{
public function increment(string $key, int $windowSeconds): RateLimitEntry
{
$now = time();
$window = intdiv($now, $windowSeconds) * $windowSeconds;
$name = "ratelimit:$key:$window";
// apcu_add only succeeds for the first request in a window; apcu_inc is atomic
apcu_add($name, 0, $windowSeconds);
return new RateLimitEntry((int) apcu_inc($name), $window);
}
}
$router->addMiddleware(new RateLimitMiddleware(100, 60, new ApcuRateLimitStore()));
A Redis store works the same way with INCR and EXPIRE. APCu is shared between the workers of one PHP-FPM pool; use Redis to share limits between servers.
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');
}
}
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']]);
});
All response helpers return a new immutable Response instance, replacing any body and Content-Type it had:
// JSON response (throws JsonException if the data can't be encoded)
$response->json(['key' => 'value'], 200);
$response->json(['url' => '/a/b'], 200, JSON_UNESCAPED_SLASHES); // optional json_encode() flags
// Plain text
$response->text('Hello World', 200);
// HTML (writes raw unescaped content — sanitise user input before passing)
$response->html('<h1>Hello</h1>', 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);
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
}
A 405 also lists the allowed methods, under details.available_methods and in the Allow header. 404s and 405s are the client's errors, so they aren't written to the error log; 500s are, with the real exception message.
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.
| Rule | Description |
|---|---|
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'
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);
SwooleEmitter sends a response through OpenSwoole's HTTP server. It needs the openswoole extension (22 or later). Register your routes once, before the server starts, and every worker reuses the compiled route table:
use Dromos\Http\Emitter\SwooleEmitter;
use Dromos\Http\Request;
use Dromos\Router;
use OpenSwoole\Http\Request as SwooleRequest;
use OpenSwoole\Http\Response as SwooleResponse;
use OpenSwoole\Http\Server;
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/routes.php';
$router = new Router();
$server = new Server('0.0.0.0', 9501);
$server->on('request', function (SwooleRequest $in, SwooleResponse $out) use ($router) {
$query = $in->server['query_string'] ?? '';
$request = Request::create(
method: $in->server['request_method'],
uri: $in->server['request_uri'] . ($query === '' ? '' : '?' . $query),
serverParams: array_change_key_case($in->server, CASE_UPPER),
headers: $in->header ?? [],
queryParams: $in->get ?? [],
parsedBody: $in->post,
body: (string) $in->rawContent(),
);
(new SwooleEmitter($out))->emit($router->handle($request));
});
$server->start();
rawContent() into parsedBody yourself, as benchmark/src/SwooleRequestFactory.php does.Set-Cookie headers.Content-Length. A larger file or socket stream is sent in 8 KB chunks with chunked transfer encoding, so it never sits in memory whole.Implement EmitterInterface to send responses anywhere else:
use Dromos\Http\Emitter\EmitterInterface;
use Dromos\Http\Message\ResponseInterface;
class MyEmitter implements EmitterInterface
{
public function emit(ResponseInterface $response): void
{
// Send the status, headers and body
}
}
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.
Each file is loaded once per process: calling load() again with the same file does nothing, while a different file (say .env.local) still loads. Under OpenSwoole or RoadRunner, call it once at bootstrap, before the server starts. $_ENV and putenv() are shared by every request a worker handles, so read values into your configuration there rather than per request.
Dromos has two route caches. Both store the compiled route table in a PHP file that opcache keeps in shared memory.
Put your routes in their own file and load it with a cache file:
// public/index.php
Router::loadRoutes(__DIR__ . '/../routes.php', __DIR__ . '/../storage/cache/routes.php');
(new Emitter())->emit((new Router())->handle(new Request()));
// routes.php
use Dromos\Router;
Router::Get('/users/{id}', [UserController::class, 'show']);
Router::group('/admin', function ($group) {
$group->middleware(RequireAdmin::class); // by class name, so it can be cached
$group->get('/stats', [StatsController::class, 'index']);
});
The first request runs routes.php and stores the routes. Every later request dispatches straight from the cache: routes.php doesn't run, and nothing is registered or compiled. Under PHP-FPM, a request with 2,000 routes takes about the same time as one with 20.
routes.php changes, as soon as PHP would notice changed code. While opcache holds both files as current, routes.php isn't even checked, so a request touches no file. A change counts when opcache revalidates routes.php (every opcache.revalidate_freq seconds, with opcache.validate_timestamps on), or when opcache is reset or the file invalidated, as with opcache.validate_timestamps off. Without opcache, routes.php is checked on every request (its size, modification time and inode).routes.php includes other route files, delete the cache file when you deploy changes to them.routes.php defines any, it simply runs on every request. Give middleware by class name to keep them cacheable; they're built by the controller resolver when a request first reaches one of their routes.loadRoutes() mean the file runs on every request, so the order of all routes is kept. Routes added after it work as normal.enableCache() below.If your routes use closures, keep registering them on every request and let the cache skip compiling them:
ROUTER_CACHE_FILE=/var/www/app/storage/cache/routes.php
Router::initialize(); // Enables the cache if ROUTER_CACHE_FILE is set
// Register your routes as normal, on every request
Router::Get("/users/{id}", [UserController::class, 'show']);
Or manage it manually:
Router::enableCache('/var/www/app/storage/cache/routes.php');
Router::clearCache(); // delete the file; it's rebuilt on the next request
Router::disableCache();
How it works:
Under PHP-FPM, add Dromos to opcache's preload so its classes are compiled once when PHP-FPM starts, instead of being loaded on each request:
opcache.preload=/var/www/app/vendor/pauldeano/dromos/preload.php
opcache.preload_user=www-data
Restart PHP-FPM after upgrading Dromos. If your app already has a preload script, require this one from it.
Security: cache files are run with require. Keep them in a directory only your application can write to, never a shared one such as /tmp on a multi-user server.
Upgrading from 1.1: the old JSON cache file is replaced automatically. In 1.1, routes in the cache file could be dispatched without being registered. That skipped their middleware and kept deleted routes alive. This still works in 1.2 but triggers an E_USER_DEPRECATED notice, and it stops working in 2.0. Register your routes on every request.
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Dromos\Router;
use Dromos\Http\Request;
use Dromos\Http\Response;
use Dromos\Http\Emitter\Emitter;
use Dromos\Env\EnvLoader;
use Dromos\Middleware\CorsMiddleware;
use Dromos\Middleware\RateLimitMiddleware;
use Dromos\Middleware\AuthMiddleware;
use Dromos\Validation\Validator;
// Load environment
EnvLoader::load(__DIR__ . '/.env');
// Optional: enable route caching via ROUTER_CACHE_FILE env var
Router::initialize();
// Define routes
Router::group('/api', function ($api) {
// Public
$api->get('/health', function (Request $request, Response $response) {
return $response->json(['status' => 'ok']);
});
// Protected
$api->group('/v1', function ($v1) {
$v1->middleware(new AuthMiddleware(function ($token) {
return MyAuth::validate($token);
}));
$v1->get('/users', [UserController::class, 'index']);
$v1->post('/users', [UserController::class, 'store']);
});
});
// Boot
$router = new Router();
$router->addMiddleware(new CorsMiddleware());
$router->addMiddleware(new RateLimitMiddleware(100, 60));
$response = $router->handle(new Request());
(new Emitter())->emit($response);
Run the test suite and static analysis with:
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse
For unit testing, use Router::reset() between tests to clear all registered routes and middleware:
Router::reset();
RateLimitMiddleware::resetStore();
Use Request::create() to build requests without depending on PHP superglobals:
use Dromos\Http\Request;
$request = Request::create(
method: 'POST',
uri: '/api/users',
headers: ['Content-Type' => 'application/json'],
parsedBody: ['name' => 'Jane']
);
MIT