mirror of
https://github.com/notrab/dumbo.git
synced 2025-01-16 13:50:03 +01:00
feat: add cors middleware helper, add all
route handler
This commit is contained in:
parent
f1d733c37d
commit
11b85823fa
46
README.md
46
README.md
@ -133,3 +133,49 @@ $app->get('/api/data', function(Context $context) {
|
||||
|
||||
$app->run();
|
||||
```
|
||||
|
||||
### CORS
|
||||
|
||||
Usage:
|
||||
|
||||
```php
|
||||
use Dumbo\Helpers\CORS;
|
||||
|
||||
$app->use(CORS::cors()); // allow from all origin
|
||||
// or
|
||||
$app->use(CORS::cors([
|
||||
'origin' => fn($origin, $c) => $origin,
|
||||
'allow_headers' => ['X-Custom-Header', 'Upgrade-Insecure-Requests'],
|
||||
'allow_methods' => ['POST', 'GET', 'OPTIONS'],
|
||||
'expose_headers' => ['Content-Length', 'X-Kuma-Revision'],
|
||||
'max_age' => 600,
|
||||
'credentials' => true,
|
||||
]));
|
||||
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- **origin**: `string` | `string[]` | `callable(string, Context): string`
|
||||
|
||||
The value of "_Access-Control-Allow-Origin_" CORS header. You can also pass the callback function like `'origin': fn($origin, $c) => (str_ends_with($origin, '.example.com') ? $origin : 'http://example.com')`. The default is `*`.
|
||||
|
||||
- **allow_headers**: `string[]`
|
||||
|
||||
The value of "_Access-Control-Allow-Headers_" CORS header. The default is `[]`.
|
||||
|
||||
- **allow_methods**: `string[]`
|
||||
|
||||
The value of "_Access-Control-Allow-Methods_" CORS header. The default is `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']`.
|
||||
|
||||
- **expose_headers**: `string[]`
|
||||
|
||||
The value of "_Access-Control-Expose-Headers_" CORS header.
|
||||
|
||||
- **credentials**: `bool`
|
||||
|
||||
The value of "_Access-Control-Allow-Credentials_" CORS header.
|
||||
|
||||
- **max_age**: `int`
|
||||
|
||||
The value of "_Access-Control-Max-Age_" CORS header.
|
||||
|
@ -14,6 +14,7 @@ use GuzzleHttp\Psr7\ServerRequest;
|
||||
* @author Jamie Barton
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @method void all(string $path, callable(Context): (ResponseInterface|null) $handler) Add a catch-all method route
|
||||
* @method void get(string $path, callable(Context): (ResponseInterface|null) $handler) Add a GET route
|
||||
* @method void post(string $path, callable(Context): (ResponseInterface|null) $handler) Add a POST route
|
||||
* @method void put(string $path, callable(Context): (ResponseInterface|null) $handler) Add a PUT route
|
||||
@ -53,6 +54,7 @@ class Dumbo
|
||||
public function __call(string $method, array $arguments): void
|
||||
{
|
||||
$supportedMethods = [
|
||||
"all",
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
|
69
src/Helpers/CORS.php
Normal file
69
src/Helpers/CORS.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Dumbo\Helpers;
|
||||
|
||||
use Dumbo\Context;
|
||||
|
||||
class CORS
|
||||
{
|
||||
/**
|
||||
* Configure CORS headers.
|
||||
*
|
||||
* @param array $opts optional - The CORS options
|
||||
* @return callable The middleware function for handling CORS.
|
||||
*/
|
||||
public static function cors(array $opts = []): callable
|
||||
{
|
||||
return function (Context $c, callable $next) use ($opts) {
|
||||
$origin = $c->req->header('origin');
|
||||
|
||||
$allowOrigin = $opts['origin'] ?? '*';
|
||||
$allowMethods = $opts['allow_methods'] ?? ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'];
|
||||
|
||||
if (is_callable($allowOrigin)) {
|
||||
$allowOrigin = $allowOrigin($origin, $c);
|
||||
} else if (is_array($allowOrigin) && array_is_list($allowOrigin)) {
|
||||
$allowOrigin = in_array($origin, $allowOrigin) ? $origin : $allowOrigin[0];
|
||||
}
|
||||
|
||||
if (!empty($allowOrigin)) {
|
||||
$c->header('Access-Control-Allow-Origin', $allowOrigin);
|
||||
}
|
||||
|
||||
if ($opts['credentials']) {
|
||||
$c->header('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
if (!empty($opts['expose_headers'])) {
|
||||
$c->header('Access-Control-Expose-Headers', implode(', ', $opts['expose_headers']));
|
||||
}
|
||||
|
||||
if ($c->req->method() === 'OPTIONS') {
|
||||
if (!empty($opts['max_age'])) {
|
||||
$c->header('Access-Control-Max-Age', (string) $opts['max_age']);
|
||||
}
|
||||
|
||||
if (!empty($allowMethods)) {
|
||||
$c->header('Access-Control-Allow-Methods', implode(', ', $allowMethods));
|
||||
}
|
||||
|
||||
$headers = $opts['allow_headers'];
|
||||
if (empty($headers)) {
|
||||
$reqHeaders = $c->req->header('Access-Control-Request-Headers');
|
||||
if (!empty($reqHeaders)) {
|
||||
$headers = array_map('trim', explode(',', $reqHeaders));
|
||||
}
|
||||
}
|
||||
if (!empty($headers)) {
|
||||
$c->header('Access-Control-Allow-Headers', implode(', ', $opts['allow_headers']));
|
||||
}
|
||||
|
||||
return $c->getResponse()
|
||||
->withStatus(204)
|
||||
->withoutHeader('Content-Length')
|
||||
->withoutHeader('Content-Type');
|
||||
}
|
||||
|
||||
return $next($c);
|
||||
};
|
||||
}
|
||||
}
|
194
src/Router.php
194
src/Router.php
@ -2,45 +2,23 @@
|
||||
|
||||
namespace Dumbo;
|
||||
|
||||
use FastRoute\Dispatcher;
|
||||
use FastRoute\RouteCollector;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Router class for handling HTTP routes in the Dumbo framework
|
||||
*/
|
||||
class Router
|
||||
{
|
||||
private ?Dispatcher $dispatcher = null;
|
||||
|
||||
/** @var array<array{method: string, path: string, handler: callable(Context): (ResponseInterface|null), middleware: array}> */
|
||||
private $routes = [];
|
||||
|
||||
/**
|
||||
* Array of route groups
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
private array $groups = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Initializes the router and invokes the initial dispatcher.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->rebuildDispatcher();
|
||||
}
|
||||
/** @var string */
|
||||
private $prefix = "";
|
||||
|
||||
/**
|
||||
* Add a route to the router
|
||||
*
|
||||
* @param string $method The HTTP method for the route
|
||||
* @param string $path The URL path for the route
|
||||
* @param callable $handler The handler function for the route
|
||||
* @param array<callable> $middleware Array of middleware functions for the route
|
||||
* @param string $method The HTTP method for this route
|
||||
* @param string $path The path for this route
|
||||
* @param callable(Context): (ResponseInterface|null) $handler The handler function for this route
|
||||
* @param array $middleware Array of middleware functions for this route
|
||||
*/
|
||||
public function addRoute(
|
||||
string $method,
|
||||
@ -50,110 +28,118 @@ class Router
|
||||
): void {
|
||||
$this->routes[] = [
|
||||
"method" => $method,
|
||||
"path" => $path,
|
||||
"path" => $this->prefix . $path,
|
||||
"handler" => $handler,
|
||||
"middleware" => $middleware,
|
||||
];
|
||||
$this->rebuildDispatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a group of routes with a common prefix
|
||||
*
|
||||
* @param string $prefix The common prefix for the group of routes
|
||||
* @param array $groupRoutes Array of routes in the group
|
||||
*/
|
||||
public function addGroup(string $prefix, array $groupRoutes): void
|
||||
{
|
||||
$this->groups[$prefix] = $groupRoutes;
|
||||
$this->rebuildDispatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a matching route for the given request
|
||||
*
|
||||
* @param ServerRequestInterface $request The incoming HTTP request
|
||||
* @return array|null The matched route information or null if no match found
|
||||
*/
|
||||
public function findRoute(ServerRequestInterface $request): ?array
|
||||
{
|
||||
if (!$this->dispatcher) {
|
||||
return null;
|
||||
}
|
||||
$method = $request->getMethod();
|
||||
$path = $request->getUri()->getPath();
|
||||
|
||||
$httpMethod = $request->getMethod();
|
||||
$uri = $this->normalizeUri($request->getUri()->getPath());
|
||||
|
||||
$routeInfo = $this->dispatcher->dispatch($httpMethod, $uri);
|
||||
|
||||
if ($routeInfo[0] === Dispatcher::FOUND) {
|
||||
$handler = $routeInfo[1];
|
||||
$vars = $routeInfo[2];
|
||||
|
||||
return [
|
||||
"handler" => $handler["handler"],
|
||||
"params" => $vars,
|
||||
"routePath" => $handler["path"],
|
||||
"middleware" => $handler["middleware"] ?? [],
|
||||
];
|
||||
foreach ($this->routes as $route) {
|
||||
if (
|
||||
($route["method"] === $method ||
|
||||
$route["method"] === "ALL" ||
|
||||
$method === "OPTIONS") &&
|
||||
$this->matchPath($route["path"], $path)
|
||||
) {
|
||||
$params = $this->extractParams($route["path"], $path);
|
||||
return [
|
||||
"handler" => $route["handler"],
|
||||
"params" => $params,
|
||||
"routePath" => $route["path"],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all declared routes
|
||||
* Check if a route path matches the request path
|
||||
*
|
||||
* @return array All registered routes including group routes
|
||||
* This method compares the route path pattern with the actual request path,
|
||||
* accounting for path parameters (e.g., ":id" in "/users/:id").
|
||||
*
|
||||
* @param string $routePath The route path pattern
|
||||
* @param string $requestPath The actual request path
|
||||
* @return bool True if the paths match, false otherwise
|
||||
*/
|
||||
public function getRoutes(): array
|
||||
private function matchPath(string $routePath, string $requestPath): bool
|
||||
{
|
||||
$allRoutes = $this->routes;
|
||||
$routePath = trim($routePath, "/");
|
||||
$requestPath = trim($requestPath, "/");
|
||||
|
||||
foreach ($this->groups as $groupRoutes) {
|
||||
$allRoutes = array_merge($allRoutes, $groupRoutes);
|
||||
if ($routePath === "" && $requestPath === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $allRoutes;
|
||||
}
|
||||
$routeParts = $routePath ? explode("/", $routePath) : [];
|
||||
$requestParts = $requestPath ? explode("/", $requestPath) : [];
|
||||
|
||||
/**
|
||||
* Prepare the path by converting :parameter syntax to {parameter}
|
||||
*
|
||||
* @param string $path The route path to prepare
|
||||
* @return string The prepared path
|
||||
*/
|
||||
private function preparePath(string $path): string
|
||||
{
|
||||
$path = preg_replace("/:(\w+)/", '{$1}', $path);
|
||||
return $this->normalizeUri($path);
|
||||
}
|
||||
if (count($routeParts) !== count($requestParts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the FastRoute dispatcher
|
||||
*
|
||||
* This method is called whenever routes are added or modified.
|
||||
*/
|
||||
private function rebuildDispatcher(): void
|
||||
{
|
||||
$this->dispatcher = \FastRoute\simpleDispatcher(function (
|
||||
RouteCollector $r
|
||||
) {
|
||||
foreach ($this->routes as $route) {
|
||||
$path = $this->preparePath($route["path"]);
|
||||
$r->addRoute($route["method"], $path, $route);
|
||||
foreach ($routeParts as $index => $routePart) {
|
||||
if ($routePart[0] === ":") {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
if ($routePart !== $requestParts[$index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the given URI by ensuring it starts with a forward slash
|
||||
* Extract parameters from the request path based on the route path
|
||||
*
|
||||
* @param string $uri The URI to normalize
|
||||
* @return string The normalized URI
|
||||
* This method extracts values for path parameters defined in the route path
|
||||
* (e.g., it will extract "123" as the value for ":id" from "/users/123" if the
|
||||
* route path is "/users/:id").
|
||||
*
|
||||
* @param string $routePath The route path pattern
|
||||
* @param string $requestPath The actual request path
|
||||
* @return array<string, string|null> An associative array of parameter names and their values
|
||||
*/
|
||||
private function normalizeUri(string $uri): string
|
||||
private function extractParams(
|
||||
string $routePath,
|
||||
string $requestPath
|
||||
): array {
|
||||
$params = [];
|
||||
|
||||
$routePath = trim($routePath, "/");
|
||||
$requestPath = trim($requestPath, "/");
|
||||
|
||||
if ($routePath === "" && $requestPath === "") {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$routeParts = $routePath ? explode("/", $routePath) : [];
|
||||
$requestParts = $requestPath ? explode("/", $requestPath) : [];
|
||||
|
||||
foreach ($routeParts as $index => $routePart) {
|
||||
if ($routePart[0] === ":") {
|
||||
$params[substr($routePart, 1)] = $requestParts[$index] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function setPrefix(string $prefix): void
|
||||
{
|
||||
return "/" . trim($uri, "/");
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
public function getRoutes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user