66 private array $routeFiles;
71 private array $extraRoutes;
77 private ?array $routesFromFiles =
null;
80 private ?array $routeFileTimestamps =
null;
83 private ?
string $configHash =
null;
86 private ?array $matchers =
null;
103 ObjectFactory $objectFactory,
116 $this->routeFiles = $routeFiles;
117 $this->extraRoutes = $extraRoutes;
123 foreach ( $this->getMatchers() as $method => $matcher ) {
124 $cacheData[$method] = $matcher->getCacheData();
132 if ( $cacheData[self::CACHE_CONFIG_HASH_KEY] !== $this->getConfigHash() ) {
136 unset( $cacheData[self::CACHE_CONFIG_HASH_KEY] );
137 $this->matchers = [];
139 foreach ( $cacheData as $method => $data ) {
151 private function getConfigHash(): string {
152 if ( $this->configHash === null ) {
153 $this->configHash = md5( json_encode( [
155 'extraRoutes' => $this->extraRoutes,
156 'fileTimestamps' => $this->getRouteFileTimestamps()
159 return $this->configHash;
169 private function getRoutesFromFiles(): array {
170 if ( $this->routesFromFiles !== null ) {
171 return $this->routesFromFiles;
174 $this->routesFromFiles = [];
175 $this->routeFileTimestamps = [];
176 foreach ( $this->routeFiles as $fileName ) {
177 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
179 $routes = $this->loadRoutes( $fileName );
181 $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
184 return $this->routesFromFiles;
196 private function loadRoutes(
string $fileName ) {
197 $spec = $this->loadJsonFile( $fileName );
199 if ( isset( $spec[
'routes'] ) ) {
200 if ( !isset( $spec[
'module'] ) ) {
201 throw new ModuleConfigurationException(
202 "Missing module name in $fileName"
206 if ( $spec[
'module'] !== $this->pathPrefix ) {
209 throw new LogicException(
210 "Module name mismatch in $fileName: " .
211 "expected {$this->pathPrefix} but got {$spec['module']}."
216 $routes = $spec[
'routes'];
230 private function getRouteFileTimestamps(): array {
231 if ( $this->routeFileTimestamps === null ) {
232 $this->routeFileTimestamps = [];
233 foreach ( $this->routeFiles as $fileName ) {
234 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
237 return $this->routeFileTimestamps;
247 foreach ( $this->getAllRoutes() as $spec ) {
248 $key = $spec[
'path'];
250 $methods = isset( $spec[
'method'] ) ? (array)$spec[
'method'] : [
'GET' ];
252 $paths[$key] = array_merge( $paths[$key] ?? [], $methods );
261 private function getAllRoutes() {
262 $iterator =
new AppendIterator;
263 $iterator->append(
new ArrayIterator( $this->getRoutesFromFiles() ) );
264 $iterator->append(
new ArrayIterator( $this->extraRoutes ) );
273 private function getMatchers() {
274 if ( $this->matchers ===
null ) {
275 $routeDefs = $this->getAllRoutes();
279 foreach ( $routeDefs as $spec ) {
280 $methods = $spec[
'method'] ?? [
'GET' ];
281 if ( !is_array( $methods ) ) {
282 $methods = [ $methods ];
284 foreach ( $methods as $method ) {
285 if ( !isset( $matchers[$method] ) ) {
286 $matchers[$method] =
new PathMatcher;
288 $matchers[$method]->add( $spec[
'path'], $spec );
291 $this->matchers = $matchers;
294 return $this->matchers;
302 string $requestMethod
304 $matchers = $this->getMatchers();
305 $matcher = $matchers[$requestMethod] ??
null;
306 $match = $matcher ? $matcher->match(
$path ) :
null;
312 'methods' => $this->getAllowedMethods(
$path ),
315 $spec = $match[
'userData'];
317 if ( !isset( $spec[
'class'] ) && !isset( $spec[
'factory'] ) ) {
319 if ( isset( $spec[
'redirect'] ) ) {
320 $spec[
'class'] = RedirectHandler::class;
323 'Route handler definition must specify "class" or ' .
324 '"factory" or "redirect"'
332 'params' => $match[
'params'] ?? [],
334 'path' => $spec[
'path'],
348 foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
349 if ( $allowedMatcher->match( $relPath ) ) {
350 $allowed[] = $allowedMethod;
355 in_array(
'GET', $allowed ) ? array_merge( [
'HEAD' ], $allowed ) : $allowed