Completed
Pull Request — 2.1 (#12704)
by Robert
08:59
created

UrlManager::buildRules()   C

Complexity

Conditions 7
Paths 17

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 7.004

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 6.7272
c 0
b 0
f 0
ccs 22
cts 23
cp 0.9565
cc 7
eloc 18
nc 17
nop 1
crap 7.004
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\InvalidConfigException;
13
use yii\caching\Cache;
14
use yii\di\Instance;
15
16
/**
17
 * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules.
18
 *
19
 * UrlManager is configured as an application component in [[\yii\base\Application]] by default.
20
 * You can access that instance via `Yii::$app->urlManager`.
21
 *
22
 * You can modify its configuration by adding an array to your application config under `components`
23
 * as it is shown in the following example:
24
 *
25
 * ```php
26
 * 'urlManager' => [
27
 *     'enablePrettyUrl' => true,
28
 *     'rules' => [
29
 *         // your rules go here
30
 *     ],
31
 *     // ...
32
 * ]
33
 * ```
34
 *
35
 * @property string $baseUrl The base URL that is used by [[createUrl()]] to prepend to created URLs.
36
 * @property string $hostInfo The host info (e.g. "http://www.example.com") that is used by
37
 * [[createAbsoluteUrl()]] to prepend to created URLs.
38
 * @property string $scriptUrl The entry script URL that is used by [[createUrl()]] to prepend to created
39
 * URLs.
40
 *
41
 * @author Qiang Xue <[email protected]>
42
 * @since 2.0
43
 */
44
class UrlManager extends Component
45
{
46
    /**
47
     * @var boolean whether to enable pretty URLs. Instead of putting all parameters in the query
48
     * string part of a URL, pretty URLs allow using path info to represent some of the parameters
49
     * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of
50
     * "/index.php?r=news%2Fview&id=100".
51
     */
52
    public $enablePrettyUrl = false;
53
    /**
54
     * @var boolean whether to enable strict parsing. If strict parsing is enabled, the incoming
55
     * requested URL must match at least one of the [[rules]] in order to be treated as a valid request.
56
     * Otherwise, the path info part of the request will be treated as the requested route.
57
     * This property is used only when [[enablePrettyUrl]] is `true`.
58
     */
59
    public $enableStrictParsing = false;
60
    /**
61
     * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is `true`.
62
     * This property is used only if [[enablePrettyUrl]] is `true`. Each element in the array
63
     * is the configuration array for creating a single URL rule. The configuration will
64
     * be merged with [[ruleConfig]] first before it is used for creating the rule object.
65
     *
66
     * A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]]
67
     * and [[UrlRule::route|route]]: `'pattern' => 'route'`. That is, instead of using a configuration
68
     * array, one can use the key to represent the pattern and the value the corresponding route.
69
     * For example, `'post/<id:\d+>' => 'post/view'`.
70
     *
71
     * For RESTful routing the mentioned shortcut format also allows you to specify the
72
     * [[UrlRule::verb|HTTP verb]] that the rule should apply for.
73
     * You can do that  by prepending it to the pattern, separated by space.
74
     * For example, `'PUT post/<id:\d+>' => 'post/update'`.
75
     * You may specify multiple verbs by separating them with comma
76
     * like this: `'POST,PUT post/index' => 'post/create'`.
77
     * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE.
78
     * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way
79
     * so you normally would not specify a verb for normal GET request.
80
     *
81
     * Here is an example configuration for RESTful CRUD controller:
82
     *
83
     * ```php
84
     * [
85
     *     'dashboard' => 'site/index',
86
     *
87
     *     'POST <controller:[\w-]+>s' => '<controller>/create',
88
     *     '<controller:[\w-]+>s' => '<controller>/index',
89
     *
90
     *     'PUT <controller:[\w-]+>/<id:\d+>'    => '<controller>/update',
91
     *     'DELETE <controller:[\w-]+>/<id:\d+>' => '<controller>/delete',
92
     *     '<controller:[\w-]+>/<id:\d+>'        => '<controller>/view',
93
     * ];
94
     * ```
95
     *
96
     * Note that if you modify this property after the UrlManager object is created, make sure
97
     * you populate the array with rule objects instead of rule configurations.
98
     */
99
    public $rules = [];
100
    /**
101
     * @var string the URL suffix used when [[enablePrettyUrl]] is `true`.
102
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
103
     * This property is used only if [[enablePrettyUrl]] is `true`.
104
     */
105
    public $suffix;
106
    /**
107
     * @var boolean whether to show entry script name in the constructed URL. Defaults to `true`.
108
     * This property is used only if [[enablePrettyUrl]] is `true`.
109
     */
110
    public $showScriptName = true;
111
    /**
112
     * @var string the GET parameter name for route. This property is used only if [[enablePrettyUrl]] is `false`.
113
     */
114
    public $routeParam = 'r';
115
    /**
116
     * @var Cache|string the cache object or the application component ID of the cache object.
117
     * Compiled URL rules will be cached through this cache object, if it is available.
118
     *
119
     * After the UrlManager object is created, if you want to change this property,
120
     * you should only assign it with a cache object.
121
     * Set this property to `false` if you do not want to cache the URL rules.
122
     */
123
    public $cache = 'cache';
124
    /**
125
     * @var array the default configuration of URL rules. Individual rule configurations
126
     * specified via [[rules]] will take precedence when the same property of the rule is configured.
127
     */
128
    public $ruleConfig = ['class' => UrlRule::class];
129
    /**
130
     * @var UrlNormalizer|array|false the configuration for [[UrlNormalizer]] used by this UrlManager.
131
     * If `false` normalization will be skipped.
132
     * @since 2.0.10
133
     */
134
    public $normalizer = ['class' => UrlNormalizer::class];
135
136
    /**
137
     * @var string the cache key for cached rules
138
     * @since 2.0.8
139
     */
140
    protected $cacheKey = __CLASS__;
141
142
    private $_baseUrl;
143
    private $_scriptUrl;
144
    private $_hostInfo;
145
    private $_ruleCache;
146
147
148
    /**
149
     * Initializes UrlManager.
150
     */
151 40
    public function init()
152
    {
153 40
        parent::init();
154
155 40
        if ($this->normalizer !== false) {
156 39
            $this->normalizer = Instance::ensure($this->normalizer, UrlNormalizer::class);
157 39
        }
158
159 40
        if (!$this->enablePrettyUrl || empty($this->rules)) {
160 33
            return;
161
        }
162 9
        if (is_string($this->cache)) {
163 1
            $this->cache = Yii::$app->get($this->cache, false);
164 1
        }
165 9
        if ($this->cache instanceof Cache) {
166
            $cacheKey = $this->cacheKey;
167
            $hash = md5(json_encode($this->rules));
168
            if (($data = $this->cache->get($cacheKey)) !== false && isset($data[1]) && $data[1] === $hash) {
169
                $this->rules = $data[0];
170
            } else {
171
                $this->rules = $this->buildRules($this->rules);
172
                $this->cache->set($cacheKey, [$this->rules, $hash]);
173
            }
174
        } else {
175 9
            $this->rules = $this->buildRules($this->rules);
176
        }
177 9
    }
178
179
    /**
180
     * Adds additional URL rules.
181
     *
182
     * This method will call [[buildRules()]] to parse the given rule declarations and then append or insert
183
     * them to the existing [[rules]].
184
     *
185
     * Note that if [[enablePrettyUrl]] is `false`, this method will do nothing.
186
     *
187
     * @param array $rules the new rules to be added. Each array element represents a single rule declaration.
188
     * Please refer to [[rules]] for the acceptable rule format.
189
     * @param boolean $append whether to add the new rules by appending them to the end of the existing rules.
190
     */
191
    public function addRules($rules, $append = true)
192
    {
193
        if (!$this->enablePrettyUrl) {
194
            return;
195
        }
196
        $rules = $this->buildRules($rules);
197
        if ($append) {
198
            $this->rules = array_merge($this->rules, $rules);
199
        } else {
200
            $this->rules = array_merge($rules, $this->rules);
201
        }
202
    }
203
204
    /**
205
     * Builds URL rule objects from the given rule declarations.
206
     * @param array $rules the rule declarations. Each array element represents a single rule declaration.
207
     * Please refer to [[rules]] for the acceptable rule formats.
208
     * @return UrlRuleInterface[] the rule objects built from the given rule declarations
209
     * @throws InvalidConfigException if a rule declaration is invalid
210
     */
211 9
    protected function buildRules($rules)
212
    {
213 9
        $compiledRules = [];
214 9
        $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
215 9
        foreach ($rules as $key => $rule) {
216 9
            if (is_string($rule)) {
217 8
                $rule = ['route' => $rule];
218 8
                if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) {
219 1
                    $rule['verb'] = explode(',', $matches[1]);
220
                    // rules that do not apply for GET requests should not be use to create urls
221 1
                    if (!in_array('GET', $rule['verb'])) {
222 1
                        $rule['mode'] = UrlRule::PARSING_ONLY;
223 1
                    }
224 1
                    $key = $matches[4];
225 1
                }
226 8
                $rule['pattern'] = $key;
227 8
            }
228 9
            if (is_array($rule)) {
229 9
                $rule = Yii::createObject(array_merge($this->ruleConfig, $rule));
230 9
            }
231 9
            if (!$rule instanceof UrlRuleInterface) {
232
                throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.');
233
            }
234 9
            $compiledRules[] = $rule;
235 9
        }
236 9
        return $compiledRules;
237
    }
238
239
    /**
240
     * Parses the user request.
241
     * @param Request $request the request component
242
     * @return array|boolean the route and the associated parameters. The latter is always empty
243
     * if [[enablePrettyUrl]] is `false`. `false` is returned if the current request cannot be successfully parsed.
244
     */
245 3
    public function parseRequest($request)
246
    {
247 3
        if ($this->enablePrettyUrl) {
248
            /* @var $rule UrlRule */
249 3
            foreach ($this->rules as $rule) {
250 3
                if (($result = $rule->parseRequest($this, $request)) !== false) {
251 3
                    return $result;
252
                }
253 3
            }
254
255 1
            if ($this->enableStrictParsing) {
256 1
                return false;
257
            }
258
259 1
            Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__);
260
261 1
            $suffix = (string) $this->suffix;
262 1
            $pathInfo = $request->getPathInfo();
263 1
            $normalized = false;
264 1
            if ($this->normalizer !== false) {
265 1
                $pathInfo = $this->normalizer->normalizePathInfo($pathInfo, $suffix, $normalized);
266 1
            }
267 1
            if ($suffix !== '' && $pathInfo !== '') {
268 1
                $n = strlen($this->suffix);
269 1
                if (substr_compare($pathInfo, $this->suffix, -$n, $n) === 0) {
270 1
                    $pathInfo = substr($pathInfo, 0, -$n);
271 1
                    if ($pathInfo === '') {
272
                        // suffix alone is not allowed
273
                        return false;
274
                    }
275 1
                } else {
276
                    // suffix doesn't match
277 1
                    return false;
278
                }
279 1
            }
280
281 1
            if ($normalized) {
282
                // pathInfo was changed by normalizer - we need also normalize route
283 1
                return $this->normalizer->normalizeRoute([$pathInfo, []]);
284
            } else {
285 1
                return [$pathInfo, []];
286
            }
287
        } else {
288 1
            Yii::trace('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__);
289 1
            $route = $request->getQueryParam($this->routeParam, '');
290 1
            if (is_array($route)) {
291 1
                $route = '';
292 1
            }
293
294 1
            return [(string) $route, []];
295
        }
296
    }
297
298
    /**
299
     * Creates a URL using the given route and query parameters.
300
     *
301
     * You may specify the route as a string, e.g., `site/index`. You may also use an array
302
     * if you want to specify additional query parameters for the URL being created. The
303
     * array format must be:
304
     *
305
     * ```php
306
     * // generates: /index.php?r=site%2Findex&param1=value1&param2=value2
307
     * ['site/index', 'param1' => 'value1', 'param2' => 'value2']
308
     * ```
309
     *
310
     * If you want to create a URL with an anchor, you can use the array format with a `#` parameter.
311
     * For example,
312
     *
313
     * ```php
314
     * // generates: /index.php?r=site%2Findex&param1=value1#name
315
     * ['site/index', 'param1' => 'value1', '#' => 'name']
316
     * ```
317
     *
318
     * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL.
319
     *
320
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
321
     * as an absolute route.
322
     *
323
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
324
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
325
     * @return string the created URL
326
     */
327 27
    public function createUrl($params)
328
    {
329 27
        $params = (array) $params;
330 27
        $anchor = isset($params['#']) ? '#' . $params['#'] : '';
331 27
        unset($params['#'], $params[$this->routeParam]);
332
333 27
        $route = trim($params[0], '/');
334 27
        unset($params[0]);
335
336 27
        $baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $this->getScriptUrl() : $this->getBaseUrl();
337
338 27
        if ($this->enablePrettyUrl) {
339 7
            $cacheKey = $route . '?';
340 7
            foreach ($params as $key => $value) {
341 5
                if ($value !== null) {
342 5
                    $cacheKey .= $key . '&';
343 5
                }
344 7
            }
345
346 7
            $url = $this->getUrlFromCache($cacheKey, $route, $params);
347
348 7
            if ($url === false) {
349 7
                $cacheable = true;
350 7
                foreach ($this->rules as $rule) {
351
                    /* @var $rule UrlRule */
352 7
                    if (!empty($rule->defaults) && $rule->mode !== UrlRule::PARSING_ONLY) {
353
                        // if there is a rule with default values involved, the matching result may not be cached
354 1
                        $cacheable = false;
355 1
                    }
356 7
                    if (($url = $rule->createUrl($this, $route, $params)) !== false) {
357 6
                        if ($cacheable) {
358 6
                            $this->setRuleToCache($cacheKey, $rule);
359 6
                        }
360 6
                        break;
361
                    }
362 7
                }
363 7
            }
364
365 7
            if ($url !== false) {
366 6
                if (strpos($url, '://') !== false) {
367 3
                    if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) {
368 2
                        return substr($url, 0, $pos) . $baseUrl . substr($url, $pos) . $anchor;
369
                    } else {
370 1
                        return $url . $baseUrl . $anchor;
371
                    }
372
                } else {
373 4
                    return "$baseUrl/{$url}{$anchor}";
374
                }
375
            }
376
377 2
            if ($this->suffix !== null) {
378 1
                $route .= $this->suffix;
379 1
            }
380 2
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
381 2
                $route .= '?' . $query;
382 2
            }
383
384 2
            return "$baseUrl/{$route}{$anchor}";
385
        } else {
386 21
            $url = "$baseUrl?{$this->routeParam}=" . urlencode($route);
387 21
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
388 19
                $url .= '&' . $query;
389 19
            }
390
391 21
            return $url . $anchor;
392
        }
393
    }
394
395
    /**
396
     * Get URL from internal cache if exists
397
     * @param string $cacheKey generated cache key to store data.
398
     * @param string $route the route (e.g. `site/index`).
399
     * @param array $params rule params.
400
     * @return boolean|string the created URL
401
     * @see createUrl()
402
     * @since 2.0.8
403
     */
404 7
    protected function getUrlFromCache($cacheKey, $route, $params)
405
    {
406 7
        if (!empty($this->_ruleCache[$cacheKey])) {
407 4
            foreach ($this->_ruleCache[$cacheKey] as $rule) {
408
                /* @var $rule UrlRule */
409 4
                if (($url = $rule->createUrl($this, $route, $params)) !== false) {
410 4
                    return $url;
411
                }
412
            }
413
        } else {
414 7
            $this->_ruleCache[$cacheKey] = [];
415
        }
416 7
        return false;
417
    }
418
419
    /**
420
     * Store rule (e.g. [[UrlRule]]) to internal cache
421
     * @param $cacheKey
422
     * @param UrlRuleInterface $rule
423
     * @since 2.0.8
424
     */
425 6
    protected function setRuleToCache($cacheKey, UrlRuleInterface $rule)
426
    {
427 6
        $this->_ruleCache[$cacheKey][] = $rule;
428 6
    }
429
430
    /**
431
     * Creates an absolute URL using the given route and query parameters.
432
     *
433
     * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]].
434
     *
435
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
436
     * as an absolute route.
437
     *
438
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
439
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
440
     * @param string $scheme the scheme to use for the url (either `http` or `https`). If not specified
441
     * the scheme of the current request will be used.
442
     * @return string the created URL
443
     * @see createUrl()
444
     */
445 10
    public function createAbsoluteUrl($params, $scheme = null)
446
    {
447 10
        $params = (array) $params;
448 10
        $url = $this->createUrl($params);
449 10
        if (strpos($url, '://') === false) {
450 8
            $url = $this->getHostInfo() . $url;
451 8
        }
452 10
        if (is_string($scheme) && ($pos = strpos($url, '://')) !== false) {
453 4
            $url = $scheme . substr($url, $pos);
454 4
        }
455
456 10
        return $url;
457
    }
458
459
    /**
460
     * Returns the base URL that is used by [[createUrl()]] to prepend to created URLs.
461
     * It defaults to [[Request::baseUrl]].
462
     * This is mainly used when [[enablePrettyUrl]] is `true` and [[showScriptName]] is `false`.
463
     * @return string the base URL that is used by [[createUrl()]] to prepend to created URLs.
464
     * @throws InvalidConfigException if running in console application and [[baseUrl]] is not configured.
465
     */
466 5
    public function getBaseUrl()
467
    {
468 5
        if ($this->_baseUrl === null) {
469 1
            $request = Yii::$app->getRequest();
470 1
            if ($request instanceof Request) {
471 1
                $this->_baseUrl = $request->getBaseUrl();
472 1
            } else {
473
                throw new InvalidConfigException('Please configure UrlManager::baseUrl correctly as you are running a console application.');
474
            }
475 1
        }
476
477 5
        return $this->_baseUrl;
478
    }
479
480
    /**
481
     * Sets the base URL that is used by [[createUrl()]] to prepend to created URLs.
482
     * This is mainly used when [[enablePrettyUrl]] is `true` and [[showScriptName]] is `false`.
483
     * @param string $value the base URL that is used by [[createUrl()]] to prepend to created URLs.
484
     */
485 13
    public function setBaseUrl($value)
486
    {
487 13
        $this->_baseUrl = $value === null ? null : rtrim($value, '/');
488 13
    }
489
490
    /**
491
     * Returns the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
492
     * It defaults to [[Request::scriptUrl]].
493
     * This is mainly used when [[enablePrettyUrl]] is `false` or [[showScriptName]] is `true`.
494
     * @return string the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
495
     * @throws InvalidConfigException if running in console application and [[scriptUrl]] is not configured.
496
     */
497 24
    public function getScriptUrl()
498
    {
499 24
        if ($this->_scriptUrl === null) {
500 7
            $request = Yii::$app->getRequest();
501 7
            if ($request instanceof Request) {
502 7
                $this->_scriptUrl = $request->getScriptUrl();
503 7
            } else {
504
                throw new InvalidConfigException('Please configure UrlManager::scriptUrl correctly as you are running a console application.');
505
            }
506 7
        }
507
508 24
        return $this->_scriptUrl;
509
    }
510
511
    /**
512
     * Sets the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
513
     * This is mainly used when [[enablePrettyUrl]] is `false` or [[showScriptName]] is `true`.
514
     * @param string $value the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
515
     */
516 21
    public function setScriptUrl($value)
517
    {
518 21
        $this->_scriptUrl = $value;
519 21
    }
520
521
    /**
522
     * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
523
     * @return string the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
524
     * @throws InvalidConfigException if running in console application and [[hostInfo]] is not configured.
525
     */
526 10
    public function getHostInfo()
527
    {
528 10
        if ($this->_hostInfo === null) {
529 4
            $request = Yii::$app->getRequest();
530 4
            if ($request instanceof \yii\web\Request) {
531 4
                $this->_hostInfo = $request->getHostInfo();
532 4
            } else {
533
                throw new InvalidConfigException('Please configure UrlManager::hostInfo correctly as you are running a console application.');
534
            }
535 4
        }
536
537 10
        return $this->_hostInfo;
538
    }
539
540
    /**
541
     * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
542
     * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
543
     */
544 9
    public function setHostInfo($value)
545
    {
546 9
        $this->_hostInfo = $value === null ? null : rtrim($value, '/');
547 9
    }
548
}
549