X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FApp%2FRouter.php;h=181f5368d51de5668a9f5d77bb419166330c7ade;hb=85d162d1d2a738c6d21e834fe1b5cb579c4f581e;hp=c9ba21bb3538e2df167dc74176652f2e4ab659c4;hpb=0258fd17c701602480572488758ad9337ceddf8c;p=friendica.git diff --git a/src/App/Router.php b/src/App/Router.php index c9ba21bb35..181f5368d5 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -1,4 +1,23 @@ . + * + */ namespace Friendica\App; @@ -7,8 +26,11 @@ use FastRoute\DataGenerator\GroupCountBased; use FastRoute\Dispatcher; use FastRoute\RouteCollector; use FastRoute\RouteParser\Std; +use Friendica\Core\Cache\Enum\Duration; +use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Hook; use Friendica\Core\L10n; +use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Network\HTTPException; /** @@ -23,12 +45,20 @@ use Friendica\Network\HTTPException; */ class Router { - const POST = 'POST'; - const GET = 'GET'; + const DELETE = 'DELETE'; + const GET = 'GET'; + const PATCH = 'PATCH'; + const POST = 'POST'; + const PUT = 'PUT'; + const OPTIONS = 'OPTIONS'; const ALLOWED_METHODS = [ - self::POST, + self::DELETE, self::GET, + self::PATCH, + self::POST, + self::PUT, + self::OPTIONS ]; /** @var RouteCollector */ @@ -44,21 +74,48 @@ class Router */ private $parameters = []; + /** @var L10n */ + private $l10n; + + /** @var ICanCache */ + private $cache; + + /** @var ICanLock */ + private $lock; + + /** @var string */ + private $baseRoutesFilepath; + /** - * @param array $server The $_SERVER variable - * @param RouteCollector|null $routeCollector Optional the loaded Route collector + * @param array $server The $_SERVER variable + * @param string $baseRoutesFilepath The path to a base routes file to leverage cache, can be empty + * @param L10n $l10n + * @param ICanCache $cache + * @param RouteCollector|null $routeCollector */ - public function __construct(array $server, RouteCollector $routeCollector = null) + public function __construct(array $server, string $baseRoutesFilepath, L10n $l10n, ICanCache $cache, ICanLock $lock, RouteCollector $routeCollector = null) { + $this->baseRoutesFilepath = $baseRoutesFilepath; + $this->l10n = $l10n; + $this->cache = $cache; + $this->lock = $lock; + $httpMethod = $server['REQUEST_METHOD'] ?? self::GET; $this->httpMethod = in_array($httpMethod, self::ALLOWED_METHODS) ? $httpMethod : self::GET; $this->routeCollector = isset($routeCollector) ? $routeCollector : new RouteCollector(new Std(), new GroupCountBased()); + + if ($this->baseRoutesFilepath && !file_exists($this->baseRoutesFilepath)) { + throw new HTTPException\InternalServerErrorException('Routes file path does\'n exist.'); + } } /** + * This will be called either automatically if a base routes file path was submitted, + * or can be called manually with a custom route array. + * * @param array $routes The routes to add to the Router * * @return self The router instance with the loaded routes @@ -75,6 +132,9 @@ class Router $this->routeCollector = $routeCollector; + // Add routes from addons + Hook::callAll('route_collection', $this->routeCollector); + return $this; } @@ -166,12 +226,9 @@ class Router */ public function getModuleClass($cmd) { - // Add routes from addons - Hook::callAll('route_collection', $this->routeCollector); - $cmd = '/' . ltrim($cmd, '/'); - $dispatcher = new Dispatcher\GroupCountBased($this->routeCollector->getData()); + $dispatcher = new Dispatcher\GroupCountBased($this->getCachedDispatchData()); $moduleClass = null; $this->parameters = []; @@ -181,9 +238,9 @@ class Router $moduleClass = $routeInfo[1]; $this->parameters = $routeInfo[2]; } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { - throw new HTTPException\MethodNotAllowedException(L10n::t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); + throw new HTTPException\MethodNotAllowedException($this->l10n->t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); } else { - throw new HTTPException\NotFoundException(L10n::t('Page not found.')); + throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); } return $moduleClass; @@ -198,4 +255,73 @@ class Router { return $this->parameters; } + + /** + * If a base routes file path has been provided, we can load routes from it if the cache misses. + * + * @return array + * @throws HTTPException\InternalServerErrorException + */ + private function getDispatchData() + { + $dispatchData = []; + + if ($this->baseRoutesFilepath) { + $dispatchData = require $this->baseRoutesFilepath; + if (!is_array($dispatchData)) { + throw new HTTPException\InternalServerErrorException('Invalid base routes file'); + } + } + + $this->loadRoutes($dispatchData); + + return $this->routeCollector->getData(); + } + + /** + * We cache the dispatch data for speed, as computing the current routes (version 2020.09) + * takes about 850ms for each requests. + * + * The cached "routerDispatchData" lasts for a day, and must be cleared manually when there + * is any changes in the enabled addons list. + * + * Additionally, we check for the base routes file last modification time to automatically + * trigger re-computing the dispatch data. + * + * @return array|mixed + * @throws HTTPException\InternalServerErrorException + */ + private function getCachedDispatchData() + { + $routerDispatchData = $this->cache->get('routerDispatchData'); + $lastRoutesFileModifiedTime = $this->cache->get('lastRoutesFileModifiedTime'); + $forceRecompute = false; + + if ($this->baseRoutesFilepath) { + $routesFileModifiedTime = filemtime($this->baseRoutesFilepath); + $forceRecompute = $lastRoutesFileModifiedTime != $routesFileModifiedTime; + } + + if (!$forceRecompute && $routerDispatchData) { + return $routerDispatchData; + } + + if (!$this->lock->acquire('getCachedDispatchData', 0)) { + // Immediately return uncached data when we can't aquire a lock + return $this->getDispatchData(); + } + + $routerDispatchData = $this->getDispatchData(); + + $this->cache->set('routerDispatchData', $routerDispatchData, Duration::DAY); + if (!empty($routesFileModifiedTime)) { + $this->cache->set('lastRoutesFileModifiedTime', $routesFileModifiedTime, Duration::MONTH); + } + + if ($this->lock->isLocked('getCachedDispatchData')) { + $this->lock->release('getCachedDispatchData'); + } + + return $routerDispatchData; + } }