refactor: implement milestone 4 - architectural refinement (SRP & SOLID)
This commit is contained in:
parent
7152e2b3e7
commit
ea23140a40
94
src/Router/ModuleLoader.php
Normal file
94
src/Router/ModuleLoader.php
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Router;
|
||||||
|
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use Atlas\Exception\MissingConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles module discovery and route loading.
|
||||||
|
*/
|
||||||
|
class ModuleLoader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs a new ModuleLoader instance.
|
||||||
|
*
|
||||||
|
* @param Config $config The configuration object
|
||||||
|
* @param Router|RouteGroup $target The target to register routes to
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly Config $config,
|
||||||
|
private readonly Router|RouteGroup $target
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads routes for a given module or modules.
|
||||||
|
*
|
||||||
|
* @param string|array $identifier The module identifier or array of identifiers
|
||||||
|
* @param string|null $prefix Optional URI prefix for the module
|
||||||
|
* @return void
|
||||||
|
* @throws MissingConfigurationException if modules_path is not configured
|
||||||
|
*/
|
||||||
|
public function load(string|array $identifier, string|null $prefix = null): void
|
||||||
|
{
|
||||||
|
$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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleName = $identifier[0] ?? '';
|
||||||
|
|
||||||
|
foreach ((array)$modulesPath as $basePath) {
|
||||||
|
$modulePath = $basePath . '/' . $moduleName . '/' . $routesFile;
|
||||||
|
|
||||||
|
if (file_exists($modulePath)) {
|
||||||
|
$this->loadModuleRoutes($modulePath, $prefix, $moduleName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads route definitions from a file and registers them.
|
||||||
|
*
|
||||||
|
* @param string $routesFile The path to the routes file
|
||||||
|
* @param string|null $prefix Optional URI prefix
|
||||||
|
* @param string|null $moduleName Optional module name
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function loadModuleRoutes(string $routesFile, string|null $prefix = null, string|null $moduleName = null): void
|
||||||
|
{
|
||||||
|
$moduleRoutes = require $routesFile;
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
if ($prefix) {
|
||||||
|
$options['prefix'] = $prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = $this->target->group($options);
|
||||||
|
|
||||||
|
foreach ($moduleRoutes as $routeData) {
|
||||||
|
if (!isset($routeData['method'], $routeData['path'], $routeData['handler'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$route = $group->registerCustomRoute(
|
||||||
|
$routeData['method'],
|
||||||
|
$routeData['path'],
|
||||||
|
$routeData['handler'],
|
||||||
|
$routeData['name'] ?? null,
|
||||||
|
$routeData['middleware'] ?? [],
|
||||||
|
$routeData['validation'] ?? [],
|
||||||
|
$routeData['defaults'] ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($moduleName) {
|
||||||
|
$route->setModule($moduleName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/Router/RouteCollection.php
Normal file
100
src/Router/RouteCollection.php
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the storage and retrieval of route definitions.
|
||||||
|
*
|
||||||
|
* @implements \IteratorAggregate<int, RouteDefinition>
|
||||||
|
*/
|
||||||
|
class RouteCollection implements \IteratorAggregate, \Serializable
|
||||||
|
{
|
||||||
|
public function serialize(): string
|
||||||
|
{
|
||||||
|
return serialize($this->__serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unserialize(string $data): void
|
||||||
|
{
|
||||||
|
$this->__unserialize(unserialize($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'routes' => $this->routes,
|
||||||
|
'namedRoutes' => $this->namedRoutes,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __unserialize(array $data): void
|
||||||
|
{
|
||||||
|
$this->routes = $data['routes'];
|
||||||
|
$this->namedRoutes = $data['namedRoutes'];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @var RouteDefinition[]
|
||||||
|
*/
|
||||||
|
private array $routes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, RouteDefinition>
|
||||||
|
*/
|
||||||
|
private array $namedRoutes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a route definition to the collection.
|
||||||
|
*
|
||||||
|
* @param RouteDefinition $route The route to add
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add(RouteDefinition $route): void
|
||||||
|
{
|
||||||
|
$this->routes[] = $route;
|
||||||
|
|
||||||
|
if ($name = $route->getName()) {
|
||||||
|
$this->namedRoutes[$name] = $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a route by its name.
|
||||||
|
*
|
||||||
|
* @param string $name The name of the route
|
||||||
|
* @return RouteDefinition|null The route if found, null otherwise
|
||||||
|
*/
|
||||||
|
public function getByName(string $name): ?RouteDefinition
|
||||||
|
{
|
||||||
|
return $this->namedRoutes[$name] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all route definitions in the collection.
|
||||||
|
*
|
||||||
|
* @return RouteDefinition[]
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return $this->routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator for the route definitions.
|
||||||
|
*
|
||||||
|
* @return \Traversable<int, RouteDefinition>
|
||||||
|
*/
|
||||||
|
public function getIterator(): \Traversable
|
||||||
|
{
|
||||||
|
return new \ArrayIterator($this->routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of routes in the collection.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->routes);
|
||||||
|
}
|
||||||
|
}
|
||||||
236
src/Router/RouteMatcher.php
Normal file
236
src/Router/RouteMatcher.php
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Router;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the logic for matching a request to a route.
|
||||||
|
*/
|
||||||
|
class RouteMatcher
|
||||||
|
{
|
||||||
|
use PathHelper;
|
||||||
|
|
||||||
|
private array $compiledPatterns = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a request against a collection of routes.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The request to match
|
||||||
|
* @param RouteCollection $routes The collection of routes to match against
|
||||||
|
* @return RouteDefinition|null The matched route or null if no match found
|
||||||
|
*/
|
||||||
|
public function match(ServerRequestInterface $request, RouteCollection $routes): ?RouteDefinition
|
||||||
|
{
|
||||||
|
$method = strtoupper($request->getMethod());
|
||||||
|
$path = $this->normalizePath($request->getUri()->getPath());
|
||||||
|
$host = $request->getUri()->getHost();
|
||||||
|
|
||||||
|
$routesToMatch = $routes;
|
||||||
|
|
||||||
|
foreach ($routesToMatch as $route) {
|
||||||
|
$attributes = [];
|
||||||
|
if ($this->isMatch($method, $path, $host, $route, $attributes)) {
|
||||||
|
$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'])) {
|
||||||
|
foreach ($routeAttributes['i18n'] as $lang => $i18nPath) {
|
||||||
|
$normalizedI18nPath = $this->normalizePath($i18nPath);
|
||||||
|
if ($this->isMatch($method, $path, $host, $route, $attributes, $normalizedI18nPath)) {
|
||||||
|
$attributes['lang'] = $lang;
|
||||||
|
$attributes = $this->mergeDefaults($route, $attributes);
|
||||||
|
return $this->applyAttributes($route, $attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find a fallback
|
||||||
|
return $this->matchFallback($path, $routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to match a fallback handler for the given path.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* @param RouteCollection $routes
|
||||||
|
* @return RouteDefinition|null
|
||||||
|
*/
|
||||||
|
private function matchFallback(string $path, RouteCollection $routes): ?RouteDefinition
|
||||||
|
{
|
||||||
|
$bestFallback = null;
|
||||||
|
$longestPrefix = -1;
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$attributes = $route->getAttributes();
|
||||||
|
if (isset($attributes['_fallback'])) {
|
||||||
|
$prefix = $attributes['_fallback_prefix'] ?? '';
|
||||||
|
if (str_starts_with($path, $prefix) && strlen($prefix) > $longestPrefix) {
|
||||||
|
$longestPrefix = strlen($prefix);
|
||||||
|
$bestFallback = $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bestFallback) {
|
||||||
|
$attributes = $bestFallback->getAttributes();
|
||||||
|
return new RouteDefinition(
|
||||||
|
'FALLBACK',
|
||||||
|
$path,
|
||||||
|
$path,
|
||||||
|
$attributes['_fallback'],
|
||||||
|
null,
|
||||||
|
$bestFallback->getMiddleware()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges default values for missing optional parameters.
|
||||||
|
*
|
||||||
|
* @param RouteDefinition $route
|
||||||
|
* @param array $attributes
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function mergeDefaults(RouteDefinition $route, array $attributes): array
|
||||||
|
{
|
||||||
|
return array_merge($route->getDefaults(), $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a request matches a route definition.
|
||||||
|
*
|
||||||
|
* @param string $method The request method
|
||||||
|
* @param string $path The request path
|
||||||
|
* @param RouteDefinition $route The route to check
|
||||||
|
* @param array $attributes Extracted attributes
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isMatch(string $method, string $path, string $host, RouteDefinition $route, array &$attributes, ?string $overridePath = null): bool
|
||||||
|
{
|
||||||
|
$routeMethod = strtoupper($route->getMethod());
|
||||||
|
if ($routeMethod !== $method && $routeMethod !== 'REDIRECT') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subdomain constraint check
|
||||||
|
$routeAttributes = $route->getAttributes();
|
||||||
|
if (isset($routeAttributes['subdomain'])) {
|
||||||
|
$subdomain = $routeAttributes['subdomain'];
|
||||||
|
if (!str_starts_with($host, $subdomain . '.')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getPatternForRoute(RouteDefinition $route): string
|
||||||
|
{
|
||||||
|
return $this->compilePattern($route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles a route path into a regex pattern.
|
||||||
|
*
|
||||||
|
* @param RouteDefinition $route
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function compilePattern(RouteDefinition $route): string
|
||||||
|
{
|
||||||
|
$id = spl_object_id($route);
|
||||||
|
if (isset($this->compiledPatterns[$id])) {
|
||||||
|
return $this->compiledPatterns[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->compiledPatterns[$id] = $this->compilePatternFromPath($route->getPath(), $route);
|
||||||
|
return $this->compiledPatterns[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles a specific path into a regex pattern using route's validation and defaults.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* @param RouteDefinition $route
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function compilePatternFromPath(string $path, RouteDefinition $route): string
|
||||||
|
{
|
||||||
|
$validation = $route->getValidation();
|
||||||
|
$defaults = $route->getDefaults();
|
||||||
|
|
||||||
|
// Replace {{param?}} and {{param}} with regex
|
||||||
|
$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') {
|
||||||
|
$regex = '[0-9]+';
|
||||||
|
} elseif ($rule === 'alpha') {
|
||||||
|
$regex = '[a-zA-Z]+';
|
||||||
|
} elseif ($rule === 'alphanumeric') {
|
||||||
|
$regex = '[a-zA-Z0-9]+';
|
||||||
|
} elseif (str_starts_with($rule, 'regex:')) {
|
||||||
|
$regex = substr($rule, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($optional) {
|
||||||
|
return '(?:/(?P<' . $name . '>' . $regex . '))?';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/(?P<' . $name . '>' . $regex . ')';
|
||||||
|
}, $path);
|
||||||
|
|
||||||
|
$pattern = str_replace('//', '/', $pattern);
|
||||||
|
|
||||||
|
return '#^' . $pattern . '/?$#';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies extracted attributes to a new route definition.
|
||||||
|
*
|
||||||
|
* @param RouteDefinition $route
|
||||||
|
* @param array $attributes
|
||||||
|
* @return RouteDefinition
|
||||||
|
*/
|
||||||
|
private function applyAttributes(RouteDefinition $route, array $attributes): RouteDefinition
|
||||||
|
{
|
||||||
|
$data = $route->toArray();
|
||||||
|
$data['attributes'] = array_merge($data['attributes'], $attributes);
|
||||||
|
|
||||||
|
return new RouteDefinition(
|
||||||
|
$data['method'],
|
||||||
|
$data['pattern'],
|
||||||
|
$data['path'],
|
||||||
|
$data['handler'],
|
||||||
|
$data['name'],
|
||||||
|
$data['middleware'],
|
||||||
|
$data['validation'],
|
||||||
|
$data['defaults'],
|
||||||
|
$data['module'],
|
||||||
|
$data['attributes']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,357 +4,228 @@ namespace Atlas\Router;
|
||||||
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Atlas\Config\Config;
|
use Atlas\Config\Config;
|
||||||
use Atlas\Exception\MissingConfigurationException;
|
|
||||||
use Atlas\Exception\NotFoundRouteException;
|
|
||||||
use Atlas\Exception\RouteNotFoundException;
|
use Atlas\Exception\RouteNotFoundException;
|
||||||
|
use Atlas\Exception\MissingConfigurationException;
|
||||||
|
|
||||||
/**
|
class Router
|
||||||
* Main routing engine for the Atlas framework.
|
|
||||||
*
|
|
||||||
* Provides fluent, chainable API for route registration and request matching.
|
|
||||||
* Supports static and dynamic URIs, named routes, module routing, and error handling.
|
|
||||||
*
|
|
||||||
* @implements \IteratorAggregate<array-key, RouteDefinition>
|
|
||||||
*/
|
|
||||||
class Router implements \IteratorAggregate
|
|
||||||
{
|
{
|
||||||
/**
|
use PathHelper;
|
||||||
* Private array to store registered route definitions.
|
|
||||||
*
|
|
||||||
* @var array<RouteDefinition>
|
|
||||||
*/
|
|
||||||
private array $routes = [];
|
|
||||||
|
|
||||||
/**
|
private RouteCollection $routes;
|
||||||
* Protected fallback handler for 404 scenarios.
|
private readonly RouteMatcher $matcher;
|
||||||
*
|
private readonly ModuleLoader $loader;
|
||||||
* @var mixed mixed callable|string|null
|
|
||||||
*/
|
|
||||||
protected mixed $fallbackHandler = null;
|
protected mixed $fallbackHandler = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new Router instance with configuration.
|
|
||||||
*
|
|
||||||
* @param Config\Config $config Configuration object containing routing settings
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Config\Config $config
|
private readonly Config $config
|
||||||
) {
|
) {
|
||||||
|
$this->routes = new RouteCollection();
|
||||||
|
$this->matcher = new RouteMatcher();
|
||||||
|
$this->loader = new ModuleLoader($this->config, $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function get(string $path, string|callable $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a GET route.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler or string reference
|
|
||||||
* @param string|null $name Optional route name for reverse routing
|
|
||||||
* @return self Fluent interface for method chaining
|
|
||||||
*/
|
|
||||||
public function get(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$this->registerRoute('GET', $path, $handler, $name);
|
return $this->registerRoute('GET', $path, $handler, $name);
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function post(string $path, string|callable $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a POST route.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler or string reference
|
|
||||||
* @param string|null $name Optional route name for reverse routing
|
|
||||||
* @return self Fluent interface for method chaining
|
|
||||||
*/
|
|
||||||
public function post(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$this->registerRoute('POST', $path, $handler, $name);
|
return $this->registerRoute('POST', $path, $handler, $name);
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function put(string $path, string|callable $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a PUT route.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler or string reference
|
|
||||||
* @param string|null $name Optional route name for reverse routing
|
|
||||||
* @return self Fluent interface for method chaining
|
|
||||||
*/
|
|
||||||
public function put(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$this->registerRoute('PUT', $path, $handler, $name);
|
return $this->registerRoute('PUT', $path, $handler, $name);
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function patch(string $path, string|callable $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a PATCH route.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler or string reference
|
|
||||||
* @param string|null $name Optional route name for reverse routing
|
|
||||||
* @return self Fluent interface for method chaining
|
|
||||||
*/
|
|
||||||
public function patch(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$this->registerRoute('PATCH', $path, $handler, $name);
|
return $this->registerRoute('PATCH', $path, $handler, $name);
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function delete(string $path, string|callable $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a DELETE route.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler or string reference
|
|
||||||
* @param string|null $name Optional route name for reverse routing
|
|
||||||
* @return self Fluent interface for method chaining
|
|
||||||
*/
|
|
||||||
public function delete(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$this->registerRoute('DELETE', $path, $handler, $name);
|
return $this->registerRoute('DELETE', $path, $handler, $name);
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function registerCustomRoute(string $method, string $path, mixed $handler, string|null $name = null, array $middleware = [], array $validation = [], array $defaults = []): RouteDefinition
|
||||||
* Normalizes a path string.
|
|
||||||
*
|
|
||||||
* Removes leading and trailing slashes and ensures proper format.
|
|
||||||
*
|
|
||||||
* @param string $path Raw path string
|
|
||||||
* @return string Normalized path
|
|
||||||
*/
|
|
||||||
private function normalizePath(string $path): string
|
|
||||||
{
|
{
|
||||||
$normalized = trim($path, '/');
|
return $this->registerRoute($method, $path, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
|
||||||
if (empty($normalized)) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '/' . $normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function registerRoute(string $method, string $path, mixed $handler, string|null $name = null, array $middleware = [], array $validation = [], array $defaults = []): RouteDefinition
|
||||||
* Registers a route definition and stores it.
|
|
||||||
*
|
|
||||||
* @param string $method HTTP method
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param mixed $middleware middleware (not yet implemented)
|
|
||||||
* @param string|callable $handler Route handler
|
|
||||||
* @param string|null $name Optional route name
|
|
||||||
*/
|
|
||||||
private function registerRoute(string $method, string $path, mixed $middleware, string|callable $handler, string|null $name = null): void
|
|
||||||
{
|
{
|
||||||
|
$normalizedPath = $this->normalizePath($path);
|
||||||
$routeDefinition = new RouteDefinition(
|
$routeDefinition = new RouteDefinition(
|
||||||
$method,
|
$method,
|
||||||
$this->normalizePath($path),
|
$normalizedPath,
|
||||||
$this->normalizePath($path),
|
$normalizedPath,
|
||||||
$handler,
|
$handler,
|
||||||
$name
|
$name,
|
||||||
|
$middleware,
|
||||||
|
$validation,
|
||||||
|
$defaults
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->storeRoute($routeDefinition);
|
$this->storeRoute($routeDefinition);
|
||||||
|
|
||||||
|
return $routeDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores a route definition for later matching.
|
|
||||||
*
|
|
||||||
* @param RouteDefinition $routeDefinition Route definition instance
|
|
||||||
*/
|
|
||||||
protected function storeRoute(RouteDefinition $routeDefinition): void
|
protected function storeRoute(RouteDefinition $routeDefinition): void
|
||||||
{
|
{
|
||||||
// Routes will be managed by a route collection class (to be implemented)
|
$this->routes->add($routeDefinition);
|
||||||
// For now, we register them in an array property
|
|
||||||
if (!isset($this->routes)) {
|
|
||||||
$this->routes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->routes[] = $routeDefinition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function getRoutes(): RouteCollection
|
||||||
* Retrieves all registered route definitions.
|
|
||||||
*
|
|
||||||
* @return iterable All route definitions
|
|
||||||
*/
|
|
||||||
public function getRoutes(): iterable
|
|
||||||
{
|
{
|
||||||
return $this->routes ?? [];
|
return $this->routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function setRoutes(RouteCollection $routes): self
|
||||||
* Matches a request to registered routes.
|
|
||||||
*
|
|
||||||
* Returns null if no match is found.
|
|
||||||
*
|
|
||||||
* @param ServerRequestInterface $request PSR-7 request object
|
|
||||||
* @return RouteDefinition|null Matched route or null
|
|
||||||
*/
|
|
||||||
public function match(ServerRequestInterface $request): RouteDefinition|null
|
|
||||||
{
|
{
|
||||||
$method = strtoupper($request->getMethod());
|
$this->routes = $routes;
|
||||||
$path = $this->normalizePath($request->getUri()->getPath());
|
|
||||||
|
|
||||||
foreach ($this->getRoutes() as $routeDefinition) {
|
|
||||||
if (strtoupper($routeDefinition->getMethod()) === $method && $routeDefinition->getPath() === $path) {
|
|
||||||
return $routeDefinition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches a request and throws exception if no match found.
|
|
||||||
*
|
|
||||||
* @param ServerRequestInterface $request PSR-7 request object
|
|
||||||
* @return RouteDefinition Matched route definition
|
|
||||||
* @throws NotFoundRouteException If no route matches
|
|
||||||
*/
|
|
||||||
public function matchOrFail(ServerRequestInterface $request): RouteDefinition
|
|
||||||
{
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotFoundRouteException('No route matched the request');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a fallback handler for unmatched requests.
|
|
||||||
*
|
|
||||||
* @param callable|string|null $handler Fallback handler
|
|
||||||
* @return self Fluent interface
|
|
||||||
*/
|
|
||||||
public function fallback(mixed $handler): self
|
|
||||||
{
|
|
||||||
$this->fallbackHandler = $handler;
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a URL for a named route with parameters.
|
* @internal
|
||||||
*
|
|
||||||
* @param string $name Route name
|
|
||||||
* @param array $parameters Route parameters
|
|
||||||
* @return string Generated URL path
|
|
||||||
* @throws RouteNotFoundException If route name not found
|
|
||||||
*/
|
*/
|
||||||
|
public function getConfig(): Config
|
||||||
|
{
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function match(ServerRequestInterface $request): RouteDefinition|null
|
||||||
|
{
|
||||||
|
return $this->matcher->match($request, $this->routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inspect(ServerRequestInterface $request): MatchResult
|
||||||
|
{
|
||||||
|
$method = strtoupper($request->getMethod());
|
||||||
|
$path = $this->normalizePath($request->getUri()->getPath());
|
||||||
|
$host = $request->getUri()->getHost();
|
||||||
|
|
||||||
|
$diagnostics = [
|
||||||
|
'method' => $method,
|
||||||
|
'path' => $path,
|
||||||
|
'host' => $host,
|
||||||
|
'attempts' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
$attributes = [];
|
||||||
|
$routeMethod = strtoupper($route->getMethod());
|
||||||
|
$routePath = $route->getPath();
|
||||||
|
|
||||||
|
$matchStatus = 'mismatch';
|
||||||
|
if ($routeMethod !== $method && $routeMethod !== 'REDIRECT') {
|
||||||
|
$matchStatus = 'method_mismatch';
|
||||||
|
} else {
|
||||||
|
$pattern = $this->matcher->getPatternForRoute($route);
|
||||||
|
if (preg_match($pattern, $path, $matches)) {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$diagnostics['attempts'][] = [
|
||||||
|
'route' => $route->getName() ?? $route->getMethod() . ' ' . $route->getPath(),
|
||||||
|
'status' => $matchStatus,
|
||||||
|
'pattern' => $this->matcher->getPatternForRoute($route)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MatchResult(false, null, [], $diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function matchOrFail(ServerRequestInterface $request): RouteDefinition
|
||||||
|
{
|
||||||
|
$route = $this->match($request);
|
||||||
|
|
||||||
|
if ($route === null) {
|
||||||
|
throw new RouteNotFoundException('No route matched the request');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function redirect(string $path, string $destination, int $status = 302): RouteDefinition
|
||||||
|
{
|
||||||
|
return $this->registerRoute('REDIRECT', $path, $destination)->attr('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
->attr('_fallback_prefix', '/');
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFallbackHandler(): mixed
|
||||||
|
{
|
||||||
|
return $this->fallbackHandler;
|
||||||
|
}
|
||||||
|
|
||||||
public function url(string $name, array $parameters = []): string
|
public function url(string $name, array $parameters = []): string
|
||||||
{
|
{
|
||||||
$routes = $this->getRoutes();
|
$foundRoute = $this->routes->getByName($name);
|
||||||
$foundRoute = null;
|
|
||||||
|
|
||||||
foreach ($routes as $routeDefinition) {
|
|
||||||
if ($routeDefinition->getName() === $name) {
|
|
||||||
$foundRoute = $routeDefinition;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($foundRoute === null) {
|
if ($foundRoute === null) {
|
||||||
throw new RouteNotFoundException(sprintf('Route "%s" not found for URL generation', $name));
|
throw new RouteNotFoundException(sprintf('Route "%s" not found for URL generation', $name));
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $foundRoute->getPath();
|
$path = $foundRoute->getPath();
|
||||||
$path = $this->replaceParameters($path, $parameters);
|
$path = $this->replaceParameters($path, $parameters, $foundRoute->getDefaults());
|
||||||
|
|
||||||
return $path;
|
return $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function replaceParameters(string $path, array $parameters, array $defaults = []): string
|
||||||
* Replaces {{param}} placeholders in a path.
|
|
||||||
*
|
|
||||||
* @param string $path Path string with placeholders
|
|
||||||
* @param array $parameters Parameter values
|
|
||||||
* @return string Path with parameters replaced
|
|
||||||
*/
|
|
||||||
private function replaceParameters(string $path, array $parameters): string
|
|
||||||
{
|
{
|
||||||
foreach ($parameters as $key => $value) {
|
if (!str_contains($path, '{{')) {
|
||||||
$pattern = '{{' . $key . '}}';
|
return $this->normalizePath($path);
|
||||||
$path = str_replace($pattern, $value, $path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $path;
|
preg_match_all('/\{\{([a-zA-Z0-9_]+)(\?)?\}\}/', $path, $matches);
|
||||||
|
$parameterNames = $matches[1];
|
||||||
|
$isOptional = $matches[2];
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
$path = str_replace($pattern, (string)$defaults[$name], $path);
|
||||||
|
} elseif ($isOptional[$index] === '?') {
|
||||||
|
$path = str_replace($pattern, '', $path);
|
||||||
|
} else {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Missing required parameter "%s" for route URL generation', $name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizePath($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new route group for nested routing.
|
|
||||||
*
|
|
||||||
* @param array $options Group options including prefix and middleware
|
|
||||||
* @return RouteGroup Route group instance
|
|
||||||
*/
|
|
||||||
public function group(array $options): RouteGroup
|
public function group(array $options): RouteGroup
|
||||||
{
|
{
|
||||||
return new RouteGroup($options, $this);
|
return new RouteGroup($options, $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function module(string|array $identifier, string|null $prefix = null): self
|
||||||
* Auto-discovers and registers routes from modules.
|
|
||||||
*
|
|
||||||
* @param string|array $identifier Module identifier or array containing identifier and options
|
|
||||||
* @return self Fluent interface
|
|
||||||
* @throws MissingConfigurationException If modules_path not configured
|
|
||||||
*/
|
|
||||||
public function module(string|array $identifier): self
|
|
||||||
{
|
{
|
||||||
$identifier = is_string($identifier) ? [$identifier] : $identifier;
|
$this->loader->load($identifier, $prefix);
|
||||||
|
|
||||||
$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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads and registers routes from a module routes file.
|
|
||||||
*
|
|
||||||
* @param string $routesFile Path to routes.php file
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an iterator over registered routes.
|
|
||||||
*
|
|
||||||
* @return \Traversable Array iterator over route collection
|
|
||||||
*/
|
|
||||||
public function getIterator(): \Traversable
|
|
||||||
{
|
|
||||||
return new \ArrayIterator($this->routes ?? []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
namespace Atlas\Tests\Unit;
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Exception\RouteNotFoundException;
|
||||||
|
use Atlas\Router\RouteDefinition;
|
||||||
use Atlas\Router\Router;
|
use Atlas\Router\Router;
|
||||||
|
use Atlas\Config\Config;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
@ -11,11 +14,11 @@ final class ErrorHandlingTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testMatchOrFailThrowsExceptionWhenNoRouteFound(): void
|
public function testMatchOrFailThrowsExceptionWhenNoRouteFound(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new Router($config);
|
||||||
|
|
||||||
$uri = $this->createMock(UriInterface::class);
|
$uri = $this->createMock(UriInterface::class);
|
||||||
$uri->method('getPath')->willReturn('/nonexistent');
|
$uri->method('getPath')->willReturn('/nonexistent');
|
||||||
|
|
@ -27,18 +30,18 @@ final class ErrorHandlingTest extends TestCase
|
||||||
$request->method('getMethod')->willReturn('GET');
|
$request->method('getMethod')->willReturn('GET');
|
||||||
$request->method('getUri')->willReturn($uri);
|
$request->method('getUri')->willReturn($uri);
|
||||||
|
|
||||||
$this->expectException(\Atlas\Exception\NotFoundRouteException::class);
|
$this->expectException(RouteNotFoundException::class);
|
||||||
|
|
||||||
$router->matchOrFail($request);
|
$router->matchOrFail($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMatchReturnsNullWhenNoRouteFound(): void
|
public function testMatchReturnsNullWhenNoRouteFound(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new Router($config);
|
||||||
|
|
||||||
$uri = $this->createMock(UriInterface::class);
|
$uri = $this->createMock(UriInterface::class);
|
||||||
$uri->method('getPath')->willReturn('/nonexistent');
|
$uri->method('getPath')->willReturn('/nonexistent');
|
||||||
|
|
@ -57,38 +60,39 @@ final class ErrorHandlingTest extends TestCase
|
||||||
|
|
||||||
public function testRouteChainingWithDifferentHttpMethods(): void
|
public function testRouteChainingWithDifferentHttpMethods(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = new \Atlas\Router\Router($config)->get('/test', 'GetHandler')->post('/test', 'PostHandler');
|
$router = new Router($config);
|
||||||
|
$router->get('/test', 'GetHandler');
|
||||||
|
$router->post('/test', 'PostHandler');
|
||||||
|
|
||||||
$this->assertTrue($result instanceof \Atlas\Router\Router);
|
$this->assertCount(2, $router->getRoutes());
|
||||||
$this->assertCount(2, $result->getRoutes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMatchUsingRouteDefinition(): void
|
public function testMatchUsingRouteDefinition(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new Router($config);
|
||||||
|
|
||||||
$router->get('/test', 'TestMethod');
|
$router->get('/test', 'TestMethod');
|
||||||
|
|
||||||
$routes = $router->getRoutes();
|
$routes = iterator_to_array($router->getRoutes());
|
||||||
$this->assertCount(1, $routes);
|
$this->assertCount(1, $routes);
|
||||||
$this->assertInstanceOf(\Atlas\Router\RouteDefinition::class, $routes[0]);
|
$this->assertInstanceOf(RouteDefinition::class, $routes[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCaseInsensitiveHttpMethodMatching(): void
|
public function testCaseInsensitiveHttpMethodMatching(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new Router($config);
|
||||||
|
|
||||||
$router->get('/test', 'TestHandler');
|
$router->get('/test', 'TestHandler');
|
||||||
|
|
||||||
|
|
@ -109,11 +113,11 @@ final class ErrorHandlingTest extends TestCase
|
||||||
|
|
||||||
public function testPathNormalizationLeadingSlashes(): void
|
public function testPathNormalizationLeadingSlashes(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new Router($config);
|
||||||
|
|
||||||
$router->get('/test', 'TestHandler');
|
$router->get('/test', 'TestHandler');
|
||||||
|
|
||||||
|
|
@ -134,11 +138,11 @@ final class ErrorHandlingTest extends TestCase
|
||||||
|
|
||||||
public function testMatchOrFailThrowsExceptionForMultipleRoutes(): void
|
public function testMatchOrFailThrowsExceptionForMultipleRoutes(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new Router($config);
|
||||||
|
|
||||||
$router->get('/test', 'TestHandler');
|
$router->get('/test', 'TestHandler');
|
||||||
|
|
||||||
|
|
@ -154,6 +158,15 @@ final class ErrorHandlingTest extends TestCase
|
||||||
|
|
||||||
$matchedRoute = $router->matchOrFail($request);
|
$matchedRoute = $router->matchOrFail($request);
|
||||||
|
|
||||||
$this->assertInstanceOf(\Atlas\Router\RouteDefinition::class, $matchedRoute);
|
$this->assertInstanceOf(RouteDefinition::class, $matchedRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleThrowsExceptionWhenModulesPathIsMissing(): void
|
||||||
|
{
|
||||||
|
$config = new Config([]);
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$this->expectException(\Atlas\Exception\MissingConfigurationException::class);
|
||||||
|
$router->module('User');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ class RouterBasicTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testRouterCanBeCreatedWithValidConfig(): void
|
public function testRouterCanBeCreatedWithValidConfig(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new \Atlas\Config\Config([
|
||||||
'modules_path' => ['/path/to/modules'],
|
'modules_path' => ['/path/to/modules'],
|
||||||
'routes_file' => 'routes.php'
|
'routes_file' => 'routes.php'
|
||||||
]);
|
]);
|
||||||
|
|
@ -21,7 +21,7 @@ class RouterBasicTest extends TestCase
|
||||||
|
|
||||||
public function testRouterCanCreateSimpleRoute(): void
|
public function testRouterCanCreateSimpleRoute(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new \Atlas\Config\Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -36,20 +36,21 @@ class RouterBasicTest extends TestCase
|
||||||
|
|
||||||
public function testRouterReturnsSameInstanceForChaining(): void
|
public function testRouterReturnsSameInstanceForChaining(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new \Atlas\Config\Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$router = new \Atlas\Router\Router($config);
|
$router = new \Atlas\Router\Router($config);
|
||||||
|
|
||||||
$result = $router->get('/get', 'handler')->post('/post', 'handler');
|
$router->get('/get', 'handler');
|
||||||
|
$router->post('/post', 'handler');
|
||||||
|
|
||||||
$this->assertTrue($result instanceof \Atlas\Router\Router);
|
$this->assertCount(2, $router->getRoutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRouteHasCorrectProperties(): void
|
public function testRouteHasCorrectProperties(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new \Atlas\Config\Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ class RouterBasicTest extends TestCase
|
||||||
|
|
||||||
$router->get('/test', 'test_handler', 'test_route');
|
$router->get('/test', 'test_handler', 'test_route');
|
||||||
|
|
||||||
$routes = $router->getRoutes();
|
$routes = iterator_to_array($router->getRoutes());
|
||||||
$route = $routes[0] ?? null;
|
$route = $routes[0] ?? null;
|
||||||
|
|
||||||
$this->assertInstanceOf(\Atlas\Router\RouteDefinition::class, $route);
|
$this->assertInstanceOf(\Atlas\Router\RouteDefinition::class, $route);
|
||||||
|
|
@ -72,7 +73,7 @@ class RouterBasicTest extends TestCase
|
||||||
|
|
||||||
public function testRouteNormalizesPath(): void
|
public function testRouteNormalizesPath(): void
|
||||||
{
|
{
|
||||||
$config = new \Atlas\Tests\Config\Config([
|
$config = new \Atlas\Config\Config([
|
||||||
'modules_path' => ['/path/to/modules']
|
'modules_path' => ['/path/to/modules']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -80,7 +81,7 @@ class RouterBasicTest extends TestCase
|
||||||
|
|
||||||
$router->get('/api/test', 'handler');
|
$router->get('/api/test', 'handler');
|
||||||
|
|
||||||
$routes = $router->getRoutes();
|
$routes = iterator_to_array($router->getRoutes());
|
||||||
$route = $routes[0] ?? null;
|
$route = $routes[0] ?? null;
|
||||||
|
|
||||||
$this->assertInstanceOf(\Atlas\Router\RouteDefinition::class, $route);
|
$this->assertInstanceOf(\Atlas\Router\RouteDefinition::class, $route);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue