Compare commits
4 commits
566ed2d878
...
eabe1af518
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eabe1af518 | ||
|
|
02dc9387ac | ||
|
|
f29b424c00 | ||
|
|
a0d9ac75d6 |
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,7 +4,9 @@ composer.lock
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
.junie/
|
||||||
/coverage/
|
/coverage/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.log
|
*.log
|
||||||
|
.junie/
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
# Atlas Routing: Development Guidelines
|
|
||||||
|
|
||||||
These guidelines ensure that all development by AI agents remains consistent with the project's standards for quality, maintainability, and architectural purity.
|
|
||||||
|
|
||||||
## 1. Execution Policy (CRITICAL)
|
|
||||||
- **Sequential Implementation**: Milestones defined in `MILESTONES.md` MUST be implemented one at a time.
|
|
||||||
- **No Auto-Advance**: Do not automatically move to the next milestone. Stop and wait for verification or explicit instruction after completing a milestone.
|
|
||||||
- **Strict Completion (Definition of Done)**: A milestone is NOT complete until:
|
|
||||||
- The full suite of tests passes.
|
|
||||||
- Zero deprecation warnings.
|
|
||||||
- Zero errors.
|
|
||||||
- Zero failures.
|
|
||||||
|
|
||||||
## 2. Core Requirements
|
|
||||||
- **PHP Version**: `^8.2`
|
|
||||||
- **Principles**:
|
|
||||||
- **SOLID**: Strict adherence to object-oriented design principles.
|
|
||||||
- **KISS**: Prefer simple solutions over clever ones.
|
|
||||||
- **DRY**: Minimize duplication by abstracting common logic.
|
|
||||||
- **YAGNI**: Avoid over-engineering; only implement what is actually required.
|
|
||||||
|
|
||||||
## 3. Coding Style & Architecture
|
|
||||||
- **Verbose Coding Style**: Code must be expressive and self-documenting. Use descriptive variable and method names.
|
|
||||||
- **Single Responsibility Principle (SRP)**:
|
|
||||||
- **Classes**: Each class must have one, and only one, reason to change.
|
|
||||||
- **Methods**: Each method should perform a single, well-defined task.
|
|
||||||
- **Type Safety**: Strictly use PHP 8.2+ type hinting for all properties, parameters, and return values.
|
|
||||||
- **Interoperability**: Prioritize PSR compliance (especially PSR-7 for HTTP messages).
|
|
||||||
|
|
||||||
## 4. Documentation & Quality Assurance
|
|
||||||
- **Well Documented**: Every public class and method must have comprehensive PHPDoc blocks.
|
|
||||||
- **Fully Tested**:
|
|
||||||
- Aim for high test coverage.
|
|
||||||
- Every bug fix must include a regression test.
|
|
||||||
- Every new feature must be accompanied by relevant tests.
|
|
||||||
- Use PHPUnit for the testing suite.
|
|
||||||
|
|
@ -65,8 +65,12 @@ $router->group(['prefix' => '/api', 'middleware' => ['auth']])->group(function($
|
||||||
$group->get('/settings', 'SettingsHandler');
|
$group->get('/settings', 'SettingsHandler');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
All group routes inherit whatever options you pass in (middleware, prefix, etc).
|
||||||
|
|
||||||
|
While the above syntax works and is completely viable, I find the double group method syntax a bit confusing.
|
||||||
|
|
||||||
|
So, here is another way you can do it (my personal preferred method) that is, in my opinion, cleaner, and more readable.
|
||||||
|
|
||||||
You can also save a route group to a variable for more flexible route definitions:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$api = $router->group(['prefix' => '/api']);
|
$api = $router->group(['prefix' => '/api']);
|
||||||
|
|
|
||||||
123
atlas
123
atlas
|
|
@ -4,8 +4,8 @@ require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
use Atlas\Router\Router;
|
use Atlas\Router\Router;
|
||||||
use Atlas\Config\Config;
|
use Atlas\Config\Config;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Atlas\Commands\ListRoutesCommand;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Atlas\Commands\TestRouteCommand;
|
||||||
|
|
||||||
$command = $argv[1] ?? 'help';
|
$command = $argv[1] ?? 'help';
|
||||||
|
|
||||||
|
|
@ -31,126 +31,11 @@ if (file_exists($bootstrapFile)) {
|
||||||
|
|
||||||
switch ($command) {
|
switch ($command) {
|
||||||
case 'route:list':
|
case 'route:list':
|
||||||
$json = in_array('--json', $argv);
|
(new ListRoutesCommand())->execute($router, $argv);
|
||||||
$routes = $router->getRoutes();
|
|
||||||
$output = [];
|
|
||||||
foreach ($routes as $route) {
|
|
||||||
$output[] = $route->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
echo json_encode($output, JSON_PRETTY_PRINT) . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
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'] ?? '',
|
|
||||||
is_string($r['handler']) ? $r['handler'] : 'Closure'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'route:test':
|
case 'route:test':
|
||||||
$method = $argv[2] ?? 'GET';
|
(new TestRouteCommand())->execute($router, $argv);
|
||||||
$path = $argv[3] ?? '/';
|
|
||||||
$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}"; }
|
|
||||||
};
|
|
||||||
|
|
||||||
$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() {
|
|
||||||
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 : []; }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$result = $router->inspect($request);
|
|
||||||
|
|
||||||
if ($result->isFound()) {
|
|
||||||
echo "Match Found!" . PHP_EOL;
|
|
||||||
echo "Route: " . $result->getRoute()->getName() . " [" . $result->getRoute()->getMethod() . " " . $result->getRoute()->getPath() . "]" . PHP_EOL;
|
|
||||||
echo "Parameters: " . json_encode($result->getParameters()) . PHP_EOL;
|
|
||||||
exit(0);
|
|
||||||
} else {
|
|
||||||
echo "No Match Found." . PHP_EOL;
|
|
||||||
if (in_array('--verbose', $argv)) {
|
|
||||||
echo "Diagnostics:" . PHP_EOL;
|
|
||||||
foreach ($result->getDiagnostics()['attempts'] as $attempt) {
|
|
||||||
echo " - {$attempt['route']}: {$attempt['status']} (Pattern: {$attempt['pattern']})" . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
44
src/Commands/ListRoutesCommand.php
Normal file
44
src/Commands/ListRoutesCommand.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Commands;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to list all registered routes in a table or JSON format.
|
||||||
|
*/
|
||||||
|
class ListRoutesCommand
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the command.
|
||||||
|
*
|
||||||
|
* @param Router $router The router instance
|
||||||
|
* @param array $argv CLI arguments
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function execute(Router $router, array $argv): void
|
||||||
|
{
|
||||||
|
$json = in_array('--json', $argv);
|
||||||
|
$routes = $router->getRoutes();
|
||||||
|
$output = [];
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$output[] = $route->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode($output, JSON_PRETTY_PRINT) . PHP_EOL;
|
||||||
|
} else {
|
||||||
|
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'] ?? '',
|
||||||
|
is_string($r['handler']) ? $r['handler'] : 'Closure'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/Commands/TestRouteCommand.php
Normal file
120
src/Commands/TestRouteCommand.php
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Commands;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to test a specific request against the routing table.
|
||||||
|
*/
|
||||||
|
class TestRouteCommand
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Executes the command.
|
||||||
|
*
|
||||||
|
* @param Router $router The router instance
|
||||||
|
* @param array $argv CLI arguments
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function execute(Router $router, array $argv): void
|
||||||
|
{
|
||||||
|
$method = $argv[2] ?? 'GET';
|
||||||
|
$path = $argv[3] ?? '/';
|
||||||
|
$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}"; }
|
||||||
|
};
|
||||||
|
|
||||||
|
$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() {
|
||||||
|
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 : []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$result = $router->inspect($request);
|
||||||
|
|
||||||
|
if ($result->isFound()) {
|
||||||
|
echo "Match Found!" . PHP_EOL;
|
||||||
|
echo "Route: " . $result->getRoute()->getName() . " [" . $result->getRoute()->getMethod() . " " . $result->getRoute()->getPath() . "]" . PHP_EOL;
|
||||||
|
echo "Parameters: " . json_encode($result->getParameters()) . PHP_EOL;
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
echo "No Match Found." . PHP_EOL;
|
||||||
|
if (in_array('--verbose', $argv)) {
|
||||||
|
echo "Diagnostics:" . PHP_EOL;
|
||||||
|
foreach ($result->getDiagnostics()['attempts'] as $attempt) {
|
||||||
|
echo " - {$attempt['route']}: {$attempt['status']} (Pattern: {$attempt['pattern']})" . PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,9 +31,7 @@ class RouteGroup
|
||||||
*/
|
*/
|
||||||
public static function create(array $options, Router $router): self
|
public static function create(array $options, Router $router): self
|
||||||
{
|
{
|
||||||
$self = new self($options);
|
return new self($options, $router);
|
||||||
$self->router = $router;
|
|
||||||
return $self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
public function get(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
||||||
|
|
@ -174,8 +172,13 @@ class RouteGroup
|
||||||
return $this->joinPaths($this->options['prefix'] ?? '', $path);
|
return $this->joinPaths($this->options['prefix'] ?? '', $path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function group(array $options): RouteGroup
|
public function group(array|callable $options): RouteGroup
|
||||||
{
|
{
|
||||||
|
if (is_callable($options)) {
|
||||||
|
$options($this);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
$prefix = $this->options['prefix'] ?? '';
|
$prefix = $this->options['prefix'] ?? '';
|
||||||
$newPrefix = $this->joinPaths($prefix, $options['prefix'] ?? '');
|
$newPrefix = $this->joinPaths($prefix, $options['prefix'] ?? '');
|
||||||
|
|
||||||
|
|
|
||||||
47
tests/Unit/RouteGroupClosureTest.php
Normal file
47
tests/Unit/RouteGroupClosureTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class RouteGroupClosureTest extends TestCase
|
||||||
|
{
|
||||||
|
private Router $router;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['modules_path' => ['/path/to/modules']]);
|
||||||
|
$this->router = new Router($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupSupportsClosureRegistration(): void
|
||||||
|
{
|
||||||
|
$this->router->group(['prefix' => '/api'])->group(function($group) {
|
||||||
|
$group->get('/users', 'UserHandler');
|
||||||
|
$group->post('/users', 'UserCreateHandler');
|
||||||
|
});
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(2, $routes);
|
||||||
|
$this->assertSame('/api/users', $routes[0]->getPath());
|
||||||
|
$this->assertSame('GET', $routes[0]->getMethod());
|
||||||
|
$this->assertSame('/api/users', $routes[1]->getPath());
|
||||||
|
$this->assertSame('POST', $routes[1]->getMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNestedGroupClosureInheritance(): void
|
||||||
|
{
|
||||||
|
$this->router->group(['prefix' => '/api', 'middleware' => ['auth']])->group(function($group) {
|
||||||
|
$group->group(['prefix' => '/v1'])->group(function($v1) {
|
||||||
|
$v1->get('/profile', 'ProfileHandler');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(1, $routes);
|
||||||
|
$this->assertSame('/api/v1/profile', $routes[0]->getPath());
|
||||||
|
$this->assertContains('auth', $routes[0]->getMiddleware());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue