Skip to content

Commit 7824da0

Browse files
committed
implements service routes command
1 parent d967922 commit 7824da0

File tree

4 files changed

+320
-2
lines changed

4 files changed

+320
-2
lines changed

config/routes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
$routes->plugin('CakeDC/Api', ['path' => '/api'], function ($routes) {
2020
$useVersioning = Configure::read('Api.useVersioning');
2121
$versionPrefix = Configure::read('Api.versionPrefix');
22-
$middlewares = Configure::read('Api.Middleware');
22+
$middlewares = Configure::read('Api.Middleware', []);
2323
$middlewareNames = array_keys($middlewares);
2424

2525
$routes->applyMiddleware(...$middlewareNames);

src/Command/ServiceRoutesCommand.php

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2016 - 2024, Cake Development Corporation (http://cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2016 - 2024, Cake Development Corporation (http://cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\Api\Command;
15+
16+
use Cake\Command\Command;
17+
use Cake\Console\Arguments;
18+
use Cake\Console\ConsoleIo;
19+
use Cake\Console\ConsoleOptionParser;
20+
use CakeDC\Api\Service\ServiceRegistry;
21+
22+
/**
23+
* Provides interactive CLI tools for CakeDC Api routing.
24+
*/
25+
class ServiceRoutesCommand extends Command
26+
{
27+
/**
28+
* @inheritDoc
29+
*/
30+
public static function defaultName(): string
31+
{
32+
return 'service routes';
33+
}
34+
35+
/**
36+
* Build the option parser.
37+
*
38+
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to update
39+
* @return \Cake\Console\ConsoleOptionParser
40+
*/
41+
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
42+
{
43+
$parser = parent::buildOptionParser($parser);
44+
$parser->setDescription(__('Display all routes in a service'));
45+
$parser->addArgument('service', [
46+
'help' => __('The name of the service to display routes for.'),
47+
'required' => true,
48+
]);
49+
50+
return $parser;
51+
}
52+
53+
/**
54+
* Display all routes in an application
55+
*
56+
* @param \Cake\Console\Arguments $args The command arguments.
57+
* @param \Cake\Console\ConsoleIo $io The console io
58+
* @return int|null The exit code or null for success
59+
*/
60+
public function execute(Arguments $args, ConsoleIo $io): ?int
61+
{
62+
$serviceName = $args->getArgument('service');
63+
$header = ['Route name', 'Method(s)', 'URI template', 'Service', 'Action', 'Plugin'];
64+
if ($args->getOption('verbose')) {
65+
$header[] = 'Defaults';
66+
}
67+
68+
$service = ServiceRegistry::getServiceLocator()->get($serviceName);
69+
if ($service === null) {
70+
$io->error(__('Service "{0}" not found', $serviceName));
71+
return Command::CODE_ERROR;
72+
}
73+
74+
$availableRoutes = $service->routes();
75+
76+
$output = $duplicateRoutesCounter = [];
77+
78+
foreach ($availableRoutes as $route) {
79+
$methods = isset($route->defaults['_method']) ? (array)$route->defaults['_method'] : [''];
80+
81+
$item = [
82+
$route->options['_name'] ?? $route->getName(),
83+
implode(', ', $methods),
84+
$route->template,
85+
$route->defaults['controller'] ?? '',
86+
$route->defaults['action'] ?? '',
87+
$route->defaults['plugin'] ?? '',
88+
];
89+
90+
if ($args->getOption('verbose')) {
91+
ksort($route->defaults);
92+
$item[] = json_encode($route->defaults);
93+
}
94+
95+
$output[] = $item;
96+
97+
foreach ($methods as $method) {
98+
if (!isset($duplicateRoutesCounter[$route->template][$method])) {
99+
$duplicateRoutesCounter[$route->template][$method] = 0;
100+
}
101+
102+
$duplicateRoutesCounter[$route->template][$method]++;
103+
}
104+
}
105+
106+
if ($args->getOption('sort')) {
107+
usort($output, function ($a, $b) {
108+
return strcasecmp($a[0], $b[0]);
109+
});
110+
}
111+
112+
array_unshift($output, $header);
113+
114+
$io->helper('table')->output($output);
115+
$io->out();
116+
117+
$duplicateRoutes = [];
118+
119+
foreach ($availableRoutes as $route) {
120+
$methods = isset($route->defaults['_method']) ? (array)$route->defaults['_method'] : [''];
121+
122+
foreach ($methods as $method) {
123+
if (
124+
$duplicateRoutesCounter[$route->template][$method] > 1 ||
125+
($method === '' && count($duplicateRoutesCounter[$route->template]) > 1) ||
126+
($method !== '' && isset($duplicateRoutesCounter[$route->template]['']))
127+
) {
128+
$duplicateRoutes[] = [
129+
$route->options['_name'] ?? $route->getName(),
130+
$route->template,
131+
$route->defaults['plugin'] ?? '',
132+
$route->defaults['prefix'] ?? '',
133+
$route->defaults['controller'] ?? '',
134+
$route->defaults['action'] ?? '',
135+
implode(', ', $methods),
136+
];
137+
138+
break;
139+
}
140+
}
141+
}
142+
143+
if ($duplicateRoutes) {
144+
array_unshift($duplicateRoutes, $header);
145+
$io->warning('The following possible route collisions were detected.');
146+
$io->helper('table')->output($duplicateRoutes);
147+
$io->out();
148+
}
149+
150+
return static::CODE_SUCCESS;
151+
}
152+
}

src/Plugin.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
namespace CakeDC\Api;
1515

16+
use Cake\Console\CommandCollection;
1617
use Cake\Core\BasePlugin;
1718
use Cake\Core\Configure;
19+
use CakeDC\Api\Command\ServiceRoutesCommand;
1820

1921
/**
2022
* Api plugin
@@ -26,7 +28,7 @@ class Plugin extends BasePlugin
2628
*/
2729
public function routes($routes): void
2830
{
29-
$middlewares = Configure::read('Api.Middleware');
31+
$middlewares = Configure::read('Api.Middleware', []);
3032
foreach ($middlewares as $alias => $middleware) {
3133
$class = $middleware['class'];
3234
if (array_key_exists('request', $middleware)) {
@@ -53,4 +55,15 @@ public function routes($routes): void
5355

5456
parent::routes($routes);
5557
}
58+
59+
/**
60+
* Add console commands for the plugin.
61+
*
62+
* @param \Cake\Console\CommandCollection $commands The command collection to update
63+
* @return \Cake\Console\CommandCollection
64+
*/
65+
public function console(CommandCollection $commands): CommandCollection
66+
{
67+
return $commands->add('service routes', ServiceRoutesCommand::class);
68+
}
5669
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeDC\Api\Test\TestCase\Command;
5+
6+
use Cake\Command\Command;
7+
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
8+
use Cake\TestSuite\TestCase;
9+
use CakeDC\Api\Service\ServiceRegistry;
10+
use CakeDC\Api\Test\ConfigTrait;
11+
12+
class ServiceRoutesCommandTest extends TestCase
13+
{
14+
use ConsoleIntegrationTestTrait;
15+
use ConfigTrait;
16+
17+
/**
18+
* setUp method
19+
*/
20+
public function setUp(): void
21+
{
22+
$this->_publicAccess();
23+
parent::setUp();
24+
$this->useCommandRunner();
25+
}
26+
27+
/**
28+
* tearDown method
29+
*/
30+
public function tearDown(): void
31+
{
32+
parent::tearDown();
33+
ServiceRegistry::getServiceLocator()->clear();
34+
}
35+
36+
/**
37+
* Test help output
38+
*/
39+
public function testServiceRoutesHelp(): void
40+
{
41+
$this->exec('service routes -h');
42+
$this->assertExitCode(Command::CODE_SUCCESS);
43+
$this->assertOutputContains('Display all routes in a service');
44+
$this->assertErrorEmpty();
45+
}
46+
47+
/**
48+
* Test basic route listing
49+
*/
50+
public function testServiceRoutesList(): void
51+
{
52+
$this->exec('service routes Articles');
53+
$this->assertExitCode(Command::CODE_SUCCESS);
54+
55+
// Assert table header
56+
$this->assertOutputContainsRow($this->getHeaderRow());
57+
58+
// Assert all routes
59+
foreach ($this->getArticleRoutes() as $route) {
60+
$this->assertOutputContainsRow($route);
61+
}
62+
}
63+
64+
private function getHeaderRow(): array
65+
{
66+
return [
67+
'<info>Route name</info>',
68+
'<info>Method(s)</info>',
69+
'<info>URI template</info>',
70+
'<info>Service</info>',
71+
'<info>Action</info>',
72+
'<info>Plugin</info>',
73+
];
74+
}
75+
76+
private function getArticleRoutes(): array
77+
{
78+
return [
79+
[
80+
'articles:untag',
81+
'PUT, POST',
82+
'/articles/untag/{id}',
83+
'articles',
84+
'untag',
85+
'',
86+
],
87+
[
88+
'articles:tag',
89+
'PUT, POST',
90+
'/articles/tag/{id}',
91+
'articles',
92+
'tag',
93+
'',
94+
],
95+
[
96+
'articles:view',
97+
'GET',
98+
'/articles/{id}',
99+
'articles',
100+
'view',
101+
'',
102+
],
103+
[
104+
'articles:edit',
105+
'PUT, PATCH',
106+
'/articles/{id}',
107+
'articles',
108+
'edit',
109+
'',
110+
],
111+
[
112+
'articles:delete',
113+
'DELETE',
114+
'/articles/{id}',
115+
'articles',
116+
'delete',
117+
'',
118+
],
119+
[
120+
'articles:describe',
121+
'OPTIONS',
122+
'/articles/{id}',
123+
'articles',
124+
'describe',
125+
'',
126+
],
127+
[
128+
'articles:index',
129+
'GET',
130+
'/articles',
131+
'articles',
132+
'index',
133+
'',
134+
],
135+
[
136+
'articles:add',
137+
'POST',
138+
'/articles',
139+
'articles',
140+
'add',
141+
'',
142+
],
143+
[
144+
'articles:describe',
145+
'OPTIONS',
146+
'/articles',
147+
'articles',
148+
'describe',
149+
'',
150+
],
151+
];
152+
}
153+
}

0 commit comments

Comments
 (0)