Merge pull request #42678 from nextcloud/refactor/app/remove-register-routes

refactor(App): Remove registerRoutes method
This commit is contained in:
Joas Schilling 2025-05-15 15:10:28 +02:00 committed by GitHub
commit 3e4ff2624c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 350 additions and 806 deletions

View file

@ -6,9 +6,6 @@
</NoInterfaceProperties>
</file>
<file src="lib/public/AppFramework/App.php">
<InternalMethod>
<code><![CDATA[new RouteConfig($this->container, $router, $routes)]]></code>
</InternalMethod>
<UndefinedClass>
<code><![CDATA[\OC]]></code>
</UndefinedClass>

View file

@ -3491,11 +3491,6 @@
<code><![CDATA[\OCA\Talk\Controller\PageController]]></code>
</UndefinedClass>
</file>
<file src="lib/private/AppFramework/Routing/RouteConfig.php">
<InvalidArrayOffset>
<code><![CDATA[$action['url-postfix']]]></code>
</InvalidArrayOffset>
</file>
<file src="lib/private/AppFramework/Services/AppConfig.php">
<MoreSpecificImplementedParamType>
<code><![CDATA[$default]]></code>
@ -4689,11 +4684,6 @@
<code><![CDATA[$this->request->server]]></code>
</NoInterfaceProperties>
</file>
<file src="lib/public/AppFramework/App.php">
<InternalMethod>
<code><![CDATA[new RouteConfig($this->container, $router, $routes)]]></code>
</InternalMethod>
</file>
<file src="lib/public/AppFramework/Http/Response.php">
<LessSpecificReturnStatement>
<code><![CDATA[array_merge($mergeWith, $this->headers)]]></code>

View file

@ -1034,7 +1034,6 @@ return array(
'OC\\AppFramework\\OCS\\V1Response' => $baseDir . '/lib/private/AppFramework/OCS/V1Response.php',
'OC\\AppFramework\\OCS\\V2Response' => $baseDir . '/lib/private/AppFramework/OCS/V2Response.php',
'OC\\AppFramework\\Routing\\RouteActionHandler' => $baseDir . '/lib/private/AppFramework/Routing/RouteActionHandler.php',
'OC\\AppFramework\\Routing\\RouteConfig' => $baseDir . '/lib/private/AppFramework/Routing/RouteConfig.php',
'OC\\AppFramework\\Routing\\RouteParser' => $baseDir . '/lib/private/AppFramework/Routing/RouteParser.php',
'OC\\AppFramework\\ScopedPsrLogger' => $baseDir . '/lib/private/AppFramework/ScopedPsrLogger.php',
'OC\\AppFramework\\Services\\AppConfig' => $baseDir . '/lib/private/AppFramework/Services/AppConfig.php',

View file

@ -1075,7 +1075,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\AppFramework\\OCS\\V1Response' => __DIR__ . '/../../..' . '/lib/private/AppFramework/OCS/V1Response.php',
'OC\\AppFramework\\OCS\\V2Response' => __DIR__ . '/../../..' . '/lib/private/AppFramework/OCS/V2Response.php',
'OC\\AppFramework\\Routing\\RouteActionHandler' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Routing/RouteActionHandler.php',
'OC\\AppFramework\\Routing\\RouteConfig' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Routing/RouteConfig.php',
'OC\\AppFramework\\Routing\\RouteParser' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Routing/RouteParser.php',
'OC\\AppFramework\\ScopedPsrLogger' => __DIR__ . '/../../..' . '/lib/private/AppFramework/ScopedPsrLogger.php',
'OC\\AppFramework\\Services\\AppConfig' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Services/AppConfig.php',

View file

@ -1,279 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\AppFramework\Routing;
use OC\AppFramework\DependencyInjection\DIContainer;
use OC\Route\Router;
/**
* Class RouteConfig
* @package OC\AppFramework\routing
*/
class RouteConfig {
/** @var DIContainer */
private $container;
/** @var Router */
private $router;
/** @var array */
private $routes;
/** @var string */
private $appName;
/** @var string[] */
private $controllerNameCache = [];
protected $rootUrlApps = [
'cloud_federation_api',
'core',
'files_sharing',
'files',
'profile',
'settings',
'spreed',
];
/**
* @param \OC\AppFramework\DependencyInjection\DIContainer $container
* @param \OC\Route\Router $router
* @param array $routes
* @internal param $appName
*/
public function __construct(DIContainer $container, Router $router, $routes) {
$this->routes = $routes;
$this->container = $container;
$this->router = $router;
$this->appName = $container['AppName'];
}
/**
* The routes and resource will be registered to the \OCP\Route\IRouter
*/
public function register() {
// parse simple
$this->processIndexRoutes($this->routes);
// parse resources
$this->processIndexResources($this->routes);
/*
* OCS routes go into a different collection
*/
$oldCollection = $this->router->getCurrentCollection();
$this->router->useCollection($oldCollection . '.ocs');
// parse ocs simple routes
$this->processOCS($this->routes);
// parse ocs simple routes
$this->processOCSResources($this->routes);
$this->router->useCollection($oldCollection);
}
private function processOCS(array $routes): void {
$ocsRoutes = $routes['ocs'] ?? [];
foreach ($ocsRoutes as $ocsRoute) {
$this->processRoute($ocsRoute, 'ocs.');
}
}
/**
* Creates one route base on the give configuration
* @param array $routes
* @throws \UnexpectedValueException
*/
private function processIndexRoutes(array $routes): void {
$simpleRoutes = $routes['routes'] ?? [];
foreach ($simpleRoutes as $simpleRoute) {
$this->processRoute($simpleRoute);
}
}
protected function processRoute(array $route, string $routeNamePrefix = ''): void {
$name = $route['name'];
$postfix = $route['postfix'] ?? '';
$root = $this->buildRootPrefix($route, $routeNamePrefix);
$url = $root . '/' . ltrim($route['url'], '/');
$verb = strtoupper($route['verb'] ?? 'GET');
$split = explode('#', $name, 2);
if (count($split) !== 2) {
throw new \UnexpectedValueException('Invalid route name: use the format foo#bar to reference FooController::bar');
}
[$controller, $action] = $split;
$controllerName = $this->buildControllerName($controller);
$actionName = $this->buildActionName($action);
/*
* The route name has to be lowercase, for symfony to match it correctly.
* This is required because smyfony allows mixed casing for controller names in the routes.
* To avoid breaking all the existing route names, registering and matching will only use the lowercase names.
* This is also safe on the PHP side because class and method names collide regardless of the casing.
*/
$routeName = strtolower($routeNamePrefix . $this->appName . '.' . $controller . '.' . $action . $postfix);
$router = $this->router->create($routeName, $url)
->method($verb);
// optionally register requirements for route. This is used to
// tell the route parser how url parameters should be matched
if (array_key_exists('requirements', $route)) {
$router->requirements($route['requirements']);
}
// optionally register defaults for route. This is used to
// tell the route parser how url parameters should be default valued
$defaults = [];
if (array_key_exists('defaults', $route)) {
$defaults = $route['defaults'];
}
$defaults['caller'] = [$this->appName, $controllerName, $actionName];
$router->defaults($defaults);
}
/**
* For a given name and url restful OCS routes are created:
* - index
* - show
* - create
* - update
* - destroy
*
* @param array $routes
*/
private function processOCSResources(array $routes): void {
$this->processResources($routes['ocs-resources'] ?? [], 'ocs.');
}
/**
* For a given name and url restful routes are created:
* - index
* - show
* - create
* - update
* - destroy
*
* @param array $routes
*/
private function processIndexResources(array $routes): void {
$this->processResources($routes['resources'] ?? []);
}
/**
* For a given name and url restful routes are created:
* - index
* - show
* - create
* - update
* - destroy
*
* @param array $resources
* @param string $routeNamePrefix
*/
protected function processResources(array $resources, string $routeNamePrefix = ''): void {
// declaration of all restful actions
$actions = [
['name' => 'index', 'verb' => 'GET', 'on-collection' => true],
['name' => 'show', 'verb' => 'GET'],
['name' => 'create', 'verb' => 'POST', 'on-collection' => true],
['name' => 'update', 'verb' => 'PUT'],
['name' => 'destroy', 'verb' => 'DELETE'],
];
foreach ($resources as $resource => $config) {
$root = $this->buildRootPrefix($config, $routeNamePrefix);
// the url parameter used as id to the resource
foreach ($actions as $action) {
$url = $root . '/' . ltrim($config['url'], '/');
$method = $action['name'];
$verb = strtoupper($action['verb'] ?? 'GET');
$collectionAction = $action['on-collection'] ?? false;
if (!$collectionAction) {
$url .= '/{id}';
}
if (isset($action['url-postfix'])) {
$url .= '/' . $action['url-postfix'];
}
$controller = $resource;
$controllerName = $this->buildControllerName($controller);
$actionName = $this->buildActionName($method);
$routeName = $routeNamePrefix . $this->appName . '.' . strtolower($resource) . '.' . $method;
$route = $this->router->create($routeName, $url)
->method($verb);
$route->defaults(['caller' => [$this->appName, $controllerName, $actionName]]);
}
}
}
private function buildRootPrefix(array $route, string $routeNamePrefix): string {
$defaultRoot = $this->appName === 'core' ? '' : '/apps/' . $this->appName;
$root = $route['root'] ?? $defaultRoot;
if ($routeNamePrefix !== '') {
// In OCS all apps are whitelisted
return $root;
}
if (!\in_array($this->appName, $this->rootUrlApps, true)) {
// Only allow root URLS for some apps
return $defaultRoot;
}
return $root;
}
/**
* Based on a given route name the controller name is generated
* @param string $controller
* @return string
*/
private function buildControllerName(string $controller): string {
if (!isset($this->controllerNameCache[$controller])) {
$this->controllerNameCache[$controller] = $this->underScoreToCamelCase(ucfirst($controller)) . 'Controller';
}
return $this->controllerNameCache[$controller];
}
/**
* Based on the action part of the route name the controller method name is generated
* @param string $action
* @return string
*/
private function buildActionName(string $action): string {
return $this->underScoreToCamelCase($action);
}
/**
* Underscored strings are converted to camel case strings
* @param string $str
* @return string
*/
private function underScoreToCamelCase(string $str): string {
$pattern = '/_[a-z]?/';
return preg_replace_callback(
$pattern,
function ($matches) {
return strtoupper(ltrim($matches[0], '_'));
},
$str);
}
}

View file

@ -76,7 +76,7 @@ class RouteParser {
$url = $root . '/' . ltrim($route['url'], '/');
$verb = strtoupper($route['verb'] ?? 'GET');
$split = explode('#', $name, 2);
$split = explode('#', $name, 3);
if (count($split) !== 2) {
throw new \UnexpectedValueException('Invalid route name: use the format foo#bar to reference FooController::bar');
}
@ -87,7 +87,7 @@ class RouteParser {
/*
* The route name has to be lowercase, for symfony to match it correctly.
* This is required because smyfony allows mixed casing for controller names in the routes.
* This is required because symfony allows mixed casing for controller names in the routes.
* To avoid breaking all the existing route names, registering and matching will only use the lowercase names.
* This is also safe on the PHP side because class and method names collide regardless of the casing.
*/

View file

@ -169,7 +169,7 @@ class Router implements IRouter {
$this->loadedApps['core'] = true;
$this->useCollection('root');
$this->setupRoutes($this->getAttributeRoutes('core'), 'core');
require __DIR__ . '/../../../core/routes.php';
$this->requireRouteFile(__DIR__ . '/../../../core/routes.php', 'core');
// Also add the OCS collection
$collection = $this->getCollection('root.ocs');

View file

@ -9,10 +9,7 @@ declare(strict_types=1);
*/
namespace OCP\AppFramework;
use OC\AppFramework\Routing\RouteConfig;
use OC\Route\Router;
use OC\ServerContainer;
use OCP\Route\IRouter;
use Psr\Log\LoggerInterface;
/**
@ -96,35 +93,6 @@ class App {
return $this->container;
}
/**
* This function is to be called to create single routes and restful routes based on the given $routes array.
*
* Example code in routes.php of tasks app (it will register two restful resources):
* $routes = array(
* 'resources' => array(
* 'lists' => array('url' => '/tasklists'),
* 'tasks' => array('url' => '/tasklists/{listId}/tasks')
* )
* );
*
* $a = new TasksApp();
* $a->registerRoutes($this, $routes);
*
* @param \OCP\Route\IRouter $router
* @param array $routes
* @since 6.0.0
* @suppress PhanAccessMethodInternal
* @deprecated 20.0.0 Just return an array from your routes.php
*/
public function registerRoutes(IRouter $router, array $routes) {
if (!($router instanceof Router)) {
throw new \RuntimeException('Can only setup routes with real router');
}
$routeConfig = new RouteConfig($this->container, $router, $routes);
$routeConfig->register();
}
/**
* This function is called by the routing component to fire up the frameworks dispatch mechanism.
*

View file

@ -0,0 +1,347 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\AppFramework\Routing;
use OC\AppFramework\Routing\RouteParser;
use Symfony\Component\Routing\Route as RoutingRoute;
use Symfony\Component\Routing\RouteCollection;
class RouteParserTest extends \Test\TestCase {
protected RouteParser $parser;
protected function setUp(): void {
$this->parser = new RouteParser();
}
public function testParseRoutes(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'],
['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('app1.folders.open'));
$this->assertArrayHasKey('app1.folders.create', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/create', 'POST', 'FoldersController', 'create', route: $collection->get('app1.folders.create'));
}
public function testParseRoutesRootApps(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'],
['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'core');
$this->assertArrayHasKey('core.folders.open', $collection->all());
$this->assertSimpleRoute('/{folderId}/open', 'GET', 'FoldersController', 'open', app: 'core', route: $collection->get('core.folders.open'));
$this->assertArrayHasKey('core.folders.create', $collection->all());
$this->assertSimpleRoute('/{folderId}/create', 'POST', 'FoldersController', 'create', app: 'core', route: $collection->get('core.folders.create'));
}
public function testParseRoutesWithResources(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'],
], 'resources' => [
'names' => ['url' => '/names'],
'folder_names' => ['url' => '/folder/names'],
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.open', $collection->all());
$this->assertSimpleResource('/apps/app1/folder/names', 'folder_names', 'FolderNamesController', 'app1', $collection);
$this->assertSimpleResource('/apps/app1/names', 'names', 'NamesController', 'app1', $collection);
}
public function testParseRoutesWithPostfix(): void {
$routes = ['routes' => [
['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'POST'],
['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'PUT', 'postfix' => '-edit']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.update', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/update', 'POST', 'FoldersController', 'update', route: $collection->get('app1.folders.update'));
$this->assertArrayHasKey('app1.folders.update-edit', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/update', 'PUT', 'FoldersController', 'update', route: $collection->get('app1.folders.update-edit'));
}
public function testParseRoutesKebabCaseAction(): void {
$routes = ['routes' => [
['name' => 'folders#open_folder', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.open_folder', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'openFolder', route: $collection->get('app1.folders.open_folder'));
}
public function testParseRoutesKebabCaseController(): void {
$routes = ['routes' => [
['name' => 'my_folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.my_folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'MyFoldersController', 'open', route: $collection->get('app1.my_folders.open'));
}
public function testParseRoutesLowercaseVerb(): void {
$routes = ['routes' => [
['name' => 'folders#delete', 'url' => '/{folderId}/delete', 'verb' => 'delete']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.delete', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/delete', 'DELETE', 'FoldersController', 'delete', route: $collection->get('app1.folders.delete'));
}
public function testParseRoutesMissingVerb(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/{folderId}/open']
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('app1.folders.open'));
}
public function testParseRoutesWithRequirements(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'requirements' => ['folderId' => '\d+']]
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', requirements: ['folderId' => '\d+'], route: $collection->get('app1.folders.open'));
}
public function testParseRoutesWithDefaults(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'defaults' => ['hello' => 'world']]
]];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertArrayHasKey('app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', defaults: ['hello' => 'world'], route: $collection->get('app1.folders.open'));
}
public function testParseRoutesInvalidName(): void {
$routes = ['routes' => [
['name' => 'folders', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$this->expectException(\UnexpectedValueException::class);
$this->parser->parseDefaultRoutes($routes, 'app1');
}
public function testParseRoutesInvalidName2(): void {
$routes = ['routes' => [
['name' => 'folders#open#action', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$this->expectException(\UnexpectedValueException::class);
$this->parser->parseDefaultRoutes($routes, 'app1');
}
public function testParseRoutesEmpty(): void {
$routes = ['routes' => []];
$collection = $this->parser->parseDefaultRoutes($routes, 'app1');
$this->assertEquals(0, $collection->count());
}
// OCS routes
public function testParseOcsRoutes(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'],
['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('ocs.app1.folders.open'));
$this->assertArrayHasKey('ocs.app1.folders.create', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/create', 'POST', 'FoldersController', 'create', route: $collection->get('ocs.app1.folders.create'));
}
public function testParseOcsRoutesRootApps(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'],
['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'core');
$this->assertArrayHasKey('ocs.core.folders.open', $collection->all());
$this->assertSimpleRoute('/{folderId}/open', 'GET', 'FoldersController', 'open', app: 'core', route: $collection->get('ocs.core.folders.open'));
$this->assertArrayHasKey('ocs.core.folders.create', $collection->all());
$this->assertSimpleRoute('/{folderId}/create', 'POST', 'FoldersController', 'create', app: 'core', route: $collection->get('ocs.core.folders.create'));
}
public function testParseOcsRoutesWithPostfix(): void {
$routes = ['ocs' => [
['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'POST'],
['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'PUT', 'postfix' => '-edit']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.update', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/update', 'POST', 'FoldersController', 'update', route: $collection->get('ocs.app1.folders.update'));
$this->assertArrayHasKey('ocs.app1.folders.update-edit', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/update', 'PUT', 'FoldersController', 'update', route: $collection->get('ocs.app1.folders.update-edit'));
}
public function testParseOcsRoutesKebabCaseAction(): void {
$routes = ['ocs' => [
['name' => 'folders#open_folder', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.open_folder', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'openFolder', route: $collection->get('ocs.app1.folders.open_folder'));
}
public function testParseOcsRoutesKebabCaseController(): void {
$routes = ['ocs' => [
['name' => 'my_folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.my_folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'MyFoldersController', 'open', route: $collection->get('ocs.app1.my_folders.open'));
}
public function testParseOcsRoutesLowercaseVerb(): void {
$routes = ['ocs' => [
['name' => 'folders#delete', 'url' => '/{folderId}/delete', 'verb' => 'delete']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.delete', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/delete', 'DELETE', 'FoldersController', 'delete', route: $collection->get('ocs.app1.folders.delete'));
}
public function testParseOcsRoutesMissingVerb(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/{folderId}/open']
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('ocs.app1.folders.open'));
}
public function testParseOcsRoutesWithRequirements(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'requirements' => ['folderId' => '\d+']]
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', requirements: ['folderId' => '\d+'], route: $collection->get('ocs.app1.folders.open'));
}
public function testParseOcsRoutesWithDefaults(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'defaults' => ['hello' => 'world']]
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.open', $collection->all());
$this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', defaults: ['hello' => 'world'], route: $collection->get('ocs.app1.folders.open'));
}
public function testParseOcsRoutesInvalidName(): void {
$routes = ['ocs' => [
['name' => 'folders', 'url' => '/{folderId}/open', 'verb' => 'GET']
]];
$this->expectException(\UnexpectedValueException::class);
$this->parser->parseOCSRoutes($routes, 'app1');
}
public function testParseOcsRoutesEmpty(): void {
$routes = ['ocs' => []];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertEquals(0, $collection->count());
}
public function testParseOcsRoutesWithResources(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'],
], 'ocs-resources' => [
'names' => ['url' => '/names', 'root' => '/core/something'],
'folder_names' => ['url' => '/folder/names'],
]];
$collection = $this->parser->parseOCSRoutes($routes, 'app1');
$this->assertArrayHasKey('ocs.app1.folders.open', $collection->all());
$this->assertOcsResource('/apps/app1/folder/names', 'folder_names', 'FolderNamesController', 'app1', $collection);
$this->assertOcsResource('/core/something/names', 'names', 'NamesController', 'app1', $collection);
}
protected function assertSimpleRoute(
string $path,
string $method,
string $controller,
string $action,
string $app = 'app1',
array $requirements = [],
array $defaults = [],
?RoutingRoute $route = null,
): void {
self::assertEquals($path, $route->getPath());
self::assertEqualsCanonicalizing([$method], $route->getMethods());
self::assertEqualsCanonicalizing($requirements, $route->getRequirements());
self::assertEquals([...$defaults, 'action' => null, 'caller' => [$app, $controller, $action]], $route->getDefaults());
}
protected function assertSimpleResource(
string $path,
string $resourceName,
string $controller,
string $app,
RouteCollection $collection,
): void {
self::assertArrayHasKey("$app.$resourceName.index", $collection->all());
self::assertArrayHasKey("$app.$resourceName.show", $collection->all());
self::assertArrayHasKey("$app.$resourceName.create", $collection->all());
self::assertArrayHasKey("$app.$resourceName.update", $collection->all());
self::assertArrayHasKey("$app.$resourceName.destroy", $collection->all());
$this->assertSimpleRoute($path, 'GET', $controller, 'index', $app, route: $collection->get("$app.$resourceName.index"));
$this->assertSimpleRoute($path, 'POST', $controller, 'create', $app, route: $collection->get("$app.$resourceName.create"));
$this->assertSimpleRoute("$path/{id}", 'GET', $controller, 'show', $app, route: $collection->get("$app.$resourceName.show"));
$this->assertSimpleRoute("$path/{id}", 'PUT', $controller, 'update', $app, route: $collection->get("$app.$resourceName.update"));
$this->assertSimpleRoute("$path/{id}", 'DELETE', $controller, 'destroy', $app, route: $collection->get("$app.$resourceName.destroy"));
}
protected function assertOcsResource(
string $path,
string $resourceName,
string $controller,
string $app,
RouteCollection $collection,
): void {
self::assertArrayHasKey("ocs.$app.$resourceName.index", $collection->all());
self::assertArrayHasKey("ocs.$app.$resourceName.show", $collection->all());
self::assertArrayHasKey("ocs.$app.$resourceName.create", $collection->all());
self::assertArrayHasKey("ocs.$app.$resourceName.update", $collection->all());
self::assertArrayHasKey("ocs.$app.$resourceName.destroy", $collection->all());
$this->assertSimpleRoute($path, 'GET', $controller, 'index', $app, route: $collection->get("ocs.$app.$resourceName.index"));
$this->assertSimpleRoute($path, 'POST', $controller, 'create', $app, route: $collection->get("ocs.$app.$resourceName.create"));
$this->assertSimpleRoute("$path/{id}", 'GET', $controller, 'show', $app, route: $collection->get("ocs.$app.$resourceName.show"));
$this->assertSimpleRoute("$path/{id}", 'PUT', $controller, 'update', $app, route: $collection->get("ocs.$app.$resourceName.update"));
$this->assertSimpleRoute("$path/{id}", 'DELETE', $controller, 'destroy', $app, route: $collection->get("ocs.$app.$resourceName.destroy"));
}
}

View file

@ -1,477 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\AppFramework\Routing;
use OC\AppFramework\DependencyInjection\DIContainer;
use OC\AppFramework\Routing\RouteConfig;
use OC\Route\Route;
use OC\Route\Router;
use OCP\App\IAppManager;
use OCP\Diagnostics\IEventLogger;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Route\IRouter;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
class RoutingTest extends \Test\TestCase {
public function testSimpleRoute(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'GET']
]];
$this->assertSimpleRoute($routes, 'folders.open', 'GET', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open');
}
public function testSimpleRouteWithUnderScoreNames(): void {
$routes = ['routes' => [
['name' => 'admin_folders#open_current', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', 'root' => '']
]];
$this->assertSimpleRoute($routes, 'admin_folders.open_current', 'DELETE', '/folders/{folderId}/open', 'AdminFoldersController', 'openCurrent', [], [], '', true);
}
public function testSimpleOCSRoute(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'GET']
]
];
$this->assertSimpleOCSRoute($routes, 'folders.open', 'GET', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open');
}
public function testSimpleRouteWithMissingVerb(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open']
]];
$this->assertSimpleRoute($routes, 'folders.open', 'GET', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open');
}
public function testSimpleOCSRouteWithMissingVerb(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open']
]
];
$this->assertSimpleOCSRoute($routes, 'folders.open', 'GET', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open');
}
public function testSimpleRouteWithLowercaseVerb(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete']
]];
$this->assertSimpleRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open');
}
public function testSimpleOCSRouteWithLowercaseVerb(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete']
]
];
$this->assertSimpleOCSRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open');
}
public function testSimpleRouteWithRequirements(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', 'requirements' => ['something']]
]];
$this->assertSimpleRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open', ['something']);
}
public function testSimpleOCSRouteWithRequirements(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', 'requirements' => ['something']]
]
];
$this->assertSimpleOCSRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open', ['something']);
}
public function testSimpleRouteWithDefaults(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', [], 'defaults' => ['param' => 'foobar']]
]];
$this->assertSimpleRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open', [], ['param' => 'foobar']);
}
public function testSimpleOCSRouteWithDefaults(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', 'defaults' => ['param' => 'foobar']]
]
];
$this->assertSimpleOCSRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open', [], ['param' => 'foobar']);
}
public function testSimpleRouteWithPostfix(): void {
$routes = ['routes' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', 'postfix' => '_something']
]];
$this->assertSimpleRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open', [], [], '_something');
}
public function testSimpleOCSRouteWithPostfix(): void {
$routes = ['ocs' => [
['name' => 'folders#open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete', 'postfix' => '_something']
]
];
$this->assertSimpleOCSRoute($routes, 'folders.open', 'DELETE', '/apps/app1/folders/{folderId}/open', 'FoldersController', 'open', [], [], '_something');
}
public function testSimpleRouteWithBrokenName(): void {
$this->expectException(\UnexpectedValueException::class);
$routes = ['routes' => [
['name' => 'folders_open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete']
]];
/** @var IRouter|MockObject $router */
$router = $this->getMockBuilder(Router::class)
->onlyMethods(['create'])
->setConstructorArgs([
$this->createMock(LoggerInterface::class),
$this->createMock(IRequest::class),
$this->createMock(IConfig::class),
$this->createMock(IEventLogger::class),
$this->createMock(ContainerInterface::class),
$this->createMock(IAppManager::class),
])
->getMock();
// load route configuration
$container = new DIContainer('app1');
$config = new RouteConfig($container, $router, $routes);
$config->register();
}
public function testSimpleOCSRouteWithBrokenName(): void {
$this->expectException(\UnexpectedValueException::class);
$routes = ['ocs' => [
['name' => 'folders_open', 'url' => '/folders/{folderId}/open', 'verb' => 'delete']
]];
/** @var IRouter|MockObject $router */
$router = $this->getMockBuilder(Router::class)
->onlyMethods(['create'])
->setConstructorArgs([
$this->createMock(LoggerInterface::class),
$this->createMock(IRequest::class),
$this->createMock(IConfig::class),
$this->createMock(IEventLogger::class),
$this->createMock(ContainerInterface::class),
$this->createMock(IAppManager::class),
])
->getMock();
// load route configuration
$container = new DIContainer('app1');
$config = new RouteConfig($container, $router, $routes);
$config->register();
}
public function testSimpleOCSRouteWithUnderScoreNames(): void {
$routes = ['ocs' => [
['name' => 'admin_folders#open_current', 'url' => '/folders/{folderId}/open', 'verb' => 'delete']
]];
$this->assertSimpleOCSRoute($routes, 'admin_folders.open_current', 'DELETE', '/apps/app1/folders/{folderId}/open', 'AdminFoldersController', 'openCurrent');
}
public function testOCSResource(): void {
$routes = ['ocs-resources' => ['account' => ['url' => '/accounts']]];
$this->assertOCSResource($routes, 'account', '/apps/app1/accounts', 'AccountController', 'id');
}
public function testOCSResourceWithUnderScoreName(): void {
$routes = ['ocs-resources' => ['admin_accounts' => ['url' => '/admin/accounts']]];
$this->assertOCSResource($routes, 'admin_accounts', '/apps/app1/admin/accounts', 'AdminAccountsController', 'id');
}
public function testOCSResourceWithRoot(): void {
$routes = ['ocs-resources' => ['admin_accounts' => ['url' => '/admin/accounts', 'root' => '/core/endpoint']]];
$this->assertOCSResource($routes, 'admin_accounts', '/core/endpoint/admin/accounts', 'AdminAccountsController', 'id');
}
public function testResource(): void {
$routes = ['resources' => ['account' => ['url' => '/accounts']]];
$this->assertResource($routes, 'account', '/apps/app1/accounts', 'AccountController', 'id');
}
public function testResourceWithUnderScoreName(): void {
$routes = ['resources' => ['admin_accounts' => ['url' => '/admin/accounts']]];
$this->assertResource($routes, 'admin_accounts', '/apps/app1/admin/accounts', 'AdminAccountsController', 'id');
}
private function assertSimpleRoute($routes, $name, $verb, $url, $controllerName, $actionName, array $requirements = [], array $defaults = [], $postfix = '', $allowRootUrl = false): void {
if ($postfix) {
$name .= $postfix;
}
// route mocks
$container = new DIContainer('app1');
$route = $this->mockRoute($container, $verb, $controllerName, $actionName, $requirements, $defaults);
/** @var IRouter|MockObject $router */
$router = $this->getMockBuilder(Router::class)
->onlyMethods(['create'])
->setConstructorArgs([
$this->createMock(LoggerInterface::class),
$this->createMock(IRequest::class),
$this->createMock(IConfig::class),
$this->createMock(IEventLogger::class),
$this->createMock(ContainerInterface::class),
$this->createMock(IAppManager::class),
])
->getMock();
// we expect create to be called once:
$router
->expects($this->once())
->method('create')
->with($this->equalTo('app1.' . $name), $this->equalTo($url))
->willReturn($route);
// load route configuration
$config = new RouteConfig($container, $router, $routes);
if ($allowRootUrl) {
self::invokePrivate($config, 'rootUrlApps', [['app1']]);
}
$config->register();
}
/**
* @param $routes
* @param string $name
* @param string $verb
* @param string $url
* @param string $controllerName
* @param string $actionName
* @param array $requirements
* @param array $defaults
* @param string $postfix
*/
private function assertSimpleOCSRoute($routes,
$name,
$verb,
$url,
$controllerName,
$actionName,
array $requirements = [],
array $defaults = [],
$postfix = '') {
if ($postfix) {
$name .= $postfix;
}
// route mocks
$container = new DIContainer('app1');
$route = $this->mockRoute($container, $verb, $controllerName, $actionName, $requirements, $defaults);
/** @var IRouter|MockObject $router */
$router = $this->getMockBuilder(Router::class)
->onlyMethods(['create'])
->setConstructorArgs([
$this->createMock(LoggerInterface::class),
$this->createMock(IRequest::class),
$this->createMock(IConfig::class),
$this->createMock(IEventLogger::class),
$this->createMock(ContainerInterface::class),
$this->createMock(IAppManager::class),
])
->getMock();
// we expect create to be called once:
$router
->expects($this->once())
->method('create')
->with($this->equalTo('ocs.app1.' . $name), $this->equalTo($url))
->willReturn($route);
// load route configuration
$config = new RouteConfig($container, $router, $routes);
$config->register();
}
/**
* @param array $yaml
* @param string $resourceName
* @param string $url
* @param string $controllerName
* @param string $paramName
*/
private function assertOCSResource($yaml, $resourceName, $url, $controllerName, $paramName): void {
/** @var IRouter|MockObject $router */
$router = $this->getMockBuilder(Router::class)
->onlyMethods(['create'])
->setConstructorArgs([
$this->createMock(LoggerInterface::class),
$this->createMock(IRequest::class),
$this->createMock(IConfig::class),
$this->createMock(IEventLogger::class),
$this->createMock(ContainerInterface::class),
$this->createMock(IAppManager::class),
])
->getMock();
// route mocks
$container = new DIContainer('app1');
$indexRoute = $this->mockRoute($container, 'GET', $controllerName, 'index');
$showRoute = $this->mockRoute($container, 'GET', $controllerName, 'show');
$createRoute = $this->mockRoute($container, 'POST', $controllerName, 'create');
$updateRoute = $this->mockRoute($container, 'PUT', $controllerName, 'update');
$destroyRoute = $this->mockRoute($container, 'DELETE', $controllerName, 'destroy');
$urlWithParam = $url . '/{' . $paramName . '}';
$calls = [
['name' => 'ocs.app1.' . $resourceName . '.index', 'pattern' => $url, 'route' => $indexRoute],
['name' => 'ocs.app1.' . $resourceName . '.show', 'pattern' => $urlWithParam, 'route' => $showRoute],
['name' => 'ocs.app1.' . $resourceName . '.create', 'pattern' => $url, 'route' => $createRoute],
['name' => 'ocs.app1.' . $resourceName . '.update', 'pattern' => $urlWithParam, 'route' => $updateRoute],
['name' => 'ocs.app1.' . $resourceName . '.destroy', 'pattern' => $urlWithParam, 'route' => $destroyRoute],
];
// we expect create to be called five times:
$router
->expects($this->exactly(5))
->method('create')
->willReturnCallback(function (string $name, string $pattern) use (&$calls) {
$expected = array_shift($calls);
$this->assertEquals($expected['name'], $name);
$this->assertEquals($expected['pattern'], $pattern);
return $expected['route'];
});
// load route configuration
$config = new RouteConfig($container, $router, $yaml);
$config->register();
}
/**
* @param string $resourceName
* @param string $url
* @param string $controllerName
* @param string $paramName
*/
private function assertResource($yaml, $resourceName, $url, $controllerName, $paramName) {
/** @var IRouter|MockObject $router */
$router = $this->getMockBuilder(Router::class)
->onlyMethods(['create'])
->setConstructorArgs([
$this->createMock(LoggerInterface::class),
$this->createMock(IRequest::class),
$this->createMock(IConfig::class),
$this->createMock(IEventLogger::class),
$this->createMock(ContainerInterface::class),
$this->createMock(IAppManager::class),
])
->getMock();
// route mocks
$container = new DIContainer('app1');
$indexRoute = $this->mockRoute($container, 'GET', $controllerName, 'index');
$showRoute = $this->mockRoute($container, 'GET', $controllerName, 'show');
$createRoute = $this->mockRoute($container, 'POST', $controllerName, 'create');
$updateRoute = $this->mockRoute($container, 'PUT', $controllerName, 'update');
$destroyRoute = $this->mockRoute($container, 'DELETE', $controllerName, 'destroy');
$urlWithParam = $url . '/{' . $paramName . '}';
$calls = [
['name' => 'app1.' . $resourceName . '.index', 'pattern' => $url, 'route' => $indexRoute],
['name' => 'app1.' . $resourceName . '.show', 'pattern' => $urlWithParam, 'route' => $showRoute],
['name' => 'app1.' . $resourceName . '.create', 'pattern' => $url, 'route' => $createRoute],
['name' => 'app1.' . $resourceName . '.update', 'pattern' => $urlWithParam, 'route' => $updateRoute],
['name' => 'app1.' . $resourceName . '.destroy', 'pattern' => $urlWithParam, 'route' => $destroyRoute],
];
// we expect create to be called five times:
$router
->expects($this->exactly(5))
->method('create')
->willReturnCallback(function (string $name, string $pattern) use (&$calls) {
$expected = array_shift($calls);
$this->assertEquals($expected['name'], $name);
$this->assertEquals($expected['pattern'], $pattern);
return $expected['route'];
});
// load route configuration
$config = new RouteConfig($container, $router, $yaml);
$config->register();
}
/**
* @param DIContainer $container
* @param string $verb
* @param string $controllerName
* @param string $actionName
* @param array $requirements
* @param array $defaults
* @return MockObject
*/
private function mockRoute(
DIContainer $container,
$verb,
$controllerName,
$actionName,
array $requirements = [],
array $defaults = [],
) {
$route = $this->getMockBuilder(Route::class)
->onlyMethods(['method', 'requirements', 'defaults'])
->disableOriginalConstructor()
->getMock();
$route
->expects($this->once())
->method('method')
->with($this->equalTo($verb))
->willReturn($route);
if (count($requirements) > 0) {
$route
->expects($this->once())
->method('requirements')
->with($this->equalTo($requirements))
->willReturn($route);
}
$route->expects($this->once())
->method('defaults')
->with($this->callback(function (array $def) use ($defaults, $controllerName, $actionName) {
$defaults['caller'] = ['app1', $controllerName, $actionName];
$this->assertEquals($defaults, $def);
return true;
}))
->willReturn($route);
return $route;
}
}