summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSam Light <sam@lightscale.co.uk>2026-06-10 19:00:32 +0100
committerSam Light <sam@lightscale.co.uk>2026-06-10 19:00:32 +0100
commit5fe7c87967ff29c4a8f03a9186918d8359f4887e (patch)
treedfdd4fdc7a4e96266305f82f5846750ab2efabc9 /src
parent01eac9658c3bc486d2d42a18557fdb82a536348e (diff)
big update
Diffstat (limited to 'src')
-rw-r--r--src/Enums/PathSegmentType.php (renamed from src/Enums/SpecialSegment.php)6
-rw-r--r--src/Exceptions/NotFoundException.php12
-rw-r--r--src/Exceptions/UnknownMethodException.php12
-rw-r--r--src/PathSegment.php123
-rw-r--r--src/PathSegmentMatch.php15
-rw-r--r--src/Route.php34
-rw-r--r--src/Router.php71
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);
+ }
}