diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Enums/PathSegmentType.php (renamed from src/Enums/SpecialSegment.php) | 6 | ||||
| -rw-r--r-- | src/Exceptions/NotFoundException.php | 12 | ||||
| -rw-r--r-- | src/Exceptions/UnknownMethodException.php | 12 | ||||
| -rw-r--r-- | src/PathSegment.php | 123 | ||||
| -rw-r--r-- | src/PathSegmentMatch.php | 15 | ||||
| -rw-r--r-- | src/Route.php | 34 | ||||
| -rw-r--r-- | src/Router.php | 71 |
7 files changed, 254 insertions, 19 deletions
diff --git a/src/Enums/SpecialSegment.php b/src/Enums/PathSegmentType.php index 6b451c5..6547842 100644 --- a/src/Enums/SpecialSegment.php +++ b/src/Enums/PathSegmentType.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Lightscale\Router\Enums; -enum SpecialSegment: string +enum PathSegmentType { - case Root = '<root>'; + case Raw; + case Root; + case Parameter; } diff --git a/src/Exceptions/NotFoundException.php b/src/Exceptions/NotFoundException.php new file mode 100644 index 0000000..306ba15 --- /dev/null +++ b/src/Exceptions/NotFoundException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Lightscale\Router\Exceptions; + +use RuntimeException; + +class NotFoundException extends RuntimeException +{ + protected $message = 'Could not find request route'; +} diff --git a/src/Exceptions/UnknownMethodException.php b/src/Exceptions/UnknownMethodException.php new file mode 100644 index 0000000..220cf42 --- /dev/null +++ b/src/Exceptions/UnknownMethodException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Lightscale\Router\Exceptions; + +use RuntimeException; + +class UnknownMethodException extends RuntimeException +{ + protected $message = 'Unknown HTTP request method'; +} diff --git a/src/PathSegment.php b/src/PathSegment.php index 7f24660..5333d17 100644 --- a/src/PathSegment.php +++ b/src/PathSegment.php @@ -5,37 +5,66 @@ declare(strict_types=1); namespace Lightscale\Router; use Lightscale\Router\Enums\HttpMethod; -use Lightscale\Router\Enums\SpecialSegment; +use Lightscale\Router\Enums\PathSegmentType; class PathSegment { private const MAX_ANCESTORY_DEPTH = 100; - protected string $content; - - protected ?SpecialSegment $specialSegment = null; - /** @var array<string, self> */ protected array $children; - /** @var array<value-of<HttpMethod>, Route[]> */ + /** @var array<value-of<HttpMethod>, Route> */ protected array $routes = []; + protected ?self $parent = null; + public function __construct( - SpecialSegment|string $content, - protected ?self $parent = null, + protected ?string $value = null, + protected PathSegmentType $type = PathSegmentType::Raw, ) { - if ($content instanceof SpecialSegment) { - $this->content = $content->value; - $this->specialSegment = $content; - } else { - $this->content = $content; + if (PathSegmentType::Root === $type) { + $this->value = null; } } public function getContent(): string { - return $this->content; + return match ($this->type) { + PathSegmentType::Root => '', + default => $this->value ?? '', + }; + } + + public function getValue(): ?string + { + return $this->value; + } + + private static function createKey( + ?string $value, + PathSegmentType $type, + ): string { + return match ($type) { + PathSegmentType::Raw => $value ?? '', + PathSegmentType::Root => '<root>', + PathSegmentType::Parameter => '<parameter>', + }; + } + + public function getKey(): string + { + return self::createKey($this->value, $this->type); + } + + public function getType(): PathSegmentType + { + return $this->type; + } + + public function setParent(?self $segment): void + { + $this->parent = $segment; } public function getParent(): ?self @@ -72,7 +101,8 @@ class PathSegment public function addChild(self $segment): void { - $this->children[$segment->getContent()] = $segment; + $segment->setParent($this); + $this->children[$segment->getKey()] = $segment; } /** @return array<string, self> */ @@ -81,7 +111,68 @@ class PathSegment return $this->children; } - public function addRoute(): void + public function getChildrenCount(): int + { + return count($this->children); + } + + public function findChild( + ?string $value = null, + ): ?PathSegment { + $key = self::createKey($value, PathSegmentType::Raw); + $seg = $this->children[$key] ?? null; + + if ($seg instanceof self) { + return $seg; + } + + $key = self::createKey($value, PathSegmentType::Parameter); + $seg = $this->children[$key] ?? null; + + return $seg; + } + + public function child( + ?string $value = null, + PathSegmentType $type = PathSegmentType::Raw, + ): self { + $key = self::createKey($value, $type); + $seg = $this->children[$key] ?? null; + + if (null === $seg) { + $seg = new PathSegment($value, $type); + } + + $this->addChild($seg); + + return $seg; + } + + public function getPath(): string + { + $parts = array_map( + fn ($seg) => $seg->getContent(), + $this->getAncestorsAndSelf() + ); + + return implode('/', $parts); + } + + public function addRoute(Route $route): void + { + $route->setSegment($this); + $method = $route->getMethod()->value; + $this->routes[$method] = $route; + } + + public function getRoute(HttpMethod $method): ?Route + { + return $this->routes[$method->value] ?? null; + } + + /** @return array<value-of<HttpMethod>, Route> */ + public function getRoutes(): array { + return $this->routes; } } diff --git a/src/PathSegmentMatch.php b/src/PathSegmentMatch.php new file mode 100644 index 0000000..738b4a5 --- /dev/null +++ b/src/PathSegmentMatch.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace Lightscale\Router; + +readonly class PathSegmentMatch +{ + /** @param array<string, string> $parameters */ + public function __construct( + public PathSegment $segment, + public array $parameters, + ) { + } +} diff --git a/src/Route.php b/src/Route.php index 1a9ad03..aeb3f66 100644 --- a/src/Route.php +++ b/src/Route.php @@ -6,16 +6,48 @@ namespace Lightscale\Router; use Closure; use Lightscale\Router\Enums\HttpMethod; +use Psr\Http\Message\RequestInterface; class Route { protected Closure $handler; + protected PathSegment $segment; + public function __construct( - protected PathSegment $segment, protected HttpMethod $method, callable $handler, + ?PathSegment $segment = null, ) { $this->handler = Closure::fromCallable($handler); + + if (null !== $segment) { + $this->segment = $segment; + } + } + + public function __invoke(RequestInterface $request): mixed + { + return ($this->handler)($request); + } + + public function getMethod(): HttpMethod + { + return $this->method; + } + + public function setSegment(PathSegment $segment): void + { + $this->segment = $segment; + } + + public function getSegment(): PathSegment + { + return $this->segment; + } + + public function getPath(): string + { + return $this->segment->getPath(); } } diff --git a/src/Router.php b/src/Router.php index efcccfa..f666fe5 100644 --- a/src/Router.php +++ b/src/Router.php @@ -4,6 +4,77 @@ declare(strict_types=1); namespace Lightscale\Router; +use Lightscale\Router\Enums\HttpMethod; +use Lightscale\Router\Enums\PathSegmentType; +use Lightscale\Router\Exceptions\NotFoundException; +use Lightscale\Router\Exceptions\UnknownMethodException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + class Router { + private PathSegment $root; + + public function __construct() + { + $this->root = new PathSegment(type: PathSegmentType::Root); + } + + public function root(): PathSegment + { + return $this->root; + } + + /** @return string[] */ + private function splitPath(string $path): array + { + $split = explode('/', rtrim($path, '/')); + array_shift($split); + + return $split; + } + + public function findSegment(string $path): ?PathSegmentMatch + { + $pathSplit = $this->splitPath($path); + $seg = $this->root(); + $params = []; + + while (($v = array_shift($pathSplit)) !== null && null !== $seg) { + $seg = $seg->findChild($v); + + if (PathSegmentType::Parameter === $seg?->getType()) { + $params[$seg->getValue() ?? ''] = $v; + } + } + + return null === $seg ? null : new PathSegmentMatch( + segment: $seg, + parameters: $params + ); + } + + public function dispatch(RequestInterface $request): ResponseInterface + { + $uri = $request->getUri(); + $match = $this->findSegment($uri->getPath()); + + if (null === $match) { + throw new NotFoundException(); + } + + $segment = $match->segment; + $method = $request->getMethod(); + $method = ( + HttpMethod::tryFrom(strtolower($method)) ?? + throw new UnknownMethodException() + ); + $route = $segment->getRoute($method); + + if (null === $route) { + throw new NotFoundException(); + } + + return $route($request); + } } |
