UrlRule   F
last analyzed

Complexity

Total Complexity 111

Size/Duplication

Total Lines 568
Duplicated Lines 0 %

Test Coverage

Coverage 97.65%

Importance

Changes 0
Metric Value
eloc 236
dl 0
loc 568
ccs 208
cts 213
cp 0.9765
rs 2
c 0
b 0
f 0
wmc 111

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __toString() 0 16 5
A substitutePlaceholderNames() 0 10 3
A getParamRules() 0 3 1
A getCreateUrlStatus() 0 3 1
F createUrl() 0 84 31
A hasNormalizer() 0 3 1
B init() 0 29 11
A trimSlashes() 0 7 2
F parseRequest() 0 67 21
B preparePattern() 0 36 10
A getNormalizer() 0 7 2
D translatePattern() 0 77 23

How to fix   Complexity   

Complex Class

Complex classes like UrlRule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UrlRule, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\BaseObject;
12
use yii\base\InvalidConfigException;
13
14
/**
15
 * UrlRule represents a rule used by [[UrlManager]] for parsing and generating URLs.
16
 *
17
 * To define your own URL parsing and creation logic you can extend from this class
18
 * and add it to [[UrlManager::rules]] like this:
19
 *
20
 * ```php
21
 * 'rules' => [
22
 *     ['class' => 'MyUrlRule', 'pattern' => '...', 'route' => 'site/index', ...],
23
 *     // ...
24
 * ]
25
 * ```
26
 *
27
 * @property-read int|null $createUrlStatus Status of the URL creation after the last [[createUrl()]] call.
28
 * `null` if rule does not provide info about create status.
29
 *
30
 * @author Qiang Xue <[email protected]>
31
 * @since 2.0
32
 */
33
class UrlRule extends BaseObject implements UrlRuleInterface
34
{
35
    /**
36
     * Set [[mode]] with this value to mark that this rule is for URL parsing only.
37
     */
38
    const PARSING_ONLY = 1;
39
    /**
40
     * Set [[mode]] with this value to mark that this rule is for URL creation only.
41
     */
42
    const CREATION_ONLY = 2;
43
    /**
44
     * Represents the successful URL generation by last [[createUrl()]] call.
45
     * @see createStatus
46
     * @since 2.0.12
47
     */
48
    const CREATE_STATUS_SUCCESS = 0;
49
    /**
50
     * Represents the unsuccessful URL generation by last [[createUrl()]] call, because rule does not support
51
     * creating URLs.
52
     * @see createStatus
53
     * @since 2.0.12
54
     */
55
    const CREATE_STATUS_PARSING_ONLY = 1;
56
    /**
57
     * Represents the unsuccessful URL generation by last [[createUrl()]] call, because of mismatched route.
58
     * @see createStatus
59
     * @since 2.0.12
60
     */
61
    const CREATE_STATUS_ROUTE_MISMATCH = 2;
62
    /**
63
     * Represents the unsuccessful URL generation by last [[createUrl()]] call, because of mismatched
64
     * or missing parameters.
65
     * @see createStatus
66
     * @since 2.0.12
67
     */
68
    const CREATE_STATUS_PARAMS_MISMATCH = 4;
69
70
    /**
71
     * @var string|null the name of this rule. If not set, it will use [[pattern]] as the name.
72
     */
73
    public $name;
74
    /**
75
     * On the rule initialization, the [[pattern]] matching parameters names will be replaced with [[placeholders]].
76
     * @var string the pattern used to parse and create the path info part of a URL.
77
     * @see host
78
     * @see placeholders
79
     */
80
    public $pattern;
81
    /**
82
     * @var string|null the pattern used to parse and create the host info part of a URL (e.g. `https://example.com`).
83
     * @see pattern
84
     */
85
    public $host;
86
    /**
87
     * @var string the route to the controller action
88
     */
89
    public $route;
90
    /**
91
     * @var array the default GET parameters (name => value) that this rule provides.
92
     * When this rule is used to parse the incoming request, the values declared in this property
93
     * will be injected into $_GET.
94
     */
95
    public $defaults = [];
96
    /**
97
     * @var string|null the URL suffix used for this rule.
98
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
99
     * If not set, the value of [[UrlManager::suffix]] will be used.
100
     */
101
    public $suffix;
102
    /**
103
     * @var string|array|null the HTTP verb (e.g. GET, POST, DELETE) that this rule should match.
104
     * Use array to represent multiple verbs that this rule may match.
105
     * If this property is not set, the rule can match any verb.
106
     * Note that this property is only used when parsing a request. It is ignored for URL creation.
107
     */
108
    public $verb;
109
    /**
110
     * @var int|null a value indicating if this rule should be used for both request parsing and URL creation,
111
     * parsing only, or creation only.
112
     * If not set or 0, it means the rule is both request parsing and URL creation.
113
     * If it is [[PARSING_ONLY]], the rule is for request parsing only.
114
     * If it is [[CREATION_ONLY]], the rule is for URL creation only.
115
     */
116
    public $mode;
117
    /**
118
     * @var bool a value indicating if parameters should be url encoded.
119
     */
120
    public $encodeParams = true;
121
    /**
122
     * @var UrlNormalizer|array|false|null the configuration for [[UrlNormalizer]] used by this rule.
123
     * If `null`, [[UrlManager::normalizer]] will be used, if `false`, normalization will be skipped
124
     * for this rule.
125
     * @since 2.0.10
126
     */
127
    public $normalizer;
128
129
    /**
130
     * @var int|null status of the URL creation after the last [[createUrl()]] call.
131
     * @since 2.0.12
132
     */
133
    protected $createStatus;
134
    /**
135
     * @var array list of placeholders for matching parameters names. Used in [[parseRequest()]], [[createUrl()]].
136
     * On the rule initialization, the [[pattern]] parameters names will be replaced with placeholders.
137
     * This array contains relations between the original parameters names and their placeholders.
138
     * The array keys are the placeholders and the values are the original names.
139
     *
140
     * @see parseRequest()
141
     * @see createUrl()
142
     * @since 2.0.7
143
     */
144
    protected $placeholders = [];
145
146
    /**
147
     * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL.
148
     */
149
    private $_template;
150
    /**
151
     * @var string the regex for matching the route part. This is used in generating URL.
152
     */
153
    private $_routeRule;
154
    /**
155
     * @var array list of regex for matching parameters. This is used in generating URL.
156
     */
157
    private $_paramRules = [];
158
    /**
159
     * @var array list of parameters used in the route.
160
     */
161
    private $_routeParams = [];
162
163
164
    /**
165
     * @return string
166
     * @since 2.0.11
167
     */
168 13
    public function __toString()
169
    {
170 13
        $str = '';
171 13
        if ($this->verb !== null) {
172 3
            $str .= implode(',', $this->verb) . ' ';
0 ignored issues
show
Bug introduced by
It seems like $this->verb can also be of type string; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
            $str .= implode(',', /** @scrutinizer ignore-type */ $this->verb) . ' ';
Loading history...
173
        }
174 13
        if ($this->host !== null && strrpos($this->name, $this->host) === false) {
0 ignored issues
show
Bug introduced by
It seems like $this->name can also be of type null; however, parameter $haystack of strrpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        if ($this->host !== null && strrpos(/** @scrutinizer ignore-type */ $this->name, $this->host) === false) {
Loading history...
175 1
            $str .= $this->host . '/';
176
        }
177 13
        $str .= $this->name;
178
179 13
        if ($str === '') {
180 1
            return '/';
181
        }
182
183 13
        return $str;
184
    }
185
186
    /**
187
     * Initializes this rule.
188
     */
189 101
    public function init()
190
    {
191 101
        if ($this->pattern === null) {
192
            throw new InvalidConfigException('UrlRule::pattern must be set.');
193
        }
194 101
        if ($this->route === null) {
195
            throw new InvalidConfigException('UrlRule::route must be set.');
196
        }
197 101
        if (is_array($this->normalizer)) {
198 1
            $normalizerConfig = array_merge(['class' => UrlNormalizer::className()], $this->normalizer);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

198
            $normalizerConfig = array_merge(['class' => /** @scrutinizer ignore-deprecated */ UrlNormalizer::className()], $this->normalizer);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
199 1
            $this->normalizer = Yii::createObject($normalizerConfig);
200
        }
201 101
        if ($this->normalizer !== null && $this->normalizer !== false && !$this->normalizer instanceof UrlNormalizer) {
202
            throw new InvalidConfigException('Invalid config for UrlRule::normalizer.');
203
        }
204 101
        if ($this->verb !== null) {
205 22
            if (is_array($this->verb)) {
206 22
                foreach ($this->verb as $i => $verb) {
207 22
                    $this->verb[$i] = strtoupper($verb);
208
                }
209
            } else {
210 1
                $this->verb = [strtoupper($this->verb)];
211
            }
212
        }
213 101
        if ($this->name === null) {
214 101
            $this->name = $this->pattern;
215
        }
216
217 101
        $this->preparePattern();
218
    }
219
220
    /**
221
     * Process [[$pattern]] on rule initialization.
222
     */
223 101
    private function preparePattern()
224
    {
225 101
        $this->pattern = $this->trimSlashes($this->pattern);
226 101
        $this->route = trim($this->route, '/');
227
228 101
        if ($this->host !== null) {
229 17
            $this->host = rtrim($this->host, '/');
230 17
            $this->pattern = rtrim($this->host . '/' . $this->pattern, '/');
231 97
        } elseif ($this->pattern === '') {
232 23
            $this->_template = '';
233 23
            $this->pattern = '#^$#u';
234
235 23
            return;
236 87
        } elseif (($pos = strpos($this->pattern, '://')) !== false) {
237 9
            if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) {
238 9
                $this->host = substr($this->pattern, 0, $pos2);
239
            } else {
240 9
                $this->host = $this->pattern;
241
            }
242 83
        } elseif (strncmp($this->pattern, '//', 2) === 0) {
243 8
            if (($pos2 = strpos($this->pattern, '/', 2)) !== false) {
244 8
                $this->host = substr($this->pattern, 0, $pos2);
245
            } else {
246 8
                $this->host = $this->pattern;
247
            }
248
        } else {
249 79
            $this->pattern = '/' . $this->pattern . '/';
250
        }
251
252 91
        if (strpos($this->route, '<') !== false && preg_match_all('/<([\w._-]+)>/', $this->route, $matches)) {
253 16
            foreach ($matches[1] as $name) {
254 16
                $this->_routeParams[$name] = "<$name>";
255
            }
256
        }
257
258 91
        $this->translatePattern(true);
259
    }
260
261
    /**
262
     * Prepares [[$pattern]] on rule initialization - replace parameter names by placeholders.
263
     *
264
     * @param bool $allowAppendSlash Defines position of slash in the param pattern in [[$pattern]].
265
     * If `false` slash will be placed at the beginning of param pattern. If `true` slash position will be detected
266
     * depending on non-optional pattern part.
267
     */
268 91
    private function translatePattern($allowAppendSlash)
269
    {
270 91
        $tr = [
271 91
            '.' => '\\.',
272 91
            '*' => '\\*',
273 91
            '$' => '\\$',
274 91
            '[' => '\\[',
275 91
            ']' => '\\]',
276 91
            '(' => '\\(',
277 91
            ')' => '\\)',
278 91
        ];
279
280 91
        $tr2 = [];
281 91
        $requiredPatternPart = $this->pattern;
282 91
        $oldOffset = 0;
283 91
        if (preg_match_all('/<([\w._-]+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
284 89
            $appendSlash = false;
285 89
            foreach ($matches as $match) {
286 89
                $name = $match[1][0];
287 89
                $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
288 89
                $placeholder = 'a' . hash('crc32b', $name); // placeholder must begin with a letter
289 89
                $this->placeholders[$placeholder] = $name;
290 89
                if (array_key_exists($name, $this->defaults)) {
291 16
                    $length = strlen($match[0][0]);
292 16
                    $offset = $match[0][1];
293 16
                    $requiredPatternPart = str_replace("/{$match[0][0]}/", '//', $requiredPatternPart);
294
                    if (
295 16
                        $allowAppendSlash
296 16
                        && ($appendSlash || $offset === 1)
297 16
                        && (($offset - $oldOffset) === 1)
298 16
                        && isset($this->pattern[$offset + $length])
299 16
                        && $this->pattern[$offset + $length] === '/'
300 16
                        && isset($this->pattern[$offset + $length + 1])
301
                    ) {
302
                        // if pattern starts from optional params, put slash at the end of param pattern
303
                        // @see https://github.com/yiisoft/yii2/issues/13086
304 4
                        $appendSlash = true;
305 4
                        $tr["<$name>/"] = "((?P<$placeholder>$pattern)/)?";
306
                    } elseif (
307 16
                        $offset > 1
308 16
                        && $this->pattern[$offset - 1] === '/'
309 16
                        && (!isset($this->pattern[$offset + $length]) || $this->pattern[$offset + $length] === '/')
310
                    ) {
311 8
                        $appendSlash = false;
312 8
                        $tr["/<$name>"] = "(/(?P<$placeholder>$pattern))?";
313
                    }
314 16
                    $tr["<$name>"] = "(?P<$placeholder>$pattern)?";
315 16
                    $oldOffset = $offset + $length;
316
                } else {
317 89
                    $appendSlash = false;
318 89
                    $tr["<$name>"] = "(?P<$placeholder>$pattern)";
319
                }
320
321 89
                if (isset($this->_routeParams[$name])) {
322 16
                    $tr2["<$name>"] = "(?P<$placeholder>$pattern)";
323
                } else {
324 89
                    $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#u";
325
                }
326
            }
327
        }
328
329
        // we have only optional params in route - ensure slash position on param patterns
330 91
        if ($allowAppendSlash && trim($requiredPatternPart, '/') === '') {
331 12
            $this->translatePattern(false);
332 12
            return;
333
        }
334
335 91
        $this->_template = preg_replace('/<([\w._-]+):?([^>]+)?>/', '<$1>', $this->pattern);
336 91
        $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u';
337
338
        // if host starts with relative scheme, then insert pattern to match any
339 91
        if ($this->host !== null && strncmp($this->host, '//', 2) === 0) {
340 8
            $this->pattern = substr_replace($this->pattern, '[\w]+://', 2, 0);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->pattern, '[\w]+://', 2, 0) can also be of type array. However, the property $pattern is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
341
        }
342
343 91
        if (!empty($this->_routeParams)) {
344 16
            $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
345
        }
346
    }
347
348
    /**
349
     * @param UrlManager $manager the URL manager
350
     * @return UrlNormalizer|null
351
     * @since 2.0.10
352
     */
353 16
    protected function getNormalizer($manager)
354
    {
355 16
        if ($this->normalizer === null) {
356 15
            return $manager->normalizer;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $manager->normalizer also could return the type array|boolean|string which is incompatible with the documented return type null|yii\web\UrlNormalizer.
Loading history...
357
        }
358
359 1
        return $this->normalizer;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->normalizer also could return the type array|boolean which is incompatible with the documented return type null|yii\web\UrlNormalizer.
Loading history...
360
    }
361
362
    /**
363
     * @param UrlManager $manager the URL manager
364
     * @return bool
365
     * @since 2.0.10
366
     */
367 16
    protected function hasNormalizer($manager)
368
    {
369 16
        return $this->getNormalizer($manager) instanceof UrlNormalizer;
370
    }
371
372
    /**
373
     * Parses the given request and returns the corresponding route and parameters.
374
     * @param UrlManager $manager the URL manager
375
     * @param Request $request the request component
376
     * @return array|bool the parsing result. The route and the parameters are returned as an array.
377
     * If `false`, it means this rule cannot be used to parse this path info.
378
     */
379 16
    public function parseRequest($manager, $request)
380
    {
381 16
        if ($this->mode === self::CREATION_ONLY) {
382 3
            return false;
383
        }
384
385 16
        if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
0 ignored issues
show
Bug introduced by
It seems like $this->verb can also be of type string; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

385
        if (!empty($this->verb) && !in_array($request->getMethod(), /** @scrutinizer ignore-type */ $this->verb, true)) {
Loading history...
386 2
            return false;
387
        }
388
389 16
        $suffix = (string) ($this->suffix === null ? $manager->suffix : $this->suffix);
390 16
        $pathInfo = $request->getPathInfo();
391 16
        $normalized = false;
392 16
        if ($this->hasNormalizer($manager)) {
393 4
            $pathInfo = $this->getNormalizer($manager)->normalizePathInfo($pathInfo, $suffix, $normalized);
394
        }
395 16
        if ($suffix !== '' && $pathInfo !== '') {
396 9
            $n = strlen($suffix);
397 9
            if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) {
398 9
                $pathInfo = substr($pathInfo, 0, -$n);
399 9
                if ($pathInfo === '') {
400
                    // suffix alone is not allowed
401 9
                    return false;
402
                }
403
            } else {
404 8
                return false;
405
            }
406
        }
407
408 16
        if ($this->host !== null) {
409 3
            $pathInfo = strtolower($request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo);
0 ignored issues
show
Bug introduced by
It seems like $request->getHostInfo() can also be of type null; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

409
            $pathInfo = strtolower(/** @scrutinizer ignore-type */ $request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo);
Loading history...
410
        }
411
412 16
        if (!preg_match($this->pattern, $pathInfo, $matches)) {
413 15
            return false;
414
        }
415 16
        $matches = $this->substitutePlaceholderNames($matches);
416
417 16
        foreach ($this->defaults as $name => $value) {
418 3
            if (!isset($matches[$name]) || $matches[$name] === '') {
419 3
                $matches[$name] = $value;
420
            }
421
        }
422 16
        $params = $this->defaults;
423 16
        $tr = [];
424 16
        foreach ($matches as $name => $value) {
425 16
            if (isset($this->_routeParams[$name])) {
426 4
                $tr[$this->_routeParams[$name]] = $value;
427 4
                unset($params[$name]);
428 16
            } elseif (isset($this->_paramRules[$name])) {
429 15
                $params[$name] = $value;
430
            }
431
        }
432 16
        if ($this->_routeRule !== null) {
433 4
            $route = strtr($this->route, $tr);
434
        } else {
435 16
            $route = $this->route;
436
        }
437
438 16
        Yii::debug("Request parsed with URL rule: {$this->name}", __METHOD__);
439
440 16
        if ($normalized) {
441
            // pathInfo was changed by normalizer - we need also normalize route
442 4
            return $this->getNormalizer($manager)->normalizeRoute([$route, $params]);
443
        }
444
445 14
        return [$route, $params];
446
    }
447
448
    /**
449
     * Creates a URL according to the given route and parameters.
450
     * @param UrlManager $manager the URL manager
451
     * @param string $route the route. It should not have slashes at the beginning or the end.
452
     * @param array $params the parameters
453
     * @return string|bool the created URL, or `false` if this rule cannot be used for creating this URL.
454
     */
455 78
    public function createUrl($manager, $route, $params)
456
    {
457 78
        if ($this->mode === self::PARSING_ONLY) {
458 3
            $this->createStatus = self::CREATE_STATUS_PARSING_ONLY;
459 3
            return false;
460
        }
461
462 77
        $tr = [];
463
464
        // match the route part first
465 77
        if ($route !== $this->route) {
466 64
            if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) {
467 12
                $matches = $this->substitutePlaceholderNames($matches);
468 12
                foreach ($this->_routeParams as $name => $token) {
469 12
                    if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) {
470 1
                        $tr[$token] = '';
471
                    } else {
472 12
                        $tr[$token] = $matches[$name];
473
                    }
474
                }
475
            } else {
476 63
                $this->createStatus = self::CREATE_STATUS_ROUTE_MISMATCH;
477 63
                return false;
478
            }
479
        }
480
481
        // match default params
482
        // if a default param is not in the route pattern, its value must also be matched
483 77
        foreach ($this->defaults as $name => $value) {
484 15
            if (isset($this->_routeParams[$name])) {
485 1
                continue;
486
            }
487 15
            if (!isset($params[$name])) {
488
                // allow omit empty optional params
489
                // @see https://github.com/yiisoft/yii2/issues/10970
490 10
                if (in_array($name, $this->placeholders) && strcmp($value, '') === 0) {
491 1
                    $params[$name] = '';
492
                } else {
493 10
                    $this->createStatus = self::CREATE_STATUS_PARAMS_MISMATCH;
494 10
                    return false;
495
                }
496
            }
497 15
            if (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically
498 15
                unset($params[$name]);
499 15
                if (isset($this->_paramRules[$name])) {
500 15
                    $tr["<$name>"] = '';
501
                }
502 14
            } elseif (!isset($this->_paramRules[$name])) {
503 13
                $this->createStatus = self::CREATE_STATUS_PARAMS_MISMATCH;
504 13
                return false;
505
            }
506
        }
507
508
        // match params in the pattern
509 77
        foreach ($this->_paramRules as $name => $rule) {
510 69
            if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
511 67
                $tr["<$name>"] = $this->encodeParams ? urlencode($params[$name]) : $params[$name];
512 67
                unset($params[$name]);
513 56
            } elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
514 55
                $this->createStatus = self::CREATE_STATUS_PARAMS_MISMATCH;
515 55
                return false;
516
            }
517
        }
518
519 77
        $url = $this->trimSlashes(strtr($this->_template, $tr));
520 77
        if ($this->host !== null) {
521 13
            $pos = strpos($url, '/', 8);
522 13
            if ($pos !== false) {
523 13
                $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos));
524
            }
525 65
        } elseif (strpos($url, '//') !== false) {
526 11
            $url = preg_replace('#/+#', '/', trim($url, '/'));
527
        }
528
529 77
        if ($url !== '') {
530 69
            $url .= ($this->suffix === null ? $manager->suffix : $this->suffix);
531
        }
532
533 77
        if (!empty($params) && ($query = http_build_query($params)) !== '') {
534 55
            $url .= '?' . $query;
535
        }
536
537 77
        $this->createStatus = self::CREATE_STATUS_SUCCESS;
538 77
        return $url;
539
    }
540
541
    /**
542
     * Returns status of the URL creation after the last [[createUrl()]] call.
543
     *
544
     * @return int|null Status of the URL creation after the last [[createUrl()]] call. `null` if rule does not provide
545
     * info about create status.
546
     * @see createStatus
547
     * @since 2.0.12
548
     */
549 77
    public function getCreateUrlStatus()
550
    {
551 77
        return $this->createStatus;
552
    }
553
554
    /**
555
     * Returns list of regex for matching parameter.
556
     * @return array parameter keys and regexp rules.
557
     *
558
     * @since 2.0.6
559
     */
560
    protected function getParamRules()
561
    {
562
        return $this->_paramRules;
563
    }
564
565
    /**
566
     * Iterates over [[placeholders]] and checks whether each placeholder exists as a key in $matches array.
567
     * When found - replaces this placeholder key with a appropriate name of matching parameter.
568
     * Used in [[parseRequest()]], [[createUrl()]].
569
     *
570
     * @param array $matches result of `preg_match()` call
571
     * @return array input array with replaced placeholder keys
572
     * @see placeholders
573
     * @since 2.0.7
574
     */
575 28
    protected function substitutePlaceholderNames(array $matches)
576
    {
577 28
        foreach ($this->placeholders as $placeholder => $name) {
578 27
            if (isset($matches[$placeholder])) {
579 27
                $matches[$name] = $matches[$placeholder];
580 27
                unset($matches[$placeholder]);
581
            }
582
        }
583
584 28
        return $matches;
585
    }
586
587
    /**
588
     * Trim slashes in passed string. If string begins with '//', two slashes are left as is
589
     * in the beginning of a string.
590
     *
591
     * @param string $string
592
     * @return string
593
     */
594 101
    private function trimSlashes($string)
595
    {
596 101
        if (strncmp($string, '//', 2) === 0) {
597 16
            return '//' . trim($string, '/');
598
        }
599
600 101
        return trim($string, '/');
601
    }
602
}
603