Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ The package does not bundle a concrete PSR-7 implementation. Applications or lat

Phase 4.6 closes the reviewed Phase 4 HTTP package foundation. HTML and JSON error rendering, problem-details DTOs, content negotiation, debug pages, automatic health routes, SAPI request creation, concrete response transmission, process termination/recycle adapters, runtime adapters, trace propagation and OpenTelemetry propagation remain deferred to later reviewed slices.

Phase 6.4C adds `Evolve\Http\Routing\Console\RouteListCommand` as a caller-registerable console command adapter for route inspection. The command receives an explicit `RouteCollection` through constructor injection and uses the existing Core Console API, keeping the dependency direction as HTTP -> Core.

`route:list` renders only configured route methods and paths. It preserves `RouteCollection` order, method order, method case and route path text exactly, and it does not invoke or expose route handlers. Empty collections write `No routes are configured.` to normal output.

The adapter accepts no arguments or options. Unsupported input writes `The route:list command does not accept arguments or options.` to error output and returns exit code `2` without rendering routes.

The command is not automatically registered in `packages/core/bin/evolve`, and this package does not provide application bootstrapping or automatic route discovery for it. Application-owned or skeleton CLI composition remains deferred.

## Publication Status

EvolvePHP 2 is pre-release. This package is not yet independently published, and the current canonical source is the EvolvePHP monorepo:
Expand Down
49 changes: 49 additions & 0 deletions packages/http/src/Routing/Console/RouteListCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Evolve\Http\Routing\Console;

use Evolve\Core\Console\Command;
use Evolve\Core\Console\CommandInput;
use Evolve\Core\Console\CommandOutput;
use Evolve\Core\Console\CommandResult;
use Evolve\Http\Routing\RouteCollection;

final readonly class RouteListCommand implements Command
{
public function __construct(private RouteCollection $routes) {}

public function name(): string
{
return 'route:list';
}

public function description(): string
{
return 'List configured HTTP routes.';
}

public function execute(CommandInput $input, CommandOutput $output): CommandResult
{
if ($input->tokens() !== []) {
$output->writeError('The route:list command does not accept arguments or options.');

return new CommandResult(2);
}

$routes = $this->routes->all();

if ($routes === []) {
$output->write('No routes are configured.');

return new CommandResult(0);
}

foreach ($routes as $route) {
$output->write(implode('|', $route->methods()) . ' ' . $route->path());
}

return new CommandResult(0);
}
}
141 changes: 141 additions & 0 deletions packages/http/tests/Unit/Routing/Console/RouteListCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Evolve\Http\Tests\Unit\Routing\Console;

use Evolve\Core\Console\CommandInput;
use Evolve\Core\Console\CommandOutput;
use Evolve\Http\Routing\Console\RouteListCommand;
use Evolve\Http\Routing\Route;
use Evolve\Http\Routing\RouteCollection;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class RouteListCommandTest extends TestCase
{
public function testCommandMetadataIsExact(): void
{
$command = $this->command([]);

self::assertTrue((new \ReflectionClass(RouteListCommand::class))->isFinal());
self::assertTrue((new \ReflectionClass(RouteListCommand::class))->isReadOnly());
self::assertSame('route:list', $command->name());
self::assertSame('List configured HTTP routes.', $command->description());
}

public function testEmptyCollectionEmitsMessageAndReturnsSuccess(): void
{
$output = new RecordingCommandOutput();
$result = $this->command([])->execute(new CommandInput([]), $output);

self::assertSame(0, $result->exitCode());
self::assertSame(['No routes are configured.'], $output->normal);
self::assertSame([], $output->error);
}

public function testRoutesAreRenderedWithExactMethodsPathsAndOrder(): void
{
$output = new RecordingCommandOutput();
$firstHandler = $this->handler();
$secondHandler = $this->handler();
$thirdHandler = $this->handler();
$command = $this->command([
new Route(['GET'], '/users', $firstHandler),
new Route(['post', 'PATCH'], '/mixed-case', $secondHandler),
new Route(['DELETE'], '/users/{id}', $thirdHandler),
]);

$result = $command->execute(new CommandInput([]), $output);

self::assertSame(0, $result->exitCode());
self::assertSame(
[
'GET /users',
'post|PATCH /mixed-case',
'DELETE /users/{id}',
],
$output->normal,
);
self::assertSame([], $output->error);
self::assertSame(0, $firstHandler->calls);
self::assertSame(0, $secondHandler->calls);
self::assertSame(0, $thirdHandler->calls);
}

public function testUnsupportedArgumentReturnsUsageErrorOnly(): void
{
$output = new RecordingCommandOutput();
$handler = $this->handler();
$result = $this->command([new Route(['GET'], '/users', $handler)])
->execute(new CommandInput(['--json']), $output);

self::assertSame(2, $result->exitCode());
self::assertSame([], $output->normal);
self::assertSame(['The route:list command does not accept arguments or options.'], $output->error);
self::assertSame(0, $handler->calls);
}

public function testMultipleUnsupportedTokensAreRejectedIdentically(): void
{
$output = new RecordingCommandOutput();
$result = $this->command([new Route(['GET'], '/users', $this->handler())])
->execute(new CommandInput(['--verbose', '/users']), $output);

self::assertSame(2, $result->exitCode());
self::assertSame([], $output->normal);
self::assertSame(['The route:list command does not accept arguments or options.'], $output->error);
}

/**
* @param list<Route> $routes
*/
private function command(array $routes): RouteListCommand
{
return new RouteListCommand(new RouteCollection($routes));
}

private function handler(): RecordingRouteListHandler
{
return new RecordingRouteListHandler($this->createStub(ResponseInterface::class));
}
}

final class RecordingCommandOutput implements CommandOutput
{
/**
* @var list<string>
*/
public array $normal = [];

/**
* @var list<string>
*/
public array $error = [];

public function write(string $message): void
{
$this->normal[] = $message;
}

public function writeError(string $message): void
{
$this->error[] = $message;
}
}

final class RecordingRouteListHandler implements RequestHandlerInterface
{
public int $calls = 0;

public function __construct(private readonly ResponseInterface $response) {}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$this->calls++;

return $this->response;
}
}
1 change: 1 addition & 0 deletions tests/Architecture/EvolvePhp2PackageSkeletonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ private function acceptedPackageSourceInventories()
'Middleware/MiddlewarePipeline.php',
'Response/ExecutionOutcomeResponseResolver.php',
'Response/ResponseEmitter.php',
'Routing/Console/RouteListCommand.php',
'Routing/Internal/RoutePattern.php',
'Routing/Route.php',
'Routing/RouteCollection.php',
Expand Down