mirror of
https://github.com/notrab/dumbo.git
synced 2025-01-17 06:08:31 +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();
|
$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
|
* @author Jamie Barton
|
||||||
* @version 1.0.0
|
* @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 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 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
|
* @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
|
public function __call(string $method, array $arguments): void
|
||||||
{
|
{
|
||||||
$supportedMethods = [
|
$supportedMethods = [
|
||||||
|
"all",
|
||||||
"get",
|
"get",
|
||||||
"post",
|
"post",
|
||||||
"put",
|
"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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
212
src/Router.php
212
src/Router.php
@ -2,45 +2,23 @@
|
|||||||
|
|
||||||
namespace Dumbo;
|
namespace Dumbo;
|
||||||
|
|
||||||
use FastRoute\Dispatcher;
|
|
||||||
use FastRoute\RouteCollector;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
|
||||||
* Router class for handling HTTP routes in the Dumbo framework
|
|
||||||
*/
|
|
||||||
class Router
|
class Router
|
||||||
{
|
{
|
||||||
private ?Dispatcher $dispatcher = null;
|
|
||||||
|
|
||||||
/** @var array<array{method: string, path: string, handler: callable(Context): (ResponseInterface|null), middleware: array}> */
|
/** @var array<array{method: string, path: string, handler: callable(Context): (ResponseInterface|null), middleware: array}> */
|
||||||
private $routes = [];
|
private $routes = [];
|
||||||
|
|
||||||
/**
|
/** @var string */
|
||||||
* Array of route groups
|
private $prefix = "";
|
||||||
*
|
|
||||||
* @var array<string, array>
|
|
||||||
*/
|
|
||||||
private array $groups = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* Initializes the router and invokes the initial dispatcher.
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->rebuildDispatcher();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a route to the router
|
* Add a route to the router
|
||||||
*
|
*
|
||||||
* @param string $method The HTTP method for the route
|
* @param string $method The HTTP method for this route
|
||||||
* @param string $path The URL path for the route
|
* @param string $path The path for this route
|
||||||
* @param callable $handler The handler function for the route
|
* @param callable(Context): (ResponseInterface|null) $handler The handler function for this route
|
||||||
* @param array<callable> $middleware Array of middleware functions for the route
|
* @param array $middleware Array of middleware functions for this route
|
||||||
*/
|
*/
|
||||||
public function addRoute(
|
public function addRoute(
|
||||||
string $method,
|
string $method,
|
||||||
@ -50,110 +28,118 @@ class Router
|
|||||||
): void {
|
): void {
|
||||||
$this->routes[] = [
|
$this->routes[] = [
|
||||||
"method" => $method,
|
"method" => $method,
|
||||||
"path" => $path,
|
"path" => $this->prefix . $path,
|
||||||
"handler" => $handler,
|
"handler" => $handler,
|
||||||
"middleware" => $middleware,
|
"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
|
public function findRoute(ServerRequestInterface $request): ?array
|
||||||
{
|
{
|
||||||
if (!$this->dispatcher) {
|
$method = $request->getMethod();
|
||||||
return null;
|
$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];
|
|
||||||
|
|
||||||
|
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 [
|
return [
|
||||||
"handler" => $handler["handler"],
|
"handler" => $route["handler"],
|
||||||
"params" => $vars,
|
"params" => $params,
|
||||||
"routePath" => $handler["path"],
|
"routePath" => $route["path"],
|
||||||
"middleware" => $handler["middleware"] ?? [],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
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
|
||||||
*/
|
*/
|
||||||
|
private function matchPath(string $routePath, string $requestPath): bool
|
||||||
|
{
|
||||||
|
$routePath = trim($routePath, "/");
|
||||||
|
$requestPath = trim($requestPath, "/");
|
||||||
|
|
||||||
|
if ($routePath === "" && $requestPath === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$routeParts = $routePath ? explode("/", $routePath) : [];
|
||||||
|
$requestParts = $requestPath ? explode("/", $requestPath) : [];
|
||||||
|
|
||||||
|
if (count($routeParts) !== count($requestParts)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($routeParts as $index => $routePart) {
|
||||||
|
if ($routePart[0] === ":") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($routePart !== $requestParts[$index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract parameters from the request path based on the route path
|
||||||
|
*
|
||||||
|
* 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 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
|
||||||
|
{
|
||||||
|
$this->prefix = $prefix;
|
||||||
|
}
|
||||||
|
|
||||||
public function getRoutes(): array
|
public function getRoutes(): array
|
||||||
{
|
{
|
||||||
$allRoutes = $this->routes;
|
return $this->routes;
|
||||||
|
|
||||||
foreach ($this->groups as $groupRoutes) {
|
|
||||||
$allRoutes = array_merge($allRoutes, $groupRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $allRoutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize the given URI by ensuring it starts with a forward slash
|
|
||||||
*
|
|
||||||
* @param string $uri The URI to normalize
|
|
||||||
* @return string The normalized URI
|
|
||||||
*/
|
|
||||||
private function normalizeUri(string $uri): string
|
|
||||||
{
|
|
||||||
return "/" . trim($uri, "/");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user