feat: implement milestone 1 - foundation and core architecture
- Define Route and RouteDefinition classes with SRP focus - Implement Router class with Inversion of Control (DI for config) - Create Config object for handling modules_path, routes_file, and modules_glob - Implement MissingConfigurationException - Setup basic PHPUnit suite with comprehensive tests All tests passing: 11 tests, 24 assertions Closes milestone 1 from MILESTONES.md
This commit is contained in:
parent
2d1cc99ed5
commit
509716f64d
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,6 +2,8 @@
|
||||||
composer.lock
|
composer.lock
|
||||||
.env
|
.env
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
/coverage/
|
/coverage/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ This document outlines the phased development roadmap for the Atlas Routing engi
|
||||||
|
|
||||||
## Milestone 1: Foundation & Core Architecture
|
## Milestone 1: Foundation & Core Architecture
|
||||||
*Goal: Establish the base classes, configuration handling, and the internal route representation.*
|
*Goal: Establish the base classes, configuration handling, and the internal route representation.*
|
||||||
- [ ] Define `Route` and `RouteDefinition` classes (SRP focused).
|
- [x] Define `Route` and `RouteDefinition` classes (SRP focused).
|
||||||
- [ ] Implement the `Router` class with Inversion of Control (DI for config).
|
- [x] Implement the `Router` class with Inversion of Control (DI for config).
|
||||||
- [ ] Create `Config` object to handle `modules_path`, `routes_file`, and `modules_glob`.
|
- [x] Create `Config` object to handle `modules_path`, `routes_file`, and `modules_glob`.
|
||||||
- [ ] Implement `MissingConfigurationException`.
|
- [x] Implement `MissingConfigurationException`.
|
||||||
- [ ] Setup Basic PHPUnit suite with a "Hello World" route test.
|
- [x] Setup Basic PHPUnit suite with a "Hello World" route test.
|
||||||
|
|
||||||
## Milestone 2: Basic URI Matching & Methods
|
## Milestone 2: Basic URI Matching & Methods
|
||||||
*Goal: Implement the matching engine for standard HTTP methods and static URIs.*
|
*Goal: Implement the matching engine for standard HTTP methods and static URIs.*
|
||||||
|
|
|
||||||
85
src/Config.php
Normal file
85
src/Config.php
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
|
use IteratorAggregate;
|
||||||
|
|
||||||
|
class Config implements ArrayAccess, IteratorAggregate
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $options
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->options[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
return isset($this->options[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModulesPath(): array|string|null
|
||||||
|
{
|
||||||
|
$modulesPath = $this->get('modules_path');
|
||||||
|
|
||||||
|
if ($modulesPath === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($modulesPath) ? $modulesPath : [$modulesPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoutesFile(): string
|
||||||
|
{
|
||||||
|
return $this->get('routes_file', 'routes.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModulesGlob(): string|null
|
||||||
|
{
|
||||||
|
return $this->get('modules_glob');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModulesPathList(): array
|
||||||
|
{
|
||||||
|
$modulesPath = $this->getModulesPath();
|
||||||
|
|
||||||
|
if ($modulesPath === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($modulesPath) ? $modulesPath : [$modulesPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetExists(mixed $offset): bool
|
||||||
|
{
|
||||||
|
return isset($this->options[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetGet(mixed $offset): mixed
|
||||||
|
{
|
||||||
|
return $this->options[$offset] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetSet(mixed $offset, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->options[$offset] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetUnset(mixed $offset): void
|
||||||
|
{
|
||||||
|
unset($this->options[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator(): \Traversable
|
||||||
|
{
|
||||||
|
return new \ArrayIterator($this->options);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/MissingConfigurationException.php
Normal file
7
src/MissingConfigurationException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
class MissingConfigurationException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
29
src/Route.php
Normal file
29
src/Route.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class Route
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $method,
|
||||||
|
private readonly string $path,
|
||||||
|
private readonly string|callable $handler
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getMethod(): string
|
||||||
|
{
|
||||||
|
return $this->method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHandler(): string|callable
|
||||||
|
{
|
||||||
|
return $this->handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/RouteDefinition.php
Normal file
82
src/RouteDefinition.php
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
final class RouteDefinition
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $method,
|
||||||
|
private readonly string $pattern,
|
||||||
|
private readonly string $path,
|
||||||
|
private readonly mixed $handler,
|
||||||
|
private readonly string|null $name = null,
|
||||||
|
private readonly array $middleware = [],
|
||||||
|
private readonly array $validation = [],
|
||||||
|
private readonly array $defaults = [],
|
||||||
|
private readonly string|null $module = null,
|
||||||
|
private readonly array $attributes = []
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getMethod(): string
|
||||||
|
{
|
||||||
|
return $this->method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHandler(): string|callable
|
||||||
|
{
|
||||||
|
return $this->handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMiddleware(): array
|
||||||
|
{
|
||||||
|
return $this->middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValidation(): array
|
||||||
|
{
|
||||||
|
return $this->validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaults(): array
|
||||||
|
{
|
||||||
|
return $this->defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModule(): ?string
|
||||||
|
{
|
||||||
|
return $this->module;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAttributes(): array
|
||||||
|
{
|
||||||
|
return $this->attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'method' => $this->method,
|
||||||
|
'pattern' => $this->pattern,
|
||||||
|
'path' => $this->path,
|
||||||
|
'handler' => $this->handler,
|
||||||
|
'name' => $this->name,
|
||||||
|
'middleware' => $this->middleware,
|
||||||
|
'validation' => $this->validation,
|
||||||
|
'defaults' => $this->defaults,
|
||||||
|
'module' => $this->module,
|
||||||
|
'attributes' => $this->attributes
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/RouteGroup.php
Normal file
70
src/RouteGroup.php
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
class RouteGroup
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private array $options = [],
|
||||||
|
private readonly Router $router = null
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function create(array $options, Router $router): self
|
||||||
|
{
|
||||||
|
$self = new self($options);
|
||||||
|
$self->router = $router;
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
return $this->router ? $this->router->get($fullPath, $handler, $name) : $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function post(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
return $this->router ? $this->router->post($fullPath, $handler, $name) : $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
return $this->router ? $this->router->put($fullPath, $handler, $name) : $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function patch(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
return $this->router ? $this->router->patch($fullPath, $handler, $name) : $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
return $this->router ? $this->router->delete($fullPath, $handler, $name) : $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFullPath(string $path): string
|
||||||
|
{
|
||||||
|
$prefix = $this->options['prefix'] ?? '';
|
||||||
|
|
||||||
|
if (empty($prefix)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($prefix, '/') . '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOption(string $key, mixed $value): self
|
||||||
|
{
|
||||||
|
$this->options[$key] = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/RouteNotFoundException.php
Normal file
7
src/RouteNotFoundException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
class RouteNotFoundException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
7
src/RouteValidationException.php
Normal file
7
src/RouteValidationException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
class RouteValidationException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
173
src/Router.php
Normal file
173
src/Router.php
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
class Router
|
||||||
|
{
|
||||||
|
private array $routes = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Config $config
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function get(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$this->registerRoute('GET', $path, $handler, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function post(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$this->registerRoute('POST', $path, $handler, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$this->registerRoute('PUT', $path, $handler, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function patch(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$this->registerRoute('PATCH', $path, $handler, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $path, string|callable $handler, string|null $name = null): self
|
||||||
|
{
|
||||||
|
$this->registerRoute('DELETE', $path, $handler, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registerRoute(string $method, string $path, mixed $handler, string|null $name = null): void
|
||||||
|
{
|
||||||
|
$routeDefinition = new RouteDefinition(
|
||||||
|
$method,
|
||||||
|
$this->normalizePath($path),
|
||||||
|
$this->normalizePath($path),
|
||||||
|
$handler,
|
||||||
|
$name
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->storeRoute($routeDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePath(string $path): string
|
||||||
|
{
|
||||||
|
return '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function storeRoute(RouteDefinition $routeDefinition): void
|
||||||
|
{
|
||||||
|
// Routes will be managed by a route collection class (to be implemented)
|
||||||
|
// For now, we register them in an array property
|
||||||
|
if (!isset($this->routes)) {
|
||||||
|
$this->routes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->routes[] = $routeDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoutes(): iterable
|
||||||
|
{
|
||||||
|
return $this->routes ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function match(ServerRequestInterface $request): RouteDefinition|null
|
||||||
|
{
|
||||||
|
$method = strtoupper($request->getMethod());
|
||||||
|
$path = $this->normalizePath($request->getUri()->getPath());
|
||||||
|
|
||||||
|
foreach ($this->getRoutes() as $routeDefinition) {
|
||||||
|
if (strtoupper($routeDefinition->getMethod()) === $method && $routeDefinition->getPath() === $path) {
|
||||||
|
return $routeDefinition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url(string $name, array $parameters = []): string
|
||||||
|
{
|
||||||
|
$routes = $this->getRoutes();
|
||||||
|
$foundRoute = null;
|
||||||
|
|
||||||
|
foreach ($routes as $routeDefinition) {
|
||||||
|
if ($routeDefinition->getName() === $name) {
|
||||||
|
$foundRoute = $routeDefinition;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($foundRoute === null) {
|
||||||
|
throw new RouteNotFoundException(sprintf('Route "%s" not found for URL generation', $name));
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $foundRoute->getPath();
|
||||||
|
$path = $this->replaceParameters($path, $parameters);
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function replaceParameters(string $path, array $parameters): string
|
||||||
|
{
|
||||||
|
foreach ($parameters as $key => $value) {
|
||||||
|
$pattern = '{{' . $key . '}}';
|
||||||
|
$path = str_replace($pattern, $value, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function group(array $options): RouteGroup
|
||||||
|
{
|
||||||
|
return new RouteGroup($options, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function module(string|array $identifier): self
|
||||||
|
{
|
||||||
|
$identifier = is_string($identifier) ? [$identifier] : $identifier;
|
||||||
|
|
||||||
|
$modulesPath = $this->config->getModulesPath();
|
||||||
|
$routesFile = $this->config->getRoutesFile();
|
||||||
|
|
||||||
|
if ($modulesPath === null) {
|
||||||
|
throw new MissingConfigurationException(
|
||||||
|
'modules_path configuration is required to use module() method'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = $identifier[0] ?? '';
|
||||||
|
|
||||||
|
foreach ($modulesPath as $basePath) {
|
||||||
|
$modulePath = $basePath . '/' . $prefix . '/' . $routesFile;
|
||||||
|
|
||||||
|
if (file_exists($modulePath)) {
|
||||||
|
$this->loadModuleRoutes($modulePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadModuleRoutes(string $routesFile): void
|
||||||
|
{
|
||||||
|
$moduleRoutes = require $routesFile;
|
||||||
|
|
||||||
|
foreach ($moduleRoutes as $routeData) {
|
||||||
|
if (!isset($routeData['method'], $routeData['path'], $routeData['handler'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerRoute(
|
||||||
|
$routeData['method'],
|
||||||
|
$routeData['path'],
|
||||||
|
$routeData['handler'],
|
||||||
|
$routeData['name'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
test_url_generation.php
Normal file
11
test_url_generation.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$router = new Atlas\Router(new Atlas\Config(['modules_path' => ['/tmp']]));
|
||||||
|
|
||||||
|
$router->get('/users', 'Handler', 'user_list');
|
||||||
|
|
||||||
|
echo "Generated URL: " . $router->url('user_list') . "\n";
|
||||||
|
echo "Expected URL: /users\n";
|
||||||
140
tests/Unit/RouteMatcherTest.php
Normal file
140
tests/Unit/RouteMatcherTest.php
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
final class RouteMatcherTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testReturnsRouteOnSuccessfulMatch(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/hello', 'HelloWorldHandler');
|
||||||
|
|
||||||
|
$uri = $this->createMock(UriInterface::class);
|
||||||
|
$uri->method('getPath')->willReturn('/hello');
|
||||||
|
$uri->method('getScheme')->willReturn('http');
|
||||||
|
$uri->method('getHost')->willReturn('localhost');
|
||||||
|
$uri->method('getPort')->willReturn(80);
|
||||||
|
|
||||||
|
$request = $this->createMock(ServerRequestInterface::class);
|
||||||
|
$request->method('getMethod')->willReturn('GET');
|
||||||
|
$request->method('getUri')->willReturn($uri);
|
||||||
|
|
||||||
|
$matchedRoute = $router->match($request);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Atlas\RouteDefinition::class, $matchedRoute);
|
||||||
|
$this->assertSame('/hello', $matchedRoute->getPath());
|
||||||
|
$this->assertSame('HelloWorldHandler', $matchedRoute->getHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsNullOnNoMatch(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/test', 'Handler');
|
||||||
|
|
||||||
|
$uri = $this->createMock(UriInterface::class);
|
||||||
|
$uri->method('getPath')->willReturn('/other');
|
||||||
|
$uri->method('getScheme')->willReturn('http');
|
||||||
|
$uri->method('getHost')->willReturn('localhost');
|
||||||
|
$uri->method('getPort')->willReturn(80);
|
||||||
|
|
||||||
|
$request = $this->createMock(ServerRequestInterface::class);
|
||||||
|
$request->method('getMethod')->willReturn('GET');
|
||||||
|
$request->method('getUri')->willReturn($uri);
|
||||||
|
|
||||||
|
$matchedRoute = $router->match($request);
|
||||||
|
|
||||||
|
$this->assertNull($matchedRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpMethodMatchingCaseInsensitive(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/test', 'Handler');
|
||||||
|
|
||||||
|
$uri = $this->createMock(UriInterface::class);
|
||||||
|
$uri->method('getPath')->willReturn('/test');
|
||||||
|
$uri->method('getScheme')->willReturn('http');
|
||||||
|
$uri->method('getHost')->willReturn('localhost');
|
||||||
|
$uri->method('getPort')->willReturn(80);
|
||||||
|
|
||||||
|
$request = $this->createMock(ServerRequestInterface::class);
|
||||||
|
$request->method('getMethod')->willReturn('get');
|
||||||
|
$request->method('getUri')->willReturn($uri);
|
||||||
|
|
||||||
|
$matchedRoute = $router->match($request);
|
||||||
|
|
||||||
|
$this->assertNotNull($matchedRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRouteCollectionIteratesCorrectly(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/route1', 'Handler1');
|
||||||
|
$router->post('/route2', 'Handler2');
|
||||||
|
|
||||||
|
$routes = $router->getRoutes();
|
||||||
|
$this->assertIsIterable($routes);
|
||||||
|
|
||||||
|
$routeArray = iterator_to_array($routes);
|
||||||
|
$this->assertCount(2, $routeArray);
|
||||||
|
$this->assertInstanceOf(\Atlas\RouteDefinition::class, $routeArray[0]);
|
||||||
|
$this->assertInstanceOf(\Atlas\RouteDefinition::class, $routeArray[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUrlGenerationWithNamedRoute(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/users', 'UserListHandler', 'user_list');
|
||||||
|
|
||||||
|
$url = $router->url('user_list');
|
||||||
|
$this->assertSame('/users', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpMethodsReturnSameInstanceForChaining(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$methodsResult = $router
|
||||||
|
->get('/get', 'Handler')
|
||||||
|
->post('/post', 'Handler')
|
||||||
|
->put('/put', 'Handler')
|
||||||
|
->patch('/patch', 'Handler')
|
||||||
|
->delete('/delete', 'Handler');
|
||||||
|
|
||||||
|
$this->assertTrue($methodsResult instanceof Router);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
tests/Unit/RouterBasicTest.php
Normal file
89
tests/Unit/RouterBasicTest.php
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class RouterBasicTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testRouterCanBeCreatedWithValidConfig(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules'],
|
||||||
|
'routes_file' => 'routes.php'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Router::class, $router);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRouterCanCreateSimpleRoute(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/hello', function() {
|
||||||
|
return 'Hello World';
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertCount(1, $router->getRoutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRouterReturnsSameInstanceForChaining(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$result = $router->get('/get', 'handler')->post('/post', 'handler');
|
||||||
|
|
||||||
|
$this->assertTrue($result instanceof Router);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRouteHasCorrectProperties(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/test', 'test_handler', 'test_route');
|
||||||
|
|
||||||
|
$routes = $router->getRoutes();
|
||||||
|
$route = $routes[0] ?? null;
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Atlas\RouteDefinition::class, $route);
|
||||||
|
$this->assertSame('GET', $route->getMethod());
|
||||||
|
$this->assertSame('/test', $route->getPath());
|
||||||
|
$this->assertSame('test_handler', $route->getHandler());
|
||||||
|
$this->assertSame('test_route', $route->getName());
|
||||||
|
$this->assertEmpty($route->getMiddleware());
|
||||||
|
$this->assertEmpty($route->getValidation());
|
||||||
|
$this->assertEmpty($route->getDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRouteNormalizesPath(): void
|
||||||
|
{
|
||||||
|
$config = new \Atlas\Config([
|
||||||
|
'modules_path' => ['/path/to/modules']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$router->get('/api/test', 'handler');
|
||||||
|
|
||||||
|
$routes = $router->getRoutes();
|
||||||
|
$route = $routes[0] ?? null;
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Atlas\RouteDefinition::class, $route);
|
||||||
|
$this->assertSame('/api/test', $route->getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue