ghostwriter / router

[WIP] Router implementation for PHP
https://github.com/ghostwriter/router
BSD 3-Clause "New" or "Revised" License
0 stars 0 forks source link

Support Curly Brackets (`{}`) Syntax #1

Open ghostwriter opened 2 weeks ago

ghostwriter commented 2 weeks ago

Segment: A portion of a URL path that can contain dynamic or static content.

For example, in the URL https://{app}.example.com/users/{userId}/profile/{section}, {app}, /users and /profile are segments.

The segment {app} represents a dynamic part of the URL that can change based on user input.


Parameter: A variable in the URL that can represent different values.

Parameters can be used within segments to capture dynamic values or provide additional information.

For example, in the URL /search?q={query}, q is a query parameter that can be replaced with different search terms.


Name Syntax Description
Named Parameter /users/{userId} Matches a segment with a specific name, e.g., {userId}.
Regex Parameter /products/{productId:[0-9]+} Matches a segment using a regex pattern, e.g., {productId:[0-9]+}.
Wildcard Parameter /files/{path:*} Captures multiple segments, e.g., {path*}.
Optional Parameter /posts/{postId?} Matches a segment that is optional, e.g., {postId?}.
Query Parameter /search?q={query} Matches a query parameter in the URL, e.g., ?q={query}.
Matrix Parameter /items;color={color};size={size} Matches parameters in the matrix format, e.g., ;color={color};size={size}.
Static Segment /static/about Matches a static segment exactly, e.g., /static/about.
Dynamic Segment /users/{id}/profile/{section} Matches dynamic segments in the URL, e.g., /users/{id}/profile/{section}.
Subdomain Segment {tenant}.example.com Matches subdomains, e.g., {tenant}.example.com.
Hyphenated Segment /flights/{from}-{to} Matches hyphenated segments, e.g., /flights/{from}-{to}.
Dot-separated Segment /files/{category}.{filename} Matches dot-separated segments, e.g., /files/{category}.{filename}.
ghostwriter commented 2 weeks ago
<?php

declare(strict_types=1);

namespace Ghostwriter\Router\Interface;

use Closure;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

interface RouteCollectorInterface
{
    public function add(RouteInterface $route): void;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function any(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function delete(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param non-empty-string                       $name
     * @param Closure(RouteCollectorInterface): void $callback
     */
    public function domain(string $name, Closure $callback): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function get(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function head(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param list<class-string<MiddlewareInterface>> $middlewares
     * @param Closure(RouteCollectorInterface): void  $callback
     */
    public function middleware(array $middlewares, Closure $callback): self;

    /**
     * @param non-empty-string                       $name
     * @param Closure(RouteCollectorInterface): void $callback
     */
    public function name(string $name, Closure $callback): self;

    /**
     * @param non-empty-string                        $method
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function map(
        string $method,
        string $path,
        string $handler,
        string $name,
        array $middlewares = [],
    ): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function options(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function patch(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function post(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @param non-empty-string                      $path
     * @param Closure(RouteCollectorInterface):void $callback
     */
    public function prefix(string $path, Closure $callback): self;

    /**
     * @param non-empty-string                        $path
     * @param class-string<RequestHandlerInterface>   $handler
     * @param non-empty-string                        $name
     * @param list<class-string<MiddlewareInterface>> $middlewares
     */
    public function put(string $path, string $handler, string $name, array $middlewares = []): self;

    /**
     * @return array<non-empty-string,RouteInterface>
     */
    public function routes(): array;
}
ghostwriter commented 2 weeks ago
<?php

declare(strict_types=1);

namespace Ghostwriter\Router\Interface;

use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

interface RouteInterface
{
    /** @return non-empty-string */
    public function domain(): string;

    /** @return class-string<RequestHandlerInterface> */
    public function handler(): string;

    /** @return non-empty-string */
    public function method(): string;

    /** @return list<class-string<MiddlewareInterface>> */
    public function middlewares(): array;

    /** @return non-empty-string */
    public function name(): string;

    /** @return non-empty-string */
    public function path(): string;
}
ghostwriter commented 2 weeks ago
<?php

declare(strict_types=1);

namespace Ghostwriter\Router\Interface;

use Ghostwriter\Router\Interface\Exception\RouteMethodNotAllowedExceptionInterface;
use Ghostwriter\Router\Interface\Exception\RouteNotFoundExceptionInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;

interface RouterInterface
{
    public function add(RouteInterface $route): void;

    /**
     * @throws RouteNotFoundExceptionInterface
     * @throws RouteMethodNotAllowedExceptionInterface
     */
    public function match(ServerRequestInterface $serverRequest): RouteInterface;

    /**
     * @return array<non-empty-string,RouteInterface>
     */
    public function routes(): array;

    /**
     * Generate a URI from a named route.
     *
     * $router->add(Route::new('GET', '/post/{id}', 'PostHandler::class', 'post.show', ['AuthMiddleware::class'], 'localhost'))
     *
     * $router->uri('post.show', ['id' => 1], ['page' => 2], 'comments') => /post/1?page=2#comments
     *
     * @param array<string,scalar> $parameters
     * @param array<string,scalar> $query
     * 
     * @throws RouteNotFoundExceptionInterface
     */
    public function uri(
        string $name,
        array $parameters = [],
        array $query = [],
        string $fragment = ''
    ): UriInterface;
}
ghostwriter commented 2 weeks ago
final readonly class MethodOverrideMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        foreach (['X-HTTP-Method-Override', 'X-Method-Override', 'X-HTTP-Method', 'X-Method'] as $header) {
            if (! $request->hasHeader($header)) {
                continue;
            }
            return $handler->handle($request->withMethod(mb_strtoupper($request->getHeaderLine($header))));
        }

        $method = $request->getMethod();

        if ($method === 'POST') {
            $parsedBody = $request->getParsedBody();

            $method = $parsedBody['_method'] ?? $parsedBody['__METHOD__'] ?? $method;
        }

        // Continue processing with the updated request
        return $handler->handle($request->withMethod($method));
    }
}