Skip to content

Commit f4d8f58

Browse files
authored
Merge pull request #399 from WebFiori/feat/service-router
feat: ServiceRouter — auto-discovery, dynamic routing, and caching
2 parents fecea28 + 8f978c5 commit f4d8f58

17 files changed

Lines changed: 1085 additions & 0 deletions

WebFiori/Framework/App.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,9 @@ public static function getRunner() : Runner {
375375
'\\WebFiori\\Framework\\Cli\\Commands\\FreshMigrationsCommand',
376376
'\\WebFiori\\Framework\\Cli\\Commands\\SkipMigrationsCommand',
377377
'\\WebFiori\\Framework\\Cli\\Commands\\StepMigrationsCommand',
378+
'\\WebFiori\\Framework\\Cli\\Commands\\ServicesListCommand',
379+
'\\WebFiori\\Framework\\Cli\\Commands\\RoutesCacheCommand',
380+
'\\WebFiori\\Framework\\Cli\\Commands\\RoutesClearCommand',
378381
];
379382

380383
foreach ($commands as $c) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/**
4+
* This file is licensed under MIT License.
5+
*
6+
* Copyright (c) 2026-present WebFiori Framework
7+
*
8+
* For more information on the license, please visit:
9+
* https://github.com/WebFiori/.github/blob/main/LICENSE
10+
*
11+
*/
12+
namespace WebFiori\Framework\Cli\Commands;
13+
14+
use WebFiori\Cache\Cache;
15+
use WebFiori\Cache\FileStorage;
16+
use WebFiori\Cli\Command;
17+
use WebFiori\Framework\Router\RouteCache;
18+
use WebFiori\Framework\Router\Router;
19+
20+
/**
21+
* CLI command to build the route cache.
22+
*
23+
* @author Ibrahim
24+
*/
25+
class RoutesCacheCommand extends Command {
26+
public function __construct() {
27+
parent::__construct('routes:cache', [], 'Build the route cache for production.');
28+
}
29+
30+
public function exec(): int {
31+
$cache = $this->createRouteCache();
32+
$cache->setEnabled(true);
33+
$count = $cache->build();
34+
$this->success("Route cache built: $count route(s) cached.");
35+
36+
return 0;
37+
}
38+
39+
private function createRouteCache(): RouteCache {
40+
$storagePath = APP_PATH . 'Storage';
41+
42+
if (!is_dir($storagePath)) {
43+
mkdir($storagePath, 0755, true);
44+
}
45+
46+
return new RouteCache(new Cache(new FileStorage($storagePath)), true);
47+
}
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/**
4+
* This file is licensed under MIT License.
5+
*
6+
* Copyright (c) 2026-present WebFiori Framework
7+
*
8+
* For more information on the license, please visit:
9+
* https://github.com/WebFiori/.github/blob/main/LICENSE
10+
*
11+
*/
12+
namespace WebFiori\Framework\Cli\Commands;
13+
14+
use WebFiori\Cache\Cache;
15+
use WebFiori\Cache\FileStorage;
16+
use WebFiori\Cli\Command;
17+
use WebFiori\Framework\Router\RouteCache;
18+
19+
/**
20+
* CLI command to clear the route cache.
21+
*
22+
* @author Ibrahim
23+
*/
24+
class RoutesClearCommand extends Command {
25+
public function __construct() {
26+
parent::__construct('routes:clear', [], 'Clear the route cache.');
27+
}
28+
29+
public function exec(): int {
30+
$cache = $this->createRouteCache();
31+
$cache->clear();
32+
$this->success('Route cache cleared.');
33+
34+
return 0;
35+
}
36+
37+
private function createRouteCache(): RouteCache {
38+
$storagePath = APP_PATH . 'Storage';
39+
40+
return new RouteCache(new Cache(new FileStorage($storagePath)), true);
41+
}
42+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/**
4+
* This file is licensed under MIT License.
5+
*
6+
* Copyright (c) 2026-present WebFiori Framework
7+
*
8+
* For more information on the license, please visit:
9+
* https://github.com/WebFiori/.github/blob/main/LICENSE
10+
*
11+
*/
12+
namespace WebFiori\Framework\Cli\Commands;
13+
14+
use WebFiori\Cli\Command;
15+
use WebFiori\Framework\Router\ServiceRouter;
16+
17+
/**
18+
* CLI command to list all discovered services.
19+
*
20+
* @author Ibrahim
21+
*/
22+
class ServicesListCommand extends Command {
23+
public function __construct() {
24+
parent::__construct('services:list', [], 'List all auto-discovered API services.');
25+
}
26+
27+
public function exec(): int {
28+
$discovered = ServiceRouter::getDiscovered();
29+
30+
if (empty($discovered)) {
31+
$this->info('No services discovered. Use ServiceRouter::discover() to register services.');
32+
33+
return 0;
34+
}
35+
36+
$this->println('');
37+
$this->println(sprintf(' %-15s %-45s %-10s %s', 'Name', 'Class', 'Type', 'Path'));
38+
$this->println(str_repeat('-', 90));
39+
40+
foreach ($discovered as $name => $entry) {
41+
$this->println(sprintf(
42+
' %-15s %-45s %-10s %s',
43+
$name,
44+
$entry['class'],
45+
$entry['type'],
46+
$entry['path']
47+
));
48+
}
49+
50+
$this->println('');
51+
$this->info('Total: ' . count($discovered) . ' service(s).');
52+
53+
return 0;
54+
}
55+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
/**
4+
* This file is licensed under MIT License.
5+
*
6+
* Copyright (c) 2026-present WebFiori Framework
7+
*
8+
* For more information on the license, please visit:
9+
* https://github.com/WebFiori/.github/blob/main/LICENSE
10+
*
11+
*/
12+
namespace WebFiori\Framework\Router;
13+
14+
use WebFiori\Cache\Cache;
15+
16+
/**
17+
* Caches route definitions to avoid discovery/registration overhead in production.
18+
*
19+
* @author Ibrahim
20+
*/
21+
class RouteCache {
22+
/**
23+
* @var Cache The cache instance.
24+
*/
25+
private Cache $cache;
26+
/**
27+
* @var string Cache key for route data.
28+
*/
29+
private string $cacheKey;
30+
/**
31+
* @var bool Whether caching is enabled.
32+
*/
33+
private bool $enabled;
34+
35+
/**
36+
* Creates new instance.
37+
*
38+
* @param Cache $cache The cache instance to use.
39+
* @param bool $enabled Whether route caching is enabled.
40+
* @param string $cacheKey The cache key for route data.
41+
*/
42+
public function __construct(Cache $cache, bool $enabled = false, string $cacheKey = 'wf_routes_cache') {
43+
$this->cache = $cache;
44+
$this->enabled = $enabled;
45+
$this->cacheKey = $cacheKey;
46+
}
47+
48+
/**
49+
* Returns whether route caching is enabled.
50+
*
51+
* @return bool
52+
*/
53+
public function isEnabled(): bool {
54+
return $this->enabled;
55+
}
56+
57+
/**
58+
* Sets whether route caching is enabled.
59+
*
60+
* @param bool $enabled
61+
*/
62+
public function setEnabled(bool $enabled): void {
63+
$this->enabled = $enabled;
64+
}
65+
66+
/**
67+
* Checks if a route cache exists.
68+
*
69+
* @return bool
70+
*/
71+
public function isCached(): bool {
72+
return $this->cache->get($this->cacheKey) !== null;
73+
}
74+
75+
/**
76+
* Load cached routes into the Router.
77+
*
78+
* @return bool True if cache was loaded, false if no cache exists.
79+
*/
80+
public function load(): bool {
81+
if (!$this->enabled) {
82+
return false;
83+
}
84+
85+
$data = $this->cache->get($this->cacheKey);
86+
87+
if ($data === null) {
88+
return false;
89+
}
90+
91+
$discovered = $data['discovered'] ?? [];
92+
93+
if (!empty($discovered)) {
94+
ServiceRouter::setDiscovered($discovered);
95+
}
96+
97+
// Re-register routes from the cached service map
98+
$configs = $data['configs'] ?? [];
99+
100+
foreach ($configs as $config) {
101+
ServiceRouter::discover(
102+
$config['namespace'],
103+
$config['basePath'],
104+
$config['options'] ?? [],
105+
$config['directory'] ?? null,
106+
$config['recursive'] ?? false
107+
);
108+
}
109+
110+
return true;
111+
}
112+
113+
/**
114+
* Build route cache from current ServiceRouter state.
115+
*
116+
* @param array $discoverConfigs Array of discover() call configs to replay on load.
117+
*
118+
* @return int Number of discovered services cached.
119+
*/
120+
public function build(array $discoverConfigs = []): int {
121+
$discovered = ServiceRouter::getDiscovered();
122+
123+
$data = [
124+
'discovered' => $discovered,
125+
'configs' => $discoverConfigs,
126+
'built_at' => date('c'),
127+
'total' => count($discovered),
128+
];
129+
130+
$this->cache->set($this->cacheKey, $data, 31536000, true);
131+
132+
return count($discovered);
133+
}
134+
135+
/**
136+
* Clear the route cache.
137+
*/
138+
public function clear(): void {
139+
$this->cache->delete($this->cacheKey);
140+
}
141+
142+
/**
143+
* Returns the cache key.
144+
*
145+
* @return string
146+
*/
147+
public function getCacheKey(): string {
148+
return $this->cacheKey;
149+
}
150+
}

WebFiori/Framework/Router/RouteOption.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,8 @@ class RouteOption {
7272
* An option which is used to set an array of allowed values to route parameters.
7373
*/
7474
const VALUES = 'vars-values';
75+
/**
76+
* An option to specify a namespace for dynamic service resolution.
77+
*/
78+
const NS = 'namespace';
7579
}

0 commit comments

Comments
 (0)