/
Router.php
321 lines (277 loc) · 9.64 KB
/
Router.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
<?php namespace Cms\Classes;
use Lang;
use File;
use Cache;
use Config;
use Event;
use SystemException;
use October\Rain\Router\Router as RainRouter;
use October\Rain\Router\Helper as RouterHelper;
/**
* The router parses page URL patterns and finds pages by URLs.
*
* The page URL format is explained below.
* <pre>/blog/post/:post_id</pre>
* Name of parameters should be compatible with PHP variable names. To make a parameter optional
* add the question mark after its name:
* <pre>/blog/post/:post_id?</pre>
* By default parameters in the middle of the URL are required, for example:
* <pre>/blog/:post_id?/comments - although the :post_id parameter is marked as optional,
* it will be processed as required.</pre>
* Optional parameters can have default values which are used as fallback values in case if the real
* parameter value is not presented in the URL. Default values cannot contain the pipe symbols and question marks.
* Specify the default value after the question mark:
* <pre>/blog/category/:category_id?10 - The category_id parameter would be 10 for this URL: /blog/category</pre>
* You can also add regular expression validation to parameters. To add a validation expression
* add the pipe symbol after the parameter name (or the question mark) and specify the expression.
* The forward slash symbol is not allowed in the expressions. Examples:
* <pre>/blog/:post_id|^[0-9]+$/comments - this will match /blog/post/10/comments
* /blog/:post_id|^[0-9]+$ - this will match /blog/post/3
* /blog/:post_name?|^[a-z0-9\-]+$ - this will match /blog/my-blog-post</pre>
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class Router
{
/**
* @var \Cms\Classes\Theme A reference to the CMS theme containing the object.
*/
protected $theme;
/**
* @var array A list of parameters names and values extracted from the URL pattern and URL string.
*/
protected $parameters = [];
/**
* @var array Contains the URL map - the list of page file names and corresponding URL patterns.
*/
private static $urlMap = [];
/**
* October\Rain\Router\Router Router object with routes preloaded.
*/
private static $routerObj;
/**
* Creates the router instance.
* @param \Cms\Classes\Theme $theme Specifies the theme being processed.
*/
public function __construct(Theme $theme)
{
$this->theme = $theme;
}
/**
* Finds a page by its URL. Returns the page object and sets the $parameters property.
* @param string $url The requested URL string.
* @return \Cms\Classes\Page Returns \Cms\Classes\Page object or null if the page cannot be found.
*/
public function findByUrl($url)
{
$url = RouterHelper::normalizeUrl($url);
$apiResult = Event::fire('cms.router.beforeRoute', [$url], true);
if ($apiResult !== null) {
return $apiResult;
}
for ($pass = 1; $pass <= 2; $pass++) {
$fileName = null;
$urlList = [];
$cacheable = Config::get('cms.enableRoutesCache') && in_array(
Config::get('cache.driver'),
['apc', 'memcached', 'redis', 'array']
);
if ($cacheable) {
$fileName = $this->getCachedUrlFileName($url, $urlList);
}
/*
* Find the page by URL and cache the route
*/
if (!$fileName) {
$router = $this->getRouterObject();
if ($router->match($url)) {
$this->parameters = $router->getParameters();
$fileName = $router->matchedRoute();
if ($cacheable) {
if (!$urlList || !is_array($urlList)) {
$urlList = [];
}
$urlList[$url] = $fileName;
$key = $this->getUrlListCacheKey();
Cache::put($key, serialize($urlList), Config::get('cms.urlCacheTtl', 1));
}
}
}
/*
* Return the page
*/
if ($fileName) {
if (($page = Page::loadCached($this->theme, $fileName)) === null) {
/*
* If the page was not found on the disk, clear the URL cache
* and repeat the routing process.
*/
if ($pass == 1) {
$this->clearCache();
continue;
}
return null;
}
return $page;
}
return null;
}
}
/**
* Finds a URL by it's page. Returns the URL route for linking to the page and uses the supplied
* parameters in it's address.
* @param string $fileName Page file name.
* @param array $parameters Route parameters to consider in the URL.
* @return string A built URL matching the page route.
*/
public function findByFile($fileName, $parameters = [])
{
if (!strlen(File::extension($fileName))) {
$fileName .= '.htm';
}
$router = $this->getRouterObject();
return $router->url($fileName, $parameters);
}
/**
* Autoloads the URL map only allowing a single execution.
* @return array Returns the URL map.
*/
protected function getRouterObject()
{
if (self::$routerObj !== null) {
return self::$routerObj;
}
/*
* Load up each route rule
*/
$router = new RainRouter();
foreach ($this->getUrlMap() as $pageInfo) {
$router->route($pageInfo['file'], $pageInfo['pattern']);
}
/*
* Sort all the rules
*/
$router->sortRules();
return self::$routerObj = $router;
}
/**
* Autoloads the URL map only allowing a single execution.
* @return array Returns the URL map.
*/
protected function getUrlMap()
{
if (!count(self::$urlMap)) {
$this->loadUrlMap();
}
return self::$urlMap;
}
/**
* Loads the URL map - a list of page file names and corresponding URL patterns.
* The URL map can is cached. The clearUrlMap() method resets the cache. By default
* the map is updated every time when a page is saved in the back-end, or
* when the interval defined with the cms.urlCacheTtl expires.
* @return boolean Returns true if the URL map was loaded from the cache. Otherwise returns false.
*/
protected function loadUrlMap()
{
$key = $this->getCacheKey('page-url-map');
$cacheable = Config::get('cms.enableRoutesCache');
if ($cacheable) {
$cached = Cache::get($key, false);
}
else {
$cached = false;
}
if (!$cached || ($unserialized = @unserialize($cached)) === false) {
/*
* The item doesn't exist in the cache, create the map
*/
$pages = $this->theme->listPages();
$map = [];
foreach ($pages as $page) {
if (!$page->url) {
continue;
}
$map[] = ['file' => $page->getFileName(), 'pattern' => $page->url];
}
self::$urlMap = $map;
if ($cacheable) {
Cache::put($key, serialize($map), Config::get('cms.urlCacheTtl', 1));
}
return false;
}
self::$urlMap = $unserialized;
return true;
}
/**
* Clears the router cache.
*/
public function clearCache()
{
Cache::forget($this->getCacheKey('page-url-map'));
Cache::forget($this->getCacheKey('cms-url-list'));
}
/**
* Sets the current routing parameters.
* @param array $parameters
* @return array
*/
public function setParameters(array $parameters)
{
$this->parameters = $parameters;
}
/**
* Returns the current routing parameters.
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Returns a routing parameter.
* @return array
*/
public function getParameter($name, $default = null)
{
if (isset($this->parameters[$name]) && !empty($this->parameters[$name])) {
return $this->parameters[$name];
}
return $default;
}
/**
* Returns the caching URL key depending on the theme.
* @param string $keyName Specifies the base key name.
* @return string Returns the theme-specific key name.
*/
protected function getCacheKey($keyName)
{
return crc32($this->theme->getPath()).$keyName;
}
/**
* Returns the cache key name for the URL list.
* @return string
*/
protected function getUrlListCacheKey()
{
return $this->getCacheKey('cms-url-list');
}
/**
* Tries to load a page file name corresponding to a specified URL from the cache.
* @param string $url Specifies the requested URL.
* @param array &$urlList The URL list loaded from the cache
* @return mixed Returns the page file name if the URL exists in the cache. Otherwise returns null.
*/
protected function getCachedUrlFileName($url, &$urlList)
{
$key = $this->getUrlListCacheKey();
$urlList = Cache::get($key, false);
if ($urlList && ($urlList = @unserialize($urlList)) && is_array($urlList)) {
if (array_key_exists($url, $urlList)) {
return $urlList[$url];
}
}
return null;
}
}