diff --git a/README.md b/README.md index 58121e8..995b403 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,19 @@ The command supports several options, listed in the table below. | `--has-middleware` | `-w` | Accepts a comma-separated list of one or more middleware classes, and filters out routes that do not require those classes. The classes can be fully-qualified, unqualified, or a regular expression, supported by the preg_* functions. For example, "\Mezzio\Middleware\LazyLoadingMiddleware,LazyLoadingMiddleware,\Mezzio*". | +##### Configuration + +By default, `Mezzio\Tooling\Routes\DefaultRoutesConfigLoaderFactory` registers a `ConfigLoaderInterface` service with the application's DI container, which retrieves the application's routes from two sources: + +- `config/routes.php` +- Routes registered by any loaded `ConfigProvider` class + +However, this is a default/fallback implementation. +If you don't store any routes in _config/routes.php_ or need a custom implementation, then you need to do two things: + +1. Write a custom loader implementation that implements `Mezzio\Tooling\Routes\ConfigLoaderInterface` +2. Register it with the application's DI container as an alias for `Mezzio\Tooling\Routes\ConfigLoaderInterface` + ##### Usage Example Here is an example of what you can expect from running the command. diff --git a/composer.lock b/composer.lock index d7c20ab..6aedda8 100644 --- a/composer.lock +++ b/composer.lock @@ -285,16 +285,16 @@ }, { "name": "laminas/laminas-escaper", - "version": "2.14.0", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-escaper.git", - "reference": "0f7cb975f4443cf22f33408925c231225cfba8cb" + "reference": "c612b0488ae486284c39885efca494c180f16351" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/0f7cb975f4443cf22f33408925c231225cfba8cb", - "reference": "0f7cb975f4443cf22f33408925c231225cfba8cb", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/c612b0488ae486284c39885efca494c180f16351", + "reference": "c612b0488ae486284c39885efca494c180f16351", "shasum": "" }, "require": { @@ -306,12 +306,12 @@ "zendframework/zend-escaper": "*" }, "require-dev": { - "infection/infection": "^0.27.9", - "laminas/laminas-coding-standard": "~3.0.0", + "infection/infection": "^0.27.11", + "laminas/laminas-coding-standard": "~3.0.1", "maglnet/composer-require-checker": "^3.8.0", - "phpunit/phpunit": "^9.6.16", + "phpunit/phpunit": "^9.6.22", "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.21.1" + "vimeo/psalm": "^5.26.1" }, "type": "library", "autoload": { @@ -343,7 +343,7 @@ "type": "community_bridge" } ], - "time": "2024-10-24T10:12:53+00:00" + "time": "2024-12-17T19:39:54+00:00" }, { "name": "laminas/laminas-httphandlerrunner", @@ -5292,16 +5292,16 @@ }, { "name": "spatie/array-to-xml", - "version": "3.3.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876" + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", "shasum": "" }, "require": { @@ -5344,7 +5344,7 @@ "xml" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/3.3.0" + "source": "https://github.com/spatie/array-to-xml/tree/3.4.0" }, "funding": [ { @@ -5356,7 +5356,7 @@ "type": "github" } ], - "time": "2024-05-01T10:20:27+00:00" + "time": "2024-12-16T12:45:15+00:00" }, { "name": "squizlabs/php_codesniffer", diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index d802cd8..3ed30a8 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -25,8 +25,11 @@ use Mezzio\Tooling\Module\DeregisterCommandFactory; use Mezzio\Tooling\Module\RegisterCommand; use Mezzio\Tooling\Module\RegisterCommandFactory; +use Mezzio\Tooling\Routes\ConfigLoaderInterface; +use Mezzio\Tooling\Routes\DefaultRoutesConfigLoaderFactory; use Mezzio\Tooling\Routes\ListRoutesCommand; use Mezzio\Tooling\Routes\ListRoutesCommandFactory; +use Mezzio\Tooling\Routes\RoutesFileConfigLoader; final class ConfigProvider { @@ -65,6 +68,9 @@ public function getConsoleConfig(): array public function getDependencies(): array { return [ + 'aliases' => [ + ConfigLoaderInterface::class => RoutesFileConfigLoader::class, + ], 'factories' => [ Create::class => CreateFactory::class, CreateActionCommand::class => CreateActionCommandFactory::class, @@ -77,6 +83,7 @@ public function getDependencies(): array MigrateInteropMiddlewareCommand::class => MigrateInteropMiddlewareCommandFactory::class, MigrateMiddlewareToRequestHandlerCommand::class => MigrateMiddlewareToRequestHandlerCommandFactory::class, RegisterCommand::class => RegisterCommandFactory::class, + RoutesFileConfigLoader::class => DefaultRoutesConfigLoaderFactory::class, ], ]; } diff --git a/src/Routes/DefaultRoutesConfigLoaderFactory.php b/src/Routes/DefaultRoutesConfigLoaderFactory.php new file mode 100644 index 0000000..1722e6c --- /dev/null +++ b/src/Routes/DefaultRoutesConfigLoaderFactory.php @@ -0,0 +1,36 @@ +get(Application::class); + + /** @var MiddlewareFactory $factory */ + $factory = $container->get(MiddlewareFactory::class); + + return new RoutesFileConfigLoader( + 'config/routes.php', + $application, + $factory, + $container + ); + } +} diff --git a/src/Routes/Filter/RouteFilterOptions.php b/src/Routes/Filter/RouteFilterOptions.php index ef196a3..ad77051 100644 --- a/src/Routes/Filter/RouteFilterOptions.php +++ b/src/Routes/Filter/RouteFilterOptions.php @@ -4,85 +4,65 @@ namespace Mezzio\Tooling\Routes\Filter; -use function get_object_vars; +use function array_filter; +use function array_map; use function in_array; -use function is_array; -use function is_string; +use function strtoupper; -final class RouteFilterOptions +final class RouteFilterOptions implements RouteFilterOptionsInterface { - private string $middleware; - private string $name; - private string $path; + private array $allowedFilterOptions = ['methods', 'middleware', 'name', 'path']; + private array $methods; - /** @var array */ - private array $methods = []; - - /** - * @param string|array $methods - */ + /** @param list $methods */ public function __construct( - string $middleware = '', - string $name = '', - string $path = '', - $methods = [] + private string|null $middleware, + private string|null $name, + private string|null $path, + array $methods ) { - if (is_string($methods)) { - $this->methods = [$methods]; - } - if (is_array($methods)) { - $this->methods = $methods; - } - $this->middleware = $middleware; - $this->name = $name; - $this->path = $path; + $methods = array_filter($methods, 'strlen'); + $this->methods = array_map(static fn(string $value): string => strtoupper($value), $methods); + } + + private function getFilterOptionsMinusMethods(): array + { + return array_filter($this->allowedFilterOptions, fn($value) => $value !== "methods"); } public function has(string $filterOption): bool { - if (in_array($filterOption, ['middleware', 'name', 'path'])) { - return $this->$filterOption !== null; + if (! in_array($filterOption, $this->allowedFilterOptions)) { + return false; } - if ($filterOption === 'methods') { - return [] !== $this->methods; + if (in_array($filterOption, $this->getFilterOptionsMinusMethods())) { + return $this->$filterOption !== null; } - return false; + return $this->methods !== []; } - public function getMiddleware(): string + public function getMiddleware(): string|null { return $this->middleware; } - public function getName(): string + public function getName(): string|null { return $this->name; } - public function getPath(): string + public function getPath(): string|null { return $this->path; } /** - * @return array + * @return array */ public function getMethods(): array { return $this->methods; } - - public function toArray(): array - { - $values = []; - foreach (get_object_vars($this) as $key => $value) { - if (! empty($value)) { - $values[$key] = $value; - } - } - - return $values; - } } diff --git a/src/Routes/Filter/RouteFilterOptionsInterface.php b/src/Routes/Filter/RouteFilterOptionsInterface.php new file mode 100644 index 0000000..b7a2978 --- /dev/null +++ b/src/Routes/Filter/RouteFilterOptionsInterface.php @@ -0,0 +1,35 @@ + + */ + public function getMethods(): array; +} diff --git a/src/Routes/Filter/RoutesFilter.php b/src/Routes/Filter/RoutesFilter.php index cf210b1..699da7f 100644 --- a/src/Routes/Filter/RoutesFilter.php +++ b/src/Routes/Filter/RoutesFilter.php @@ -5,22 +5,16 @@ namespace Mezzio\Tooling\Routes\Filter; use ArrayIterator; -use Exception; use FilterIterator; -use Iterator; use Mezzio\Router\Route; +use Throwable; -use function array_filter; use function array_intersect; -use function array_walk; use function in_array; -use function is_array; -use function is_string; use function preg_match; use function sprintf; use function str_replace; use function stripos; -use function strtoupper; /** * RoutesFilter filters a traversable list of Route objects based on any of the four Route criteria, @@ -35,6 +29,14 @@ final class RoutesFilter extends FilterIterator { /** * @param ArrayIterator $routes + * @param RouteFilterOptionsInterface $filterOptions An array storing the list of route options to + * filter a route on along with their respective values. + * The four allowed route options are: name, path, method, + * and middleware. Name and path can be a fixed string, + * such as user.profile, or a regular expression, such as + * user.*. Middleware can only contain a class name. + * Method can be either a string which contains one of + * the allowed HTTP methods, or an array of HTTP methods. */ public function __construct( ArrayIterator $routes, @@ -46,18 +48,12 @@ public function __construct( * Middleware can only contain a class name. Method can be either a string which contains one of the * allowed HTTP methods, or an array of HTTP methods. */ - private array $filterOptions = [] + private RouteFilterOptions $filterOptions ) { parent::__construct($routes); - - // Filter out any options that are, effectively, "empty". - $this->filterOptions = array_filter( - $this->filterOptions, - fn($value) => ! empty($value) - ); } - public function getFilterOptions(): array + public function getFilterOptions(): RouteFilterOptionsInterface { return $this->filterOptions; } @@ -67,91 +63,65 @@ public function accept(): bool /** @var Route $route */ $route = $this->getInnerIterator()->current(); - if (empty($this->filterOptions)) { - return true; - } - - if (! empty($this->filterOptions['name'])) { - return $route->getName() === $this->filterOptions['name'] + if ($this->filterOptions->has("name")) { + return $route->getName() === $this->filterOptions->getName() || $this->matchesByRegex($route, 'name'); } - if (! empty($this->filterOptions['path'])) { - return $route->getPath() === $this->filterOptions['path'] + if ($this->filterOptions->has("path")) { + return $route->getPath() === $this->filterOptions->getPath() || $this->matchesByRegex($route, 'path'); } - if (! empty($this->filterOptions['method'])) { - return $this->matchesByMethod($route); + if ($this->filterOptions->has("middleware")) { + return $this->matchesByMiddleware($route); } - if (! empty($this->filterOptions['middleware'])) { - return $this->matchesByMiddleware($route); + if ($this->filterOptions->has("methods")) { + return $this->matchesByMethod($route); } - return false; + return true; } /** * Match the route against a regular expression based on the field in $matchType. * - * $matchType can be either "path" or "name". + * @param 'name'|'path' $routeAttribute */ - public function matchesByRegex(Route $route, string $routeAttribute): bool + private function matchesByRegex(Route $route, string $routeAttribute): bool { + if (! in_array($routeAttribute, ["name", "path"])) { + return false; + } + if ($routeAttribute === 'path') { - $path = (string) $this->filterOptions['path']; + $path = (string) $this->filterOptions->getPath(); return (bool) preg_match( sprintf("/^%s/", str_replace('/', '\/', $path)), $route->getPath() ); } - if ($routeAttribute === 'name') { - return (bool) preg_match( - sprintf( - "/%s/", - (string) $this->filterOptions['name'] - ), - $route->getName() - ); - } - - return false; + return (bool) preg_match( + sprintf("/%s/", (string) $this->filterOptions->getName()), + $route->getName() + ); } /** * Match if the current route supports the method(s) supplied. */ - public function matchesByMethod(Route $route): bool + private function matchesByMethod(Route $route): bool { if ($route->allowsAnyMethod()) { return true; } - if ($this->filterOptions['method'] === Route::HTTP_METHOD_ANY) { - return true; - } - - if (is_string($this->filterOptions['method'])) { - return in_array( - strtoupper($this->filterOptions['method']), - $route->getAllowedMethods() ?? [] - ); - } - - if (is_array($this->filterOptions['method'])) { - array_walk( - $this->filterOptions['method'], - fn(string &$value) => $value = strtoupper($value) - ); - return ! empty(array_intersect( - $this->filterOptions['method'], - $route->getAllowedMethods() ?? [] - )); - } - - return false; + return array_intersect( + $this->filterOptions->getMethods(), + $route->getAllowedMethods() + ) !== []; } /** @@ -163,10 +133,10 @@ public function matchesByMethod(Route $route): bool * the class' name. The intent is to perform checks from the least to the * most computationally expensive, to avoid excessive overhead. */ - public function matchesByMiddleware(Route $route): bool + private function matchesByMiddleware(Route $route): bool { $middlewareClass = $route->getMiddleware()::class; - $matchesMiddleware = (string) $this->filterOptions['middleware']; + $matchesMiddleware = $this->filterOptions->getMiddleware(); try { return $middlewareClass === $matchesMiddleware @@ -175,7 +145,7 @@ public function matchesByMiddleware(Route $route): bool sprintf('/%s/', $this->escapeNamespaceSeparatorForRegex($matchesMiddleware)), $middlewareClass ); - } catch (Exception) { + } catch (Throwable) { return false; } } diff --git a/src/Routes/ListRoutesCommand.php b/src/Routes/ListRoutesCommand.php index fecd2c5..e2ebf42 100644 --- a/src/Routes/ListRoutesCommand.php +++ b/src/Routes/ListRoutesCommand.php @@ -7,10 +7,10 @@ use ArrayIterator; use Mezzio\Router\Route; use Mezzio\Router\RouteCollector; +use Mezzio\Tooling\Routes\Filter\RouteFilterOptions; use Mezzio\Tooling\Routes\Filter\RoutesFilter; use Mezzio\Tooling\Routes\Sorter\RouteSorterByName; use Mezzio\Tooling\Routes\Sorter\RouteSorterByPath; -use Psr\Container\ContainerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; @@ -28,8 +28,7 @@ class ListRoutesCommand extends Command /** @var array */ private array $routes = []; - /** @var array */ - private array $filterOptions = []; + private RouteFilterOptions $filterOptions; private const HELP = <<<'EOT' Prints the application's routing table. @@ -80,7 +79,7 @@ class ListRoutesCommand extends Command public static $defaultName = 'mezzio:routes:list'; public function __construct( - private readonly ContainerInterface $container, + private readonly RouteCollector $routeCollector, private readonly ConfigLoaderInterface $configLoader ) { parent::__construct(); @@ -144,9 +143,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->configLoader->load(); - /** @var RouteCollector $routeCollector */ - $routeCollector = $this->container->get(RouteCollector::class); - $this->routes = $routeCollector->getRoutes(); + $this->routes = $this->routeCollector->getRoutes(); if ([] === $this->routes) { $output->writeln(self::MSG_EMPTY_ROUTING_TABLE); @@ -160,12 +157,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int : new RouteSorterByPath(); usort($this->routes, $sorter); - $this->filterOptions = [ - 'method' => strtolower((string) $input->getOption('supports-method')), - 'middleware' => strtolower((string) $input->getOption('has-middleware')), - 'name' => strtolower((string) $input->getOption('has-name')), - 'path' => strtolower((string) $input->getOption('has-path')), - ]; + $this->filterOptions = new RouteFilterOptions( + strtolower((string) $input->getOption('has-middleware')), + strtolower((string) $input->getOption('has-name')), + strtolower((string) $input->getOption('has-path')), + [strtolower((string) $input->getOption('supports-method'))], + ); switch ($format) { case 'json': diff --git a/src/Routes/ListRoutesCommandFactory.php b/src/Routes/ListRoutesCommandFactory.php index d38d88b..9fb4fe5 100644 --- a/src/Routes/ListRoutesCommandFactory.php +++ b/src/Routes/ListRoutesCommandFactory.php @@ -4,23 +4,19 @@ namespace Mezzio\Tooling\Routes; -use Mezzio\Application; -use Mezzio\MiddlewareFactory; -use Mezzio\Tooling\Routes\RoutesFileConfigLoader; +use Mezzio\Router\RouteCollector; use Psr\Container\ContainerInterface; final class ListRoutesCommandFactory { public function __invoke(ContainerInterface $container): ListRoutesCommand { - /** @var Application $application */ - $application = $container->get(Application::class); + /** @var RouteCollector $routeCollector */ + $routeCollector = $container->get(RouteCollector::class); - /** @var MiddlewareFactory $factory */ - $factory = $container->get(MiddlewareFactory::class); + /** @var ConfigLoaderInterface $configLoader */ + $configLoader = $container->get(ConfigLoaderInterface::class); - $configLoader = new RoutesFileConfigLoader('config/routes.php', $application, $factory, $container); - - return new ListRoutesCommand($container, $configLoader); + return new ListRoutesCommand($routeCollector, $configLoader); } } diff --git a/test/Routes/DefaultRoutesConfigLoaderFactoryTest.php b/test/Routes/DefaultRoutesConfigLoaderFactoryTest.php new file mode 100644 index 0000000..f6c348c --- /dev/null +++ b/test/Routes/DefaultRoutesConfigLoaderFactoryTest.php @@ -0,0 +1,35 @@ +createMock(ContainerInterface::class); + $container + ->expects($this->atMost(2)) + ->method("get") + ->willReturnOnConsecutiveCalls( + $this->createMock(Application::class), + $this->createMock(MiddlewareFactory::class), + ); + + $result = $factory($container); + + $this->assertInstanceOf(RoutesFileConfigLoader::class, $result); + } +} diff --git a/test/Routes/Filter/RouteFilterOptionsTest.php b/test/Routes/Filter/RouteFilterOptionsTest.php index 4d5dd85..efdfebd 100644 --- a/test/Routes/Filter/RouteFilterOptionsTest.php +++ b/test/Routes/Filter/RouteFilterOptionsTest.php @@ -37,30 +37,6 @@ public function testCanInitialiseOptionsCorrectly(): void $this->assertSame($options['methods'], $routeFilterOptions->getMethods()); } - /** - * @param array $options - * @param array $expectedResult - * @dataProvider initDataProvider - */ - public function testCanGetArrayRepresentation(array $options, array $expectedResult): void - { - /** @var string $middleware */ - $middleware = $options['middleware'] ?? ''; - - /** @var string $name */ - $name = $options['name'] ?? ''; - - /** @var string $path */ - $path = $options['path'] ?? ''; - - /** @var array $methods */ - $methods = $options['methods'] ?? []; - - $routeFilterOptions = new RouteFilterOptions($middleware, $name, $path, $methods); - - $this->assertSame($expectedResult, $routeFilterOptions->toArray()); - } - /** * @return array>>> */ diff --git a/test/Routes/ListRoutesCommandFactoryTest.php b/test/Routes/ListRoutesCommandFactoryTest.php index bf70900..138fb98 100644 --- a/test/Routes/ListRoutesCommandFactoryTest.php +++ b/test/Routes/ListRoutesCommandFactoryTest.php @@ -4,10 +4,11 @@ namespace MezzioTest\Tooling\Routes; -use Mezzio\Application; -use Mezzio\MiddlewareFactory; +use Mezzio\Router\RouteCollector; +use Mezzio\Tooling\Routes\ConfigLoaderInterface; use Mezzio\Tooling\Routes\ListRoutesCommand; use Mezzio\Tooling\Routes\ListRoutesCommandFactory; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; @@ -15,13 +16,14 @@ class ListRoutesCommandFactoryTest extends TestCase { public function testCanInstantiateListRoutesCommandObject(): void { + /** @var ContainerInterface&MockObject $container */ $container = $this->createMock(ContainerInterface::class); $container ->expects($this->atMost(2)) ->method('get') ->willReturnOnConsecutiveCalls( - $this->createMock(Application::class), - $this->createMock(MiddlewareFactory::class), + $this->createMock(RouteCollector::class), + $this->createMock(ConfigLoaderInterface::class), ); $factory = new ListRoutesCommandFactory(); diff --git a/test/Routes/ListRoutesCommandTest.php b/test/Routes/ListRoutesCommandTest.php index ae5a772..c1410bf 100644 --- a/test/Routes/ListRoutesCommandTest.php +++ b/test/Routes/ListRoutesCommandTest.php @@ -12,30 +12,19 @@ use MezzioTest\Tooling\Routes\Middleware\SimpleMiddleware; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; -use ReflectionMethod; -use Symfony\Component\Console\Formatter\OutputFormatter; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Tester\CommandTester; +use function array_key_exists; +use function implode; use function str_replace; use function strtoupper; class ListRoutesCommandTest extends TestCase { - /** @var (InputInterface&MockObject) */ - private $input; - - /** @var (ConsoleOutputInterface&MockObject) */ - private $output; - /** @var (RouteCollector&MockObject) */ - private $routeCollection; - - /** @var (ContainerInterface&MockObject) */ - private $container; + private $routeCollector; private ListRoutesCommand $command; @@ -44,21 +33,8 @@ protected function setUp(): void /** @var ConfigLoaderInterface $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); - /** @var ContainerInterface $container */ - $container = $this->createMock(ContainerInterface::class); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(ConsoleOutputInterface::class); - $this->routeCollection = $this->createMock(RouteCollector::class); - $this->command = new ListRoutesCommand($container, $configLoader); - } - - /** - * @throws ReflectionException - */ - private function reflectExecuteMethod(): ReflectionMethod - { - return new ReflectionMethod($this->command, 'execute'); + $this->routeCollector = $this->createMock(RouteCollector::class); + $this->command = new ListRoutesCommand($this->routeCollector, $configLoader); } public function testConfigureSetsExpectedDescription(): void @@ -119,157 +95,96 @@ public function testConfigureSetsExpectedOptions(): void */ public function testSuccessfulExecutionEmitsExpectedOutput(): void { - $routes = [ + $routes = [ new Route("/", new SimpleMiddleware(), ['GET'], 'home'), new Route("/", new ExpressMiddleware(), ['GET'], 'home'), ]; + /** @var RouteCollector&MockObject $collector */ $collector = $this->createMock(RouteCollector::class); $collector ->expects($this->once()) ->method('getRoutes') ->willReturn($routes); - /** @var ContainerInterface&MockObject $container */ - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->once()) - ->method('get') - ->with(RouteCollector::class) - ->willReturn($collector); - /** @var ConfigLoaderInterface&MockObject $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); - $this->command = new ListRoutesCommand($container, $configLoader); - - $outputFormatter = new OutputFormatter(false); + $this->command = new ListRoutesCommand($collector, $configLoader); - $this->output - ->method('getFormatter') - ->willReturn($outputFormatter); + $command = new CommandTester($this->command); + $command->execute([]); - // phpcs:disable Generic.Files.LineLength - $this->output - ->expects($this->atMost(7)) - ->method('writeln'); - // phpcs:enable - $this->input - ->method('getOption') - ->willReturnOnConsecutiveCalls( - 'table', - 'name', - false, - false, - false, - false - ); + $command->assertCommandIsSuccessful(); + $output = $command->getDisplay(); + $commandOutput = <<reflectExecuteMethod(); +EOF; - self::assertSame( - 0, - $method->invoke( - $this->command, - $this->input, - $this->output - ) - ); + $this->assertSame($commandOutput, $output); } public function testRendersAnEmptyResultWhenNoRoutesArePresent(): void { + /** @var RouteCollector&MockObject $collector */ $collector = $this->createMock(RouteCollector::class); - /** @var ContainerInterface&MockObject $container */ - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->once()) - ->method('get') - ->with(RouteCollector::class) - ->willReturn($collector); - /** @var ConfigLoaderInterface&MockObject $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); $configLoader ->expects($this->once()) ->method('load'); - $this->command = new ListRoutesCommand($container, $configLoader); + $this->command = new ListRoutesCommand($collector, $configLoader); - $this->input - ->method('getOption') - ->with('format') - ->willReturnOnConsecutiveCalls('table', false); - $this->output - ->expects($this->once()) - ->method('writeln') - ->with( - "There are no routes in the application's routing table." - ); + $command = new CommandTester($this->command); + $command->execute([]); - $method = $this->reflectExecuteMethod(); - - self::assertSame( - 0, - $method->invoke( - $this->command, - $this->input, - $this->output - ) - ); + $command->assertCommandIsSuccessful(); + $output = $command->getDisplay(); + $commandOutput = "There are no routes in the application's routing table.\n"; + $this->assertSame($commandOutput, $output); } public function testRendersRoutesAsJsonWhenFormatSetToJson(): void { - $routes = [ + $routes = [ new Route("/", new SimpleMiddleware(), ['GET'], 'home'), new Route("/", new ExpressMiddleware(), ['GET'], 'home'), ]; + /** @var RouteCollector&MockObject $collector */ $collector = $this->createMock(RouteCollector::class); $collector ->expects($this->once()) ->method('getRoutes') ->willReturn($routes); - /** @var ContainerInterface&MockObject $container */ - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->once()) - ->method('get') - ->with(RouteCollector::class) - ->willReturn($collector); - /** @var ConfigLoaderInterface&MockObject $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); $configLoader ->expects($this->once()) ->method('load'); - $this->command = new ListRoutesCommand($container, $configLoader); - - $this->input - ->method('getOption') - ->willReturnOnConsecutiveCalls( - 'json', // format - false, // supports-method - false, // has-middleware - false, // has-name - false, // has-path - false - ); - $this->output - ->expects($this->atMost(2)) - ->method('writeln'); - - $method = $this->reflectExecuteMethod(); - - self::assertSame( - 0, - $method->invoke( - $this->command, - $this->input, - $this->output - ) - ); + $this->command = new ListRoutesCommand($collector, $configLoader); + $command = new CommandTester($this->command); + $command->execute([ + '--format' => 'json', + ]); + + $command->assertCommandIsSuccessful(); + $output = $command->getDisplay(); + // phpcs:disable Generic.Files.LineLength + $commandOutput = <<assertSame($output, $commandOutput); } /** @@ -278,55 +193,30 @@ public function testRendersRoutesAsJsonWhenFormatSetToJson(): void */ public function testThatOnlyAllowedFormatsCanBeSupplied(string $format): void { - $routes = [ + $routes = [ new Route("/", new SimpleMiddleware(), ['GET'], 'home'), new Route("/", new ExpressMiddleware(), ['GET'], 'home'), ]; + + /** @var RouteCollector&MockObject $collector */ $collector = $this->createMock(RouteCollector::class); $collector ->expects($this->once()) ->method('getRoutes') ->willReturn($routes); - /** @var ContainerInterface&MockObject $container */ - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->once()) - ->method('get') - ->with(RouteCollector::class) - ->willReturn($collector); - /** @var ConfigLoaderInterface $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); - $this->command = new ListRoutesCommand($container, $configLoader); - - $this->input - ->method('getOption') - ->willReturnOnConsecutiveCalls( - $format, // format - false, // has-middleware - false, // supports-method - false, // has-name - false, // has-path - false // sort - ); - $this->output - ->expects($this->once()) - ->method('writeln') - ->with( - "Invalid output format supplied. Valid options are 'table' and 'json'" - ); + $this->command = new ListRoutesCommand($collector, $configLoader); - $method = $this->reflectExecuteMethod(); + $command = new CommandTester($this->command); + $command->execute([ + '--format' => $format, + ]); - self::assertSame( - -1, - $method->invoke( - $this->command, - $this->input, - $this->output - ) - ); + $output = $command->getDisplay(); + $commandOutput = "Invalid output format supplied. Valid options are 'table' and 'json'\n"; + $this->assertSame($commandOutput, $output); } /** @@ -354,54 +244,31 @@ public static function invalidFormatDataProvider(): array * @dataProvider sortRoutingTableDataProvider * @throws ReflectionException */ - public function testCanSortResults(string $sortOrder): void + public function testCanSortResults(string $sortOrder, string $commandOutput): void { - $routes = [ + $routes = [ new Route("/contact", new SimpleMiddleware(), ['GET'], 'contact'), new Route("/", new ExpressMiddleware(), ['GET'], 'home'), ]; - $this->routeCollection = $this->createMock(RouteCollector::class); - $this->routeCollection + $this->routeCollector = $this->createMock(RouteCollector::class); + $this->routeCollector ->expects($this->once()) ->method('getRoutes') ->willReturn($routes); - $this->container = $this->createMock(ContainerInterface::class); - $this->container - ->expects($this->once()) - ->method('get') - ->willReturn( - $this->routeCollection, - ); - /** @var ConfigLoaderInterface $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); - $this->command = new ListRoutesCommand($this->container, $configLoader); - - $this->input - ->method('getOption') - ->willReturnOnConsecutiveCalls( - 'json', // format - $sortOrder, // sort - false, // supports-method - false, // has-middleware - false, // has-name - false // has-path - ); - $this->output - ->expects($this->atMost(2)) - ->method('writeln'); - - $method = $this->reflectExecuteMethod(); - - self::assertSame( - 0, - $method->invoke( - $this->command, - $this->input, - $this->output - ) - ); + $this->command = new ListRoutesCommand($this->routeCollector, $configLoader); + + $command = new CommandTester($this->command); + $command->execute([ + '--format' => "json", + '--sort' => $sortOrder, + ]); + + $command->assertCommandIsSuccessful(); + $output = $command->getDisplay(); + $this->assertSame($commandOutput, $output); } /** @@ -413,11 +280,17 @@ public static function sortRoutingTableDataProvider(): array return [ [ 'name', - '[{"name":"contact","path":"\/contact","methods":"GET","middleware":"MezzioTest\\\\Tooling\\\\Routes\\\\Middleware\\\\SimpleMiddleware"},{"name":"home","path":"\/","methods":"GET","middleware":"MezzioTest\\\\Tooling\\\\Routes\\\\Middleware\\\\ExpressMiddleware"}]', + <<createMock(RouteCollector::class); - $routeCollection + /** @var RouteCollector&MockObject $routeCollector */ + $routeCollector = $this->createMock(RouteCollector::class); + $routeCollector ->expects($this->once()) ->method('getRoutes') ->willReturn($routes); - /** @var ContainerInterface&MockObject $container */ - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->once()) - ->method('get') - ->with(RouteCollector::class) - ->willReturn($routeCollection); - /** @var ConfigLoaderInterface $configLoader */ $configLoader = $this->createMock(ConfigLoaderInterface::class); - $this->command = new ListRoutesCommand($container, $configLoader); - - $this->input - ->method('getOption') - ->willReturnOnConsecutiveCalls( - 'json', // format - false, // sort - false, // supports-method - $filterOptions['middleware'], // has-middleware - false, // has-name - false // has-path - ); + $this->command = new ListRoutesCommand($routeCollector, $configLoader); - $this->output - ->expects($this->atMost(2)) - ->method('writeln'); - - $method = $this->reflectExecuteMethod(); + $command = new CommandTester($this->command); + $commandOptions = [ + '--format' => "json", + ]; + if (array_key_exists("middleware", $filterOptions)) { + $commandOptions['--has-middleware'] = implode(",", $filterOptions["middleware"]); + } + $command->execute($commandOptions); - self::assertSame( - 0, - $method->invoke( - $this->command, - $this->input, - $this->output - ) - ); + $output = $command->getDisplay(); + $this->assertSame($commandOutput, $output); + $command->assertCommandIsSuccessful(); } /** @@ -486,7 +339,11 @@ public static function filterRoutingTableDataProvider(): array // phpcs:disable Generic.Files.LineLength return [ [ - ['middleware' => 'ExpressMiddleware'], + ['middleware' => ['ExpressMiddleware']], + <<routes), - [ - 'middleware' => null, - 'name' => '', - 'path' => '/user', - ] - ); - - $this->assertSame( - ['path' => '/user'], - $routeFilter->getFilterOptions() - ); - } - /** - * @param array $filterOptions * @dataProvider validFilterDataProvider */ public function testCanFilterRoutesWithStringSearchExpression( int $expectedNumberOfRoutes, - array $filterOptions = [] + RouteFilterOptionsInterface $filterOptions ): void { $this->setUp(); @@ -93,139 +77,205 @@ public function testCanFilterRoutesWithStringSearchExpression( $this->assertCount( $expectedNumberOfRoutes, $routeFilter, - sprintf( - 'Filtered with %s', - var_export($filterOptions, true) - ) + sprintf('Filtered with %s', var_export($filterOptions, true)) ); } /** - * @psalm-return array}> + * @psalm-return array */ public static function validFilterDataProvider(): array { return [ 'middleware-simple-compound-name' => [ 5, - [ - 'middleware' => 'ExpressMiddleware', - ], + new RouteFilterOptions( + middleware: 'ExpressMiddleware', + name: null, + path: null, + methods: [], + ), ], 'middleware-simple-class-name' => [ 6, - [ - 'middleware' => 'Tooling', - ], + new RouteFilterOptions( + middleware: 'Tooling', + name: null, + path: null, + methods: [], + ), ], 'middleware-regex' => [ 6, - [ - 'middleware' => 'Tooling.*Middleware', - ], + new RouteFilterOptions( + middleware: 'Tooling.*Middleware', + name: null, + path: null, + methods: [], + ), ], 'middleware-fqcn' => [ 5, - [ - 'middleware' => ExpressMiddleware::class, - ], + new RouteFilterOptions( + middleware: ExpressMiddleware::class, + name: null, + path: null, + methods: [], + ), ], 'name-bare' => [ 1, - [ - 'name' => 'home', - ], + new RouteFilterOptions( + name: 'home', + middleware: null, + path: null, + methods: [], + ), ], 'name-regex' => [ 5, - [ - 'name' => 'user.*', - ], + new RouteFilterOptions( + name: 'user.*', + middleware: null, + path: null, + methods: [], + ), ], 'path-fq' => [ 1, - [ - 'path' => '/user', - ], + new RouteFilterOptions( + path: '/user', + middleware: null, + name: null, + methods: [], + ), ], 'path-fq-regex' => [ 4, - [ - 'path' => '/log.*', - ], + new RouteFilterOptions( + path: '/log.*', + middleware: null, + name: null, + methods: [], + ), ], 'path-root' => [ 6, - [ - 'path' => '/', - ], + new RouteFilterOptions( + path: '/', + middleware: null, + name: null, + methods: [], + ), ], 'method-get' => [ 6, - [ - 'method' => 'GET', - ], + new RouteFilterOptions( + methods: ['get'], + middleware: null, + name: null, + path: null, + ), ], 'method-any' => [ 6, - [ - 'method' => Route::HTTP_METHOD_ANY, - ], + new RouteFilterOptions( + methods: [Route::HTTP_METHOD_ANY], + middleware: null, + name: null, + path: null, + ), ], 'method-get-lc' => [ 6, - [ - 'method' => 'get', - ], + new RouteFilterOptions( + methods: ['get'], + middleware: null, + name: null, + path: null, + ), ], 'method-post-lc' => [ 2, - [ - 'method' => 'post', - ], + new RouteFilterOptions( + methods: ['post'], + middleware: null, + name: null, + path: null, + ), ], - /*[ + [ 6, - [ - 'method' => ['POST', 'GET'], - ], + new RouteFilterOptions( + methods: ['POST', 'GET'], + middleware: null, + name: null, + path: null, + ), ], [ 2, - [ - 'method' => ['POST'], - ], + new RouteFilterOptions( + methods: ['POST'], + middleware: null, + name: null, + path: null, + ), + ], + [ + 6, + new RouteFilterOptions( + methods: ['get'], + middleware: null, + name: null, + path: null, + ), ], [ 6, - [ - 'method' => ['GET'], - ], + new RouteFilterOptions( + methods: ['GET'], + middleware: null, + name: null, + path: null, + ), ], [ 1, - [ - 'method' => ['PATCH'], - ], + new RouteFilterOptions( + methods: ['PATCH'], + middleware: null, + name: null, + path: null, + ), ], [ 2, - [ - 'method' => ['PATCH', 'POST'], - ], + new RouteFilterOptions( + methods: ['PATCH', 'POST'], + middleware: null, + name: null, + path: null, + ), ], [ 2, - [ - 'method' => ['patch', 'post'], - ], + new RouteFilterOptions( + methods: ['patch', 'post'], + middleware: null, + name: null, + path: null, + ), ], [ 1, - [ - 'method' => ['patch'], - ], - ],*/ + new RouteFilterOptions( + methods: ['patch'], + middleware: null, + name: null, + path: null, + ), + ], ]; } }