diff --git a/src/Commands/ListRoutesCommand.php b/src/Commands/ListRoutesCommand.php index 9647228..36dd543 100644 --- a/src/Commands/ListRoutesCommand.php +++ b/src/Commands/ListRoutesCommand.php @@ -21,7 +21,7 @@ class ListRoutesCommand $json = in_array('--json', $argv); $routes = $router->getRoutes(); $output = []; - + foreach ($routes as $route) { $output[] = $route->toArray(); } @@ -32,10 +32,11 @@ class ListRoutesCommand printf("%-10s | %-30s | %-20s | %-30s\n", "Method", "Path", "Name", "Handler"); echo str_repeat("-", 100) . PHP_EOL; foreach ($output as $r) { - printf("%-10s | %-30s | %-20s | %-30s\n", - $r['method'], - $r['path'], - $r['name'] ?? '', + printf( + "%-10s | %-30s | %-20s | %-30s\n", + $r['method'], + $r['path'], + $r['name'] ?? '', is_string($r['handler']) ? $r['handler'] : 'Closure' ); } diff --git a/src/Commands/TestRouteCommand.php b/src/Commands/TestRouteCommand.php index aa61a4b..33443a0 100644 --- a/src/Commands/TestRouteCommand.php +++ b/src/Commands/TestRouteCommand.php @@ -25,76 +25,261 @@ class TestRouteCommand $host = 'localhost'; // Default // PSR-7 mock request - $uri = new class($path, $host) implements UriInterface { - public function __construct(private $path, private $host) {} - public function getScheme(): string { return 'http'; } - public function getAuthority(): string { return $this->host; } - public function getUserInfo(): string { return ''; } - public function getHost(): string { return $this->host; } - public function getPort(): ?int { return null; } - public function getPath(): string { return $this->path; } - public function getQuery(): string { return ''; } - public function getFragment(): string { return ''; } - public function withScheme($scheme): UriInterface { return $this; } - public function withUserInfo($user, $password = null): UriInterface { return $this; } - public function withHost($host): UriInterface { return $this; } - public function withPort($port): UriInterface { return $this; } - public function withPath($path): UriInterface { return $this; } - public function withQuery($query): UriInterface { return $this; } - public function withFragment($fragment): UriInterface { return $this; } - public function __toString(): string { return "http://{$this->host}{$this->path}"; } + $uri = new class ($path, $host) implements UriInterface { + public function __construct(private $path, private $host) + { + } + public function getScheme(): string + { + return 'http'; + } + public function getAuthority(): string + { + return $this->host; + } + public function getUserInfo(): string + { + return ''; + } + public function getHost(): string + { + return $this->host; + } + public function getPort(): ?int + { + return null; + } + public function getPath(): string + { + return $this->path; + } + public function getQuery(): string + { + return ''; + } + public function getFragment(): string + { + return ''; + } + public function withScheme($scheme): UriInterface + { + return $this; + } + public function withUserInfo($user, $password = null): UriInterface + { + return $this; + } + public function withHost($host): UriInterface + { + return $this; + } + public function withPort($port): UriInterface + { + return $this; + } + public function withPath($path): UriInterface + { + return $this; + } + public function withQuery($query): UriInterface + { + return $this; + } + public function withFragment($fragment): UriInterface + { + return $this; + } + public function __toString(): string + { + return "http://{$this->host}{$this->path}"; + } }; - $request = new class($method, $uri) implements ServerRequestInterface { - public function __construct(private $method, private $uri) {} - public function getProtocolVersion(): string { return '1.1'; } - public function withProtocolVersion($version): ServerRequestInterface { return $this; } - public function getHeaders(): array { return []; } - public function hasHeader($name): bool { return false; } - public function getHeader($name): array { return []; } - public function getHeaderLine($name): string { return ''; } - public function withHeader($name, $value): ServerRequestInterface { return $this; } - public function withAddedHeader($name, $value): ServerRequestInterface { return $this; } - public function withoutHeader($name): ServerRequestInterface { return $this; } - public function getBody(): \Psr\Http\Message\StreamInterface { return $this->createMockStream(); } - public function withBody(\Psr\Http\Message\StreamInterface $body): ServerRequestInterface { return $this; } - public function getRequestTarget(): string { return $this->uri->getPath(); } - public function withRequestTarget($requestTarget): ServerRequestInterface { return $this; } - public function getMethod(): string { return $this->method; } - public function withMethod($method): ServerRequestInterface { return $this; } - public function getUri(): UriInterface { return $this->uri; } - public function withUri(UriInterface $uri, $preserveHost = false): ServerRequestInterface { return $this; } - public function getServerParams(): array { return []; } - public function getCookieParams(): array { return []; } - public function withCookieParams(array $cookies): ServerRequestInterface { return $this; } - public function getQueryParams(): array { return []; } - public function withQueryParams(array $query): ServerRequestInterface { return $this; } - public function getUploadedFiles(): array { return []; } - public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface { return $this; } - public function getParsedBody(): null|array|object { return null; } - public function withParsedBody($data): ServerRequestInterface { return $this; } - public function getAttributes(): array { return []; } - public function getAttribute($name, $default = null): mixed { return $default; } - public function withAttribute($name, $value): ServerRequestInterface { return $this; } - public function withoutAttribute($name): ServerRequestInterface { return $this; } + $request = new class ($method, $uri) implements ServerRequestInterface { + public function __construct(private $method, private $uri) + { + } + public function getProtocolVersion(): string + { + return '1.1'; + } + public function withProtocolVersion($version): ServerRequestInterface + { + return $this; + } + public function getHeaders(): array + { + return []; + } + public function hasHeader($name): bool + { + return false; + } + public function getHeader($name): array + { + return []; + } + public function getHeaderLine($name): string + { + return ''; + } + public function withHeader($name, $value): ServerRequestInterface + { + return $this; + } + public function withAddedHeader($name, $value): ServerRequestInterface + { + return $this; + } + public function withoutHeader($name): ServerRequestInterface + { + return $this; + } + public function getBody(): \Psr\Http\Message\StreamInterface + { + return $this->createMockStream(); + } + public function withBody(\Psr\Http\Message\StreamInterface $body): ServerRequestInterface + { + return $this; + } + public function getRequestTarget(): string + { + return $this->uri->getPath(); + } + public function withRequestTarget($requestTarget): ServerRequestInterface + { + return $this; + } + public function getMethod(): string + { + return $this->method; + } + public function withMethod($method): ServerRequestInterface + { + return $this; + } + public function getUri(): UriInterface + { + return $this->uri; + } + public function withUri(UriInterface $uri, $preserveHost = false): ServerRequestInterface + { + return $this; + } + public function getServerParams(): array + { + return []; + } + public function getCookieParams(): array + { + return []; + } + public function withCookieParams(array $cookies): ServerRequestInterface + { + return $this; + } + public function getQueryParams(): array + { + return []; + } + public function withQueryParams(array $query): ServerRequestInterface + { + return $this; + } + public function getUploadedFiles(): array + { + return []; + } + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + return $this; + } + public function getParsedBody(): null|array|object + { + return null; + } + public function withParsedBody($data): ServerRequestInterface + { + return $this; + } + public function getAttributes(): array + { + return []; + } + public function getAttribute($name, $default = null): mixed + { + return $default; + } + public function withAttribute($name, $value): ServerRequestInterface + { + return $this; + } + public function withoutAttribute($name): ServerRequestInterface + { + return $this; + } - private function createMockStream() { + private function createMockStream() + { return new class implements \Psr\Http\Message\StreamInterface { - public function __toString(): string { return ''; } - public function close(): void {} - public function detach() { return null; } - public function getSize(): ?int { return 0; } - public function tell(): int { return 0; } - public function eof(): bool { return true; } - public function isSeekable(): bool { return false; } - public function seek($offset, $whence = SEEK_SET): void {} - public function rewind(): void {} - public function isWritable(): bool { return false; } - public function write($string): int { return 0; } - public function isReadable(): bool { return true; } - public function read($length): string { return ''; } - public function getContents(): string { return ''; } - public function getMetadata($key = null) { return $key ? null : []; } + public function __toString(): string + { + return ''; + } + public function close(): void + { + } + public function detach() + { + return null; + } + public function getSize(): ?int + { + return 0; + } + public function tell(): int + { + return 0; + } + public function eof(): bool + { + return true; + } + public function isSeekable(): bool + { + return false; + } + public function seek($offset, $whence = SEEK_SET): void + { + } + public function rewind(): void + { + } + public function isWritable(): bool + { + return false; + } + public function write($string): int + { + return 0; + } + public function isReadable(): bool + { + return true; + } + public function read($length): string + { + return ''; + } + public function getContents(): string + { + return ''; + } + public function getMetadata($key = null) + { + return $key ? null : []; + } }; } }; diff --git a/src/Config/Config.php b/src/Config/Config.php index 73766a8..bee6916 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -23,7 +23,8 @@ class Config implements ArrayAccess, IteratorAggregate */ public function __construct( private array $options - ) {} + ) { + } /** * Retrieves a configuration value by key. @@ -166,4 +167,4 @@ class Config implements ArrayAccess, IteratorAggregate { return new \ArrayIterator($this->options); } -} \ No newline at end of file +} diff --git a/src/Exception/MissingConfigurationException.php b/src/Exception/MissingConfigurationException.php index 6ccdeba..1643568 100644 --- a/src/Exception/MissingConfigurationException.php +++ b/src/Exception/MissingConfigurationException.php @@ -7,4 +7,4 @@ namespace Atlas\Exception; */ class MissingConfigurationException extends \RuntimeException { -} \ No newline at end of file +} diff --git a/src/Exception/RouteValidationException.php b/src/Exception/RouteValidationException.php index cfb9b6e..0cdb9da 100644 --- a/src/Exception/RouteValidationException.php +++ b/src/Exception/RouteValidationException.php @@ -7,4 +7,4 @@ namespace Atlas\Exception; */ class RouteValidationException extends \RuntimeException { -} \ No newline at end of file +} diff --git a/src/Router/MatchResult.php b/src/Router/MatchResult.php index cd1d61f..d32831a 100644 --- a/src/Router/MatchResult.php +++ b/src/Router/MatchResult.php @@ -12,7 +12,8 @@ class MatchResult implements \JsonSerializable private readonly RouteDefinition|null $route = null, private readonly array $parameters = [], private readonly array $diagnostics = [] - ) {} + ) { + } public function isFound(): bool { diff --git a/src/Router/ModuleLoader.php b/src/Router/ModuleLoader.php index 00b2be8..f5f1cbe 100644 --- a/src/Router/ModuleLoader.php +++ b/src/Router/ModuleLoader.php @@ -19,7 +19,8 @@ class ModuleLoader public function __construct( private readonly Config $config, private readonly Router|RouteGroup $target - ) {} + ) { + } /** * Loads routes for a given module or modules. @@ -63,7 +64,7 @@ class ModuleLoader private function loadModuleRoutes(string $routesFile, string|null $prefix = null, string|null $moduleName = null): void { $moduleRoutes = require $routesFile; - + $options = []; if ($prefix) { $options['prefix'] = $prefix; diff --git a/src/Router/Route.php b/src/Router/Route.php index cebe8bd..90750f3 100644 --- a/src/Router/Route.php +++ b/src/Router/Route.php @@ -25,7 +25,8 @@ final class Route private readonly string $method, private readonly string $path, private readonly mixed $handler - ) {} + ) { + } /** * Gets the HTTP method of this route. @@ -56,4 +57,4 @@ final class Route { return $this->handler; } -} \ No newline at end of file +} diff --git a/src/Router/RouteDefinition.php b/src/Router/RouteDefinition.php index 2ad849a..c480d82 100644 --- a/src/Router/RouteDefinition.php +++ b/src/Router/RouteDefinition.php @@ -65,13 +65,29 @@ class RouteDefinition implements \JsonSerializable, \Serializable private array $defaults = [], private string|null $module = null, private array $attributes = [] - ) {} + ) { + } - public function getMethod(): string { return $this->method; } - public function getPattern(): string { return $this->pattern; } - public function getPath(): string { return $this->path; } - public function getHandler(): mixed { return $this->handler; } - public function getName(): ?string { return $this->name; } + public function getMethod(): string + { + return $this->method; + } + public function getPattern(): string + { + return $this->pattern; + } + public function getPath(): string + { + return $this->path; + } + public function getHandler(): mixed + { + return $this->handler; + } + public function getName(): ?string + { + return $this->name; + } public function name(string $name): self { @@ -79,7 +95,10 @@ class RouteDefinition implements \JsonSerializable, \Serializable return $this; } - public function getMiddleware(): array { return $this->middleware; } + public function getMiddleware(): array + { + return $this->middleware; + } public function middleware(string|array $middleware): self { @@ -91,7 +110,10 @@ class RouteDefinition implements \JsonSerializable, \Serializable return $this; } - public function getValidation(): array { return $this->validation; } + public function getValidation(): array + { + return $this->validation; + } public function valid(array|string $param, array|string $rules = []): self { @@ -105,7 +127,10 @@ class RouteDefinition implements \JsonSerializable, \Serializable return $this; } - public function getDefaults(): array { return $this->defaults; } + public function getDefaults(): array + { + return $this->defaults; + } public function default(string $param, mixed $value): self { @@ -113,9 +138,15 @@ class RouteDefinition implements \JsonSerializable, \Serializable return $this; } - public function getModule(): ?string { return $this->module; } + public function getModule(): ?string + { + return $this->module; + } - public function getAttributes(): array { return $this->attributes; } + public function getAttributes(): array + { + return $this->attributes; + } public function attr(string $key, mixed $value): self { diff --git a/src/Router/RouteGroup.php b/src/Router/RouteGroup.php index 102cbd2..94fb507 100644 --- a/src/Router/RouteGroup.php +++ b/src/Router/RouteGroup.php @@ -18,7 +18,8 @@ class RouteGroup public function __construct( private array $options = [], private readonly Router|null $router = null - ) {} + ) { + } /** * Creates a new route group with options and router. @@ -42,7 +43,7 @@ class RouteGroup if ($this->router) { return $this->router->registerCustomRoute('GET', $fullPath, $handler, $name, $middleware, $validation, $defaults); } - + return new RouteDefinition('GET', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults); } @@ -56,7 +57,7 @@ class RouteGroup if ($this->router) { return $this->router->registerCustomRoute('POST', $fullPath, $handler, $name, $middleware, $validation, $defaults); } - + return new RouteDefinition('POST', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults); } @@ -70,7 +71,7 @@ class RouteGroup if ($this->router) { return $this->router->registerCustomRoute('PUT', $fullPath, $handler, $name, $middleware, $validation, $defaults); } - + return new RouteDefinition('PUT', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults); } @@ -84,7 +85,7 @@ class RouteGroup if ($this->router) { return $this->router->registerCustomRoute('PATCH', $fullPath, $handler, $name, $middleware, $validation, $defaults); } - + return new RouteDefinition('PATCH', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults); } @@ -98,7 +99,7 @@ class RouteGroup if ($this->router) { return $this->router->registerCustomRoute('DELETE', $fullPath, $handler, $name, $middleware, $validation, $defaults); } - + return new RouteDefinition('DELETE', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults); } @@ -119,10 +120,10 @@ class RouteGroup public function fallback(mixed $handler): self { $this->options['fallback'] = $handler; - + $prefix = $this->options['prefix'] ?? '/'; $middleware = $this->options['middleware'] ?? []; - + if ($this->router) { $this->router->registerCustomRoute('FALLBACK', $this->joinPaths($prefix, '/_fallback'), $handler, null, $middleware) ->attr('_fallback', $handler) @@ -136,7 +137,7 @@ class RouteGroup { $fullPath = $this->buildFullPath($path); $mergedMiddleware = array_merge($this->options['middleware'] ?? [], $middleware); - + $route = null; if ($this->router) { $route = $this->router->registerCustomRoute($method, $fullPath, $handler, $name, $mergedMiddleware); @@ -179,7 +180,7 @@ class RouteGroup $prefix = $this->options['prefix'] ?? ''; $newPrefix = $this->joinPaths($prefix, $options['prefix'] ?? ''); - + $middleware = $this->options['middleware'] ?? []; $newMiddleware = array_merge($middleware, $options['middleware'] ?? []); @@ -188,7 +189,7 @@ class RouteGroup $defaults = $this->options['defaults'] ?? []; $newDefaults = array_merge($defaults, $options['defaults'] ?? []); - + $mergedOptions = array_merge($this->options, $options); $mergedOptions['prefix'] = $newPrefix; $mergedOptions['middleware'] = $newMiddleware; @@ -267,10 +268,10 @@ class RouteGroup // But ModuleLoader uses the router directly. // If we use $this->router->module(), it won't have the group prefix/middleware. // We should probably allow ModuleLoader to take a "target" which can be a Router or RouteGroup. - + // For now, let's just use the router but we have a problem: inheritance. // A better way is to make RouteGroup have a way to load modules. - + $moduleLoader = new ModuleLoader($this->router->getConfig(), $this); $moduleLoader->load($identifier, $prefix); } @@ -289,4 +290,4 @@ class RouteGroup { return $this->router->getConfig(); } -} \ No newline at end of file +} diff --git a/src/Router/RouteMatcher.php b/src/Router/RouteMatcher.php index f5850c2..f0335d4 100644 --- a/src/Router/RouteMatcher.php +++ b/src/Router/RouteMatcher.php @@ -34,7 +34,7 @@ class RouteMatcher $attributes = $this->mergeDefaults($route, $attributes); return $this->applyAttributes($route, $attributes); } - + // i18n support: check alternative paths $routeAttributes = $route->getAttributes(); if (isset($routeAttributes['i18n']) && is_array($routeAttributes['i18n'])) { @@ -129,7 +129,7 @@ class RouteMatcher } $pattern = $overridePath ? $this->compilePatternFromPath($overridePath, $route) : $this->getPatternForRoute($route); - + if (preg_match($pattern, $path, $matches)) { $attributes = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); return true; @@ -179,10 +179,10 @@ class RouteMatcher $pattern = preg_replace_callback('#/\{\{([a-zA-Z0-9_]+)(\?)?\}\}#', function ($matches) use ($validation, $defaults) { $name = $matches[1]; $optional = (isset($matches[2]) && $matches[2] === '?') || array_key_exists($name, $defaults); - + $rules = $validation[$name] ?? []; $regex = '[^/]+'; - + // Validation rules support foreach ((array)$rules as $rule) { if ($rule === 'numeric' || $rule === 'int') { @@ -199,7 +199,7 @@ class RouteMatcher if ($optional) { return '(?:/(?P<' . $name . '>' . $regex . '))?'; } - + return '/(?P<' . $name . '>' . $regex . ')'; }, $path); @@ -219,7 +219,7 @@ class RouteMatcher { $data = $route->toArray(); $data['attributes'] = array_merge($data['attributes'], $attributes); - + return new RouteDefinition( $data['method'], $data['pattern'], diff --git a/src/Router/Router.php b/src/Router/Router.php index 1bff7e6..998e84c 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -119,7 +119,7 @@ class Router $attributes = []; $routeMethod = strtoupper($route->getMethod()); $routePath = $route->getPath(); - + $matchStatus = 'mismatch'; if ($routeMethod !== $method && $routeMethod !== 'REDIRECT') { $matchStatus = 'method_mismatch'; @@ -129,7 +129,7 @@ class Router $matchStatus = 'matched'; $attributes = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); $attributes = array_merge($route->getDefaults(), $attributes); - + return new MatchResult(true, $route, $attributes, $diagnostics); } } @@ -163,7 +163,7 @@ class Router public function fallback(mixed $handler): self { $this->fallbackHandler = $handler; - + // Register a special route to carry the global fallback $this->registerRoute('FALLBACK', '/_fallback', $handler) ->attr('_fallback', $handler) @@ -203,7 +203,7 @@ class Router foreach ($parameterNames as $index => $name) { $pattern = '{{' . $name . ($isOptional[$index] === '?' ? '?' : '') . '}}'; - + if (array_key_exists($name, $parameters)) { $path = str_replace($pattern, (string)$parameters[$name], $path); } elseif (array_key_exists($name, $defaults)) { @@ -228,4 +228,4 @@ class Router $this->loader->load($identifier, $prefix); return $this; } -} \ No newline at end of file +}