UrlManager.php 16.6 KB
Newer Older
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
5 6 7 8 9
 * @license http://www.yiiframework.com/license/
 */

namespace yii\web;

Qiang Xue committed
10
use Yii;
Qiang Xue committed
11
use yii\base\Component;
12
use yii\base\InvalidConfigException;
13
use yii\caching\Cache;
Qiang Xue committed
14 15

/**
Qiang Xue committed
16
 * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules.
Qiang Xue committed
17
 *
18
 * UrlManager is configured as an application component in [[\yii\base\Application]] by default.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
 * You can access that instance via `Yii::$app->urlManager`.
 *
 * You can modify its configuration by adding an array to your application config under `components`
 * as it is shown in the following example:
 *
 * ~~~
 * 'urlManager' => [
 *     'enablePrettyUrl' => true,
 *     'rules' => [
 *         // your rules go here
 *     ],
 *     // ...
 * ]
 * ~~~
 *
34
 * @property string $baseUrl The base URL that is used by [[createUrl()]] to prepend URLs it creates.
35 36
 * @property string $hostInfo The host info (e.g. "http://www.example.com") that is used by
 * [[createAbsoluteUrl()]] to prepend URLs it creates.
37
 *
Qiang Xue committed
38 39 40 41 42
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class UrlManager extends Component
{
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
    /**
     * @var boolean whether to enable pretty URLs. Instead of putting all parameters in the query
     * string part of a URL, pretty URLs allow using path info to represent some of the parameters
     * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of
     * "/index.php?r=news/view&id=100".
     */
    public $enablePrettyUrl = false;
    /**
     * @var boolean whether to enable strict parsing. If strict parsing is enabled, the incoming
     * requested URL must match at least one of the [[rules]] in order to be treated as a valid request.
     * Otherwise, the path info part of the request will be treated as the requested route.
     * This property is used only when [[enablePrettyUrl]] is true.
     */
    public $enableStrictParsing = false;
    /**
     * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is true.
     * This property is used only if [[enablePrettyUrl]] is true. Each element in the array
     * is the configuration array for creating a single URL rule. The configuration will
     * be merged with [[ruleConfig]] first before it is used for creating the rule object.
     *
     * A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]]
     * and [[UrlRule::route|route]]: `'pattern' => 'route'`. That is, instead of using a configuration
     * array, one can use the key to represent the pattern and the value the corresponding route.
     * For example, `'post/<id:\d+>' => 'post/view'`.
     *
     * For RESTful routing the mentioned shortcut format also allows you to specify the
     * [[UrlRule::verb|HTTP verb]] that the rule should apply for.
     * You can do that  by prepending it to the pattern, separated by space.
     * For example, `'PUT post/<id:\d+>' => 'post/update'`.
     * You may specify multiple verbs by separating them with comma
     * like this: `'POST,PUT post/index' => 'post/create'`.
     * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE.
     * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way
     * so you normally would not specify a verb for normal GET request.
     *
     * Here is an example configuration for RESTful CRUD controller:
     *
     * ~~~php
     * [
     *     'dashboard' => 'site/index',
     *
     *     'POST <controller:\w+>s' => '<controller>/create',
     *     '<controller:\w+>s' => '<controller>/index',
     *
     *     'PUT <controller:\w+>/<id:\d+>'    => '<controller>/update',
     *     'DELETE <controller:\w+>/<id:\d+>' => '<controller>/delete',
     *     '<controller:\w+>/<id:\d+>'        => '<controller>/view',
     * ];
     * ~~~
     *
     * Note that if you modify this property after the UrlManager object is created, make sure
     * you populate the array with rule objects instead of rule configurations.
     */
    public $rules = [];
    /**
     * @var string the URL suffix used when in 'path' format.
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
     * This property is used only if [[enablePrettyUrl]] is true.
     */
    public $suffix;
    /**
     * @var boolean whether to show entry script name in the constructed URL. Defaults to true.
     * This property is used only if [[enablePrettyUrl]] is true.
     */
    public $showScriptName = true;
    /**
     * @var string the GET parameter name for route. This property is used only if [[enablePrettyUrl]] is false.
     */
    public $routeParam = 'r';
    /**
     * @var Cache|string the cache object or the application component ID of the cache object.
     * Compiled URL rules will be cached through this cache object, if it is available.
     *
     * After the UrlManager object is created, if you want to change this property,
     * you should only assign it with a cache object.
     * Set this property to null if you do not want to cache the URL rules.
     */
    public $cache = 'cache';
    /**
     * @var array the default configuration of URL rules. Individual rule configurations
     * specified via [[rules]] will take precedence when the same property of the rule is configured.
     */
    public $ruleConfig = ['class' => 'yii\web\UrlRule'];
Qiang Xue committed
126

127 128
    private $_baseUrl;
    private $_hostInfo;
Qiang Xue committed
129

130 131 132 133 134 135
    /**
     * Initializes UrlManager.
     */
    public function init()
    {
        parent::init();
Qiang Xue committed
136

137 138 139 140
        if (!$this->enablePrettyUrl || empty($this->rules)) {
            return;
        }
        if (is_string($this->cache)) {
141
            $this->cache = Yii::$app->get($this->cache, false);
142 143
        }
        if ($this->cache instanceof Cache) {
jeicd committed
144
            $cacheKey = __CLASS__;
145
            $hash = md5(json_encode($this->rules));
jeicd committed
146
            if (($data = $this->cache->get($cacheKey)) !== false && isset($data[1]) && $data[1] === $hash) {
147
                $this->rules = $data[0];
148 149 150
            } else {
                $this->rules = $this->buildRules($this->rules);
                $this->cache->set($cacheKey, [$this->rules, $hash]);
151
            }
152 153 154 155 156 157 158
        } else {
            $this->rules = $this->buildRules($this->rules);
        }
    }

    /**
     * Adds additional URL rules.
159
     *
160 161
     * This method will call [[buildRules()]] to parse the given rule declarations and then append or insert
     * them to the existing [[rules]].
162 163 164
     *
     * Note that if [[enablePrettyUrl]] is false, this method will do nothing.
     *
165 166 167 168 169 170
     * @param array $rules the new rules to be added. Each array element represents a single rule declaration.
     * Please refer to [[rules]] for the acceptable rule format.
     * @param boolean $append whether to add the new rules by appending them to the end of the existing rules.
     */
    public function addRules($rules, $append = true)
    {
171 172 173
        if (!$this->enablePrettyUrl) {
            return;
        }
174 175 176 177 178
        $rules = $this->buildRules($rules);
        if ($append) {
            $this->rules = array_merge($this->rules, $rules);
        } else {
            $this->rules = array_merge($rules, $this->rules);
179
        }
180
    }
Qiang Xue committed
181

182 183 184 185 186 187 188 189 190 191
    /**
     * Builds URL rule objects from the given rule declarations.
     * @param array $rules the rule declarations. Each array element represents a single rule declaration.
     * Please refer to [[rules]] for the acceptable rule formats.
     * @return UrlRuleInterface[] the rule objects built from the given rule declarations
     * @throws InvalidConfigException if a rule declaration is invalid
     */
    protected function buildRules($rules)
    {
        $compiledRules = [];
192
        $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
193 194
        foreach ($rules as $key => $rule) {
            if (is_string($rule)) {
195 196 197 198 199 200 201 202
                $rule = ['route' => $rule];
                if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) {
                    $rule['verb'] = explode(',', $matches[1]);
                    $rule['mode'] = UrlRule::PARSING_ONLY;
                    $key = $matches[4];
                }
                $rule['pattern'] = $key;
            }
203 204 205
            if (is_array($rule)) {
                $rule = Yii::createObject(array_merge($this->ruleConfig, $rule));
            }
206 207 208
            if (!$rule instanceof UrlRuleInterface) {
                throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.');
            }
209
            $compiledRules[] = $rule;
210
        }
211
        return $compiledRules;
212
    }
Qiang Xue committed
213

214 215
    /**
     * Parses the user request.
216
     * @param Request $request the request component
217
     * @return array|boolean the route and the associated parameters. The latter is always empty
218
     * if [[enablePrettyUrl]] is false. False is returned if the current request cannot be successfully parsed.
219 220 221 222 223 224 225 226 227 228 229
     */
    public function parseRequest($request)
    {
        if ($this->enablePrettyUrl) {
            $pathInfo = $request->getPathInfo();
            /** @var UrlRule $rule */
            foreach ($this->rules as $rule) {
                if (($result = $rule->parseRequest($this, $request)) !== false) {
                    return $result;
                }
            }
230

231 232 233
            if ($this->enableStrictParsing) {
                return false;
            }
Qiang Xue committed
234

235
            Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__);
Qiang Xue committed
236

237
            $suffix = (string) $this->suffix;
238 239 240 241 242 243 244 245 246 247 248 249 250
            if ($suffix !== '' && $pathInfo !== '') {
                $n = strlen($this->suffix);
                if (substr($pathInfo, -$n) === $this->suffix) {
                    $pathInfo = substr($pathInfo, 0, -$n);
                    if ($pathInfo === '') {
                        // suffix alone is not allowed
                        return false;
                    }
                } else {
                    // suffix doesn't match
                    return false;
                }
            }
Qiang Xue committed
251

252 253 254 255 256 257 258
            return [$pathInfo, []];
        } else {
            Yii::trace('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__);
            $route = $request->getQueryParam($this->routeParam, '');
            if (is_array($route)) {
                $route = '';
            }
Qiang Xue committed
259

260
            return [(string) $route, []];
261 262
        }
    }
Qiang Xue committed
263

264
    /**
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
     * Creates a URL using the given route and query parameters.
     *
     * You may specify the route as a string, e.g., `site/index`. You may also use an array
     * if you want to specify additional query parameters for the URL being created. The
     * array format must be:
     *
     * ```php
     * // generates: /index.php?r=site/index&param1=value1&param2=value2
     * ['site/index', 'param1' => 'value1', 'param2' => 'value2']
     * ```
     *
     * If you want to create a URL with an anchor, you can use the array format with a `#` parameter.
     * For example,
     *
     * ```php
     * // generates: /index.php?r=site/index&param1=value1#name
     * ['site/index', 'param1' => 'value1', '#' => 'name']
     * ```
     *
284
     * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL.
285 286 287 288 289 290 291
     *
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
     * as an absolute route.
     *
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
     * @return string the created URL
292 293 294
     */
    public function createUrl($params)
    {
295
        $params = (array) $params;
296 297
        $anchor = isset($params['#']) ? '#' . $params['#'] : '';
        unset($params['#'], $params[$this->routeParam]);
Qiang Xue committed
298

299 300 301
        $route = trim($params[0], '/');
        unset($params[0]);
        $baseUrl = $this->getBaseUrl();
Qiang Xue committed
302

303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
        if ($this->enablePrettyUrl) {
            /** @var UrlRule $rule */
            foreach ($this->rules as $rule) {
                if (($url = $rule->createUrl($this, $route, $params)) !== false) {
                    if (strpos($url, '://') !== false) {
                        if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) {
                            return substr($url, 0, $pos) . $baseUrl . substr($url, $pos);
                        } else {
                            return $url . $baseUrl . $anchor;
                        }
                    } else {
                        return "$baseUrl/{$url}{$anchor}";
                    }
                }
            }
Qiang Xue committed
318

319 320 321 322 323 324
            if ($this->suffix !== null) {
                $route .= $this->suffix;
            }
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
                $route .= '?' . $query;
            }
Qiang Xue committed
325

326 327 328 329 330 331
            return "$baseUrl/{$route}{$anchor}";
        } else {
            $url = "$baseUrl?{$this->routeParam}=$route";
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
                $url .= '&' . $query;
            }
Qiang Xue committed
332

333 334 335
            return $url . $anchor;
        }
    }
Qiang Xue committed
336

337
    /**
338 339
     * Creates an absolute URL using the given route and query parameters.
     *
340
     * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]].
341 342 343 344 345 346 347 348 349
     *
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
     * as an absolute route.
     *
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
     * @param string $scheme the scheme to use for the url (either `http` or `https`). If not specified
     * the scheme of the current request will be used.
     * @return string the created URL
350 351
     * @see createUrl()
     */
352
    public function createAbsoluteUrl($params, $scheme = null)
353
    {
354
        $params = (array) $params;
355 356 357 358
        $url = $this->createUrl($params);
        if (strpos($url, '://') === false) {
            $url = $this->getHostInfo() . $url;
        }
359 360
        if (is_string($scheme) && ($pos = strpos($url, '://')) !== false) {
            $url = $scheme . substr($url, $pos);
361 362 363 364 365 366 367 368 369 370
        }

        return $url;
    }

    /**
     * Returns the base URL that is used by [[createUrl()]] to prepend URLs it creates.
     * It defaults to [[Request::scriptUrl]] if [[showScriptName]] is true or [[enablePrettyUrl]] is false;
     * otherwise, it defaults to [[Request::baseUrl]].
     * @return string the base URL that is used by [[createUrl()]] to prepend URLs it creates.
371
     * @throws InvalidConfigException if running in console application and [[baseUrl]] is not configured.
372 373 374 375 376
     */
    public function getBaseUrl()
    {
        if ($this->_baseUrl === null) {
            $request = Yii::$app->getRequest();
377 378 379 380 381
            if ($request instanceof \yii\web\Request) {
                $this->_baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $request->getScriptUrl() : $request->getBaseUrl();
            } else {
                throw new InvalidConfigException('Please configure UrlManager::baseUrl correctly as you are running a console application.');
            }
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
        }

        return $this->_baseUrl;
    }

    /**
     * Sets the base URL that is used by [[createUrl()]] to prepend URLs it creates.
     * @param string $value the base URL that is used by [[createUrl()]] to prepend URLs it creates.
     */
    public function setBaseUrl($value)
    {
        $this->_baseUrl = rtrim($value, '/');
    }

    /**
     * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates.
     * @return string the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates.
399
     * @throws InvalidConfigException if running in console application and [[hostInfo]] is not configured.
400 401 402 403
     */
    public function getHostInfo()
    {
        if ($this->_hostInfo === null) {
404 405 406 407 408 409
            $request = Yii::$app->getRequest();
            if ($request instanceof \yii\web\Request) {
                $this->_hostInfo = $request->getHostInfo();
            } else {
                throw new InvalidConfigException('Please configure UrlManager::hostInfo correctly as you are running a console application.');
            }
410 411 412 413 414 415 416 417 418 419 420 421 422
        }

        return $this->_hostInfo;
    }

    /**
     * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates.
     * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates.
     */
    public function setHostInfo($value)
    {
        $this->_hostInfo = rtrim($value, '/');
    }
Qiang Xue committed
423
}