nextcloud/lib/private/AppFramework/Routing/RouteConfig.php
Roeland Jago Douma edc1c77dd9
Do not create a RouteActionHandler object for each route
This is not required and doesn't allow us to be properly lazy. On top of
it this doesnt allow us to cache the routes (since closures/objects
can't be cached).

This is the first small step into cleaning up the routing we have

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2020-07-07 12:33:22 +02:00

292 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\AppFramework\Routing;
use OC\AppFramework\DependencyInjection\DIContainer;
use OCP\Route\IRouter;
/**
* Class RouteConfig
* @package OC\AppFramework\routing
*/
class RouteConfig {
/** @var DIContainer */
private $container;
/** @var IRouter */
private $router;
/** @var array */
private $routes;
/** @var string */
private $appName;
/** @var string[] */
private $controllerNameCache = [];
protected $rootUrlApps = [
'cloud_federation_api',
'core',
'files_sharing',
'files',
'settings',
'spreed',
];
/**
* @param \OC\AppFramework\DependencyInjection\DIContainer $container
* @param \OCP\Route\IRouter $router
* @param array $routes
* @internal param $appName
*/
public function __construct(DIContainer $container, IRouter $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');
}
list($controller, $action) = $split;
$controllerName = $this->buildControllerName($controller);
$actionName = $this->buildActionName($action);
$routeName = $routeNamePrefix . $this->appName . '.' . $controller . '.' . $action . $postfix;
$router = $this->router->create($routeName, $url)
->method($verb)
->setDefault('caller', [$this->appName, $controllerName, $actionName]);
// 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
if (array_key_exists('defaults', $route)) {
$router->defaults($route['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) . '.' . strtolower($method);
$this->router->create($routeName, $url)
->method($verb)
->setDefault('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);
}
}