A list of route definitions */ private array $extraRoutes; /** * @var array|null A list of route definitions loaded from * the files specified by $routeFiles */ private ?array $routesFromFiles = null; /** @var int[]|null */ private ?array $routeFileTimestamps = null; /** @var string|null */ private ?string $configHash = null; /** * @param string[] $routeFiles List of names of JSON files containing routes * See the documentation of this class for a description of the file * format. * @param array $extraRoutes Extension route array. The content of * this array must be a list of route definitions. See the documentation * of this class for a description of the expected structure. */ public function __construct( array $routeFiles, array $extraRoutes, Router $router, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator, ErrorReporter $errorReporter ) { parent::__construct( $router, '', $responseFactory, $basicAuth, $objectFactory, $restValidator, $errorReporter ); $this->routeFiles = $routeFiles; $this->extraRoutes = $extraRoutes; } /** * Get a config version hash for cache invalidation * * @return string */ protected function getConfigHash(): string { if ( $this->configHash === null ) { $this->configHash = md5( json_encode( [ 'class' => __CLASS__, 'version' => 1, 'extraRoutes' => $this->extraRoutes, 'fileTimestamps' => $this->getRouteFileTimestamps() ] ) ); } return $this->configHash; } /** * Load the defined JSON files and return the merged routes. * * @return array A list of route definitions. See this class's * documentation for a description of the format of route definitions. * @throws ModuleConfigurationException If a route file cannot be loaded or processed. */ private function getRoutesFromFiles(): array { if ( $this->routesFromFiles !== null ) { return $this->routesFromFiles; } $this->routesFromFiles = []; $this->routeFileTimestamps = []; foreach ( $this->routeFiles as $fileName ) { $this->routeFileTimestamps[$fileName] = filemtime( $fileName ); $routes = $this->loadJsonFile( $fileName ); $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes ); } return $this->routesFromFiles; } /** * Get an array of last modification times of the defined route files. * * @return int[] Last modification times */ private function getRouteFileTimestamps(): array { if ( $this->routeFileTimestamps === null ) { $this->routeFileTimestamps = []; foreach ( $this->routeFiles as $fileName ) { $this->routeFileTimestamps[$fileName] = filemtime( $fileName ); } } return $this->routeFileTimestamps; } /** * @return array[] */ public function getDefinedPaths(): array { $paths = []; foreach ( $this->getAllRoutes() as $spec ) { $key = $spec['path']; $methods = isset( $spec['method'] ) ? (array)$spec['method'] : [ 'GET' ]; $paths[$key] = array_merge( $paths[$key] ?? [], $methods ); } return $paths; } /** * @return Iterator */ private function getAllRoutes() { $iterator = new AppendIterator; $iterator->append( new ArrayIterator( $this->getRoutesFromFiles() ) ); $iterator->append( new ArrayIterator( $this->extraRoutes ) ); return $iterator; } protected function initRoutes(): void { $routeDefs = $this->getAllRoutes(); foreach ( $routeDefs as $route ) { if ( !isset( $route['path'] ) ) { throw new RouteDefinitionException( 'Missing path' ); } $path = $route['path']; $method = $route['method'] ?? 'GET'; $info = $this->makeRouteInfo( $route ); $this->addRoute( $method, $path, $info ); } } /** * Generate a route info array to be stored in the matcher tree, * in the form expected by MatcherBasedModule::addRoute() * and ultimately Module::getHandlerForPath(). */ private function makeRouteInfo( array $route ): array { static $objectSpecKeys = [ 'class', 'factory', 'services', 'optional_services', 'args', ]; if ( isset( $route['redirect'] ) ) { // Redirect shorthand $info = [ 'spec' => [ 'class' => RedirectHandler::class ], 'config' => $route, ]; } else { // Object spec at the top level $info = [ 'spec' => array_intersect_key( $route, array_flip( $objectSpecKeys ) ), 'config' => array_diff_key( $route, array_flip( $objectSpecKeys ) ), ]; } $info['path'] = $route['path']; return $info; } public function getOpenApiInfo() { // Note that mwapi-1.0 is based on OAS 3.0, so it doesn't support the // "summary" property introduced in 3.1. return [ 'title' => 'Extra Routes', 'description' => 'REST endpoints not associated with a module', 'version' => 'undefined', ]; } }