Passed
Push — refactor ( 885968...d87829 )
by Florian
02:43
created

Route   F

Complexity

Total Complexity 109

Size/Duplication

Total Lines 846
Duplicated Lines 0 %

Test Coverage

Coverage 89.09%

Importance

Changes 16
Bugs 0 Features 1
Metric Value
wmc 109
eloc 259
c 16
b 0
f 1
dl 0
loc 846
ccs 147
cts 165
cp 0.8909
rs 2

37 Methods

Rating   Name   Duplication   Size   Complexity  
A defaultConfig() 0 23 1
A setMiddleware() 0 5 1
A __construct() 0 16 1
A setNamespace() 0 5 1
A handler() 0 3 1
A setHandler() 0 9 4
B _buildVariables() 0 32 9
A scope() 0 3 1
A name() 0 3 1
A setPersistentParams() 0 5 1
A regex() 0 8 2
A params() 0 3 1
C _link() 0 49 16
B match() 0 26 11
A host() 0 3 1
A middleware() 0 20 5
A variables() 0 8 2
B setHost() 0 26 9
A setName() 0 5 1
A setAttribute() 0 5 1
A setScope() 0 5 1
A setParams() 0 5 1
A allow() 0 15 4
A setMethods() 0 15 5
A prefix() 0 3 1
A token() 0 11 2
A dispatch() 0 15 1
A namespace() 0 3 1
C link() 0 41 10
A setPattern() 0 13 3
A persistentParams() 0 3 1
A compile() 0 6 1
A apply() 0 7 2
A pattern() 0 3 1
A methods() 0 3 1
A setPrefix() 0 8 2
A attribute() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Route 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 Route, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Lead\Router;
6
7
use Closure;
8
use Generator;
9
use InvalidArgumentException;
10
use Lead\Router\Exception\RouterException;
11
use RuntimeException;
12
13
/**
14
 * The Route class.
15
 */
16
class Route implements RouteInterface
17
{
18
    /**
19
     * Class dependencies.
20
     *
21
     * @var array
22
     */
23
    protected $classes = [];
24
25
    /**
26
     * Route's name.
27
     *
28
     * @var string
29
     */
30
    public $name = '';
31
32
    /**
33
     * Named parameter.
34
     *
35
     * @var array
36
     */
37
    public $params = [];
38
39
    /**
40
     * List of parameters that should persist during dispatching.
41
     *
42
     * @var array
43
     */
44
    public $persist = [];
45
46
    /**
47
     * The attached namespace.
48
     *
49
     * @var string
50
     */
51
    public $namespace = '';
52
53
    /**
54
     * The attached request.
55
     *
56
     * @var mixed
57
     */
58
    public $request = null;
59
60
    /**
61
     * The attached response.
62
     *
63
     * @var mixed
64
     */
65
    public $response = null;
66
67
    /**
68
     * The dispatched instance (custom).
69
     *
70
     * @var object
71
     */
72
    public $dispatched = null;
73
74
    /**
75
     * The route scope.
76
     *
77
     * @var \Lead\Router\ScopeInterface|null
78
     */
79
    protected $scope = null;
80
81
    /**
82
     * The route's host.
83
     *
84
     * @var \Lead\Router\HostInterface|null
85
     */
86
    protected $host = null;
87
88
    /**
89
     * Route's allowed methods.
90
     *
91
     * @var array
92
     */
93
    protected $methods = [];
94
95
    /**
96
     * Route's prefix.
97
     *
98
     * @var string
99
     */
100
    protected $prefix = '';
101
102
    /**
103
     * Route's pattern.
104
     *
105
     * @var string
106
     */
107
    protected $pattern = '';
108
109
    /**
110
     * The tokens structure extracted from route's pattern.
111
     *
112
     * @see Parser::tokenize()
113
     * @var array|null
114
     */
115
    protected $token = null;
116
117
    /**
118
     * The route's regular expression pattern.
119
     *
120
     * @see Parser::compile()
121
     * @var string|null
122
     */
123
    protected $regex = null;
124
125
    /**
126
     * The route's variables.
127
     *
128
     * @see Parser::compile()
129
     * @var array|null
130
     */
131
    protected $variables = null;
132
133
    /**
134
     * The route's handler to execute when a request match.
135
     *
136
     * @var \Closure|null
137
     */
138
    protected $handler = null;
139
140
    /**
141
     * The middlewares.
142
     *
143
     * @var array
144
     */
145
    protected $middleware = [];
146
147
    /**
148
     * Attributes
149
     *
150
     * @var array
151
     */
152
    protected $attributes = [];
153
154
    /**
155
     * Constructs a route
156
     *
157
     * @param array $config The config array.
158
     */
159
    public function __construct(array $config = [])
160
    {
161 66
        $config = $this->defaultConfig($config);
162
163 66
        $this->classes = $config['classes'];
164 66
        $this->setNamespace($config['namespace']);
165 66
        $this->setName($config['name']);
166 66
        $this->setParams($config['params']);
167 66
        $this->setPersistentParams($config['persist']);
168 66
        $this->setHandler($config['handler']);
169 66
        $this->setPrefix($config['prefix']);
170 66
        $this->setHost($config['host'], $config['scheme']);
171 66
        $this->setMethods($config['methods']);
172 66
        $this->setScope($config['scope']);
173 66
        $this->setPattern($config['pattern']);
174 66
        $this->setMiddleware((array)$config['middleware']);
175
    }
176
177
    /**
178
     * Sets the middlewares
179
     *
180
     * @param array $middleware Middlewares
181
     * @return \Lead\Router\Route
182
     */
183
    public function setMiddleware(array $middleware)
184
    {
185 66
        $this->middleware = (array)$middleware;
186
187 66
        return $this;
188
    }
189
190
    /**
191
     * Gets the default config
192
     *
193
     * @param array $config Values to merge
194
     * @return array
195
     */
196
    protected function defaultConfig($config = []): array
197
    {
198
        $defaults = [
199
            'scheme' => '*',
200
            'host' => null,
201
            'methods' => '*',
202
            'prefix' => '',
203
            'pattern' => '',
204
            'name' => '',
205
            'namespace' => '',
206
            'handler' => null,
207
            'params' => [],
208
            'persist' => [],
209
            'scope' => null,
210
            'middleware' => [],
211
            'classes' => [
212
                'parser' => 'Lead\Router\Parser',
213
                'host' => 'Lead\Router\Host'
214
            ]
215 66
        ];
216 66
        $config += $defaults;
217
218 66
        return $config;
219
    }
220
221
    /**
222
     * Sets a route attribute
223
     *
224
     * This method can be used to set arbitrary date attributes to a route.
225
     *
226
     * @param string $name Name
227
     * @param mixed $value Value
228
     * @return \Lead\Router\RouteInterface
229
     */
230
    public function setAttribute($name, $value): RouteInterface
231
    {
232
        $this->attributes[$name] = $value;
233
234
        return $this;
235
    }
236
237
    /**
238
     * Gets a route attribute
239
     *
240
     * @param string $name Attribute name
241
     * @return mixed
242
     */
243
    public function attribute(string $name)
244
    {
245
        if (isset($this->attributes[$name])) {
246
            return $this->attributes[$name];
247
        }
248
249
        return null;
250
    }
251
252
    /**
253
     * Sets namespace
254
     *
255
     * @param string $namespace Namespace
256
     * @return self
257
     */
258
    public function setNamespace(string $namespace): self
259
    {
260 66
        $this->namespace = $namespace;
261
262 66
        return $this;
263
    }
264
265
    /**
266
     * Get namespace
267
     *
268
     * @return string
269
     */
270
    public function namespace(): string
271
    {
272
        return $this->namespace;
273
    }
274
275
    /**
276
     * Sets params
277
     *
278
     * @param array $params Params
279
     * @return self
280
     */
281
    public function setParams(array $params): self
282
    {
283 66
        $this->params = $params;
284
285 66
        return $this;
286
    }
287
288
    /**
289
     * Get parameters
290
     *
291
     * @return array
292
     */
293
    public function params(): array
294
    {
295 2
        return $this->params;
296
    }
297
298
    /**
299
     * Sets persistent params
300
     *
301
     * @param array $params Params
302
     * @return self
303
     */
304
    public function setPersistentParams(array $params): self
305
    {
306 66
        $this->persist = $params;
307
308 66
        return $this;
309
    }
310
311
    /**
312
     * Get persistent parameters
313
     *
314
     * @return array
315
     */
316
    public function persistentParams(): array
317
    {
318 35
        return $this->persist;
319
    }
320
321
    /**
322
     * Gets the routes name
323
     *
324
     * @return string
325
     */
326
    public function name(): string
327
    {
328
        return $this->name;
329
    }
330
331
    /**
332
     * Sets the routes name
333
     *
334
     * @param string $name Name
335
     * @return self
336
     */
337
    public function setName(string $name): RouteInterface
338
    {
339 66
        $this->name = $name;
340
341 66
        return $this;
342
    }
343
344
    /**
345
     * Gets the prefix
346
     *
347
     * @return string
348
     */
349
    public function prefix(): string
350
    {
351
        return $this->prefix;
352
    }
353
354
    /**
355
     * Sets the routes prefix
356
     *
357
     * @param string $prefix Prefix
358
     * @return self
359
     */
360
    public function setPrefix(string $prefix): RouteInterface
361
    {
362 66
        $this->prefix = trim($prefix, '/');
363
        if ($this->prefix) {
364 15
            $this->prefix = '/' . $this->prefix;
365
        }
366
367 66
        return $this;
368
    }
369
370
    /**
371
     * Gets the host
372
     *
373
     * @return mixed
374
     */
375
    public function host(): ?HostInterface
376
    {
377 45
        return $this->host;
378
    }
379
380
    /**
381
     * Sets the route host.
382
     *
383
     * @param string|\Lead\Router\HostInterface $host The host instance to set or none to get the set one
384
     * @param string $scheme HTTP Scheme
385
     * @return $this The current host on get or `$this` on set
386
     */
387
    public function setHost($host = null, string $scheme = '*'): RouteInterface
388
    {
389
        if (!is_string($host) && $host instanceof Host && $host !== null) {
390
            throw new InvalidArgumentException();
391
        }
392
393
        if ($host instanceof HostInterface || $host === null) {
394 19
            $this->host = $host;
395
396 19
            return $this;
397
        }
398
399
        if ($host !== '*' || $scheme !== '*') {
400 11
            $class = $this->classes['host'];
401 11
            $host = new $class(['scheme' => $scheme, 'pattern' => $host]);
402
            if (!$host instanceof HostInterface) {
403
                throw new RuntimeException('Must be an instance of HostInterface');
404
            }
405 11
            $this->host = $host;
406
407 11
            return $this;
408
        }
409
410 36
        $this->host = null;
411
412 36
        return $this;
413
    }
414
415
    /**
416
     * Gets allowed methods
417
     *
418
     * @return array
419
     */
420
    public function methods(): array
421
    {
422 9
        return array_keys($this->methods);
423
    }
424
425
    /**
426
     * Sets methods
427
     *
428
     * @param  string|array $methods
429
     * @return self
430
     */
431
    public function setMethods($methods): self
432
    {
433 66
        $methods = $methods ? (array)$methods : [];
434 66
        $methods = array_map('strtoupper', $methods);
435 66
        $methods = array_fill_keys($methods, true);
436
437
        foreach ($methods as $method) {
438
            if (is_string($method) && !in_array($method, self::VALID_METHODS, true)) {
439 64
                throw new InvalidArgumentException(sprintf('`%s` is not an allowed HTTP method', $method));
440
            }
441
        }
442
443 66
        $this->methods = $methods;
444
445 66
        return $this;
446
    }
447
448
    /**
449
     * Allows additional methods.
450
     *
451
     * @param  string|array $methods The methods to allow.
452
     * @return self
453
     */
454
    public function allow($methods = [])
455
    {
456 47
        $methods = $methods ? (array)$methods : [];
457 47
        $methods = array_map('strtoupper', $methods);
458 47
        $methods = array_fill_keys($methods, true) + $this->methods;
459
460
        foreach ($methods as $method) {
461
            if (!in_array($method, self::VALID_METHODS)) {
462 47
                throw new InvalidArgumentException(sprintf('`%s` is not an allowed HTTP method', $method));
463
            }
464
        }
465
466 47
        $this->methods = $methods;
467
468 47
        return $this;
469
    }
470
471
    /**
472
     * Gets the routes Scope
473
     *
474
     * @return \Lead\Router\Scope
475
     */
476
    public function scope(): ?ScopeInterface
477
    {
478 5
        return $this->scope;
479
    }
480
481
    /**
482
     * Sets a routes scope
483
     *
484
     * @param  \Lead\Router\Scope|null $scope Scope
485
     * @return $this;
486
     */
487
    public function setScope(?Scope $scope): RouteInterface
488
    {
489 66
        $this->scope = $scope;
490
491 66
        return $this;
492
    }
493
494
    /**
495
     * Gets the routes pattern
496
     *
497
     * @return string
498
     */
499
    public function pattern(): string
500
    {
501 2
        return $this->pattern;
502
    }
503
504
    /**
505
     * Sets the routes pattern
506
     *
507
     * @return $this
508
     */
509
    public function setPattern(string $pattern): RouteInterface
510
    {
511 66
        $this->token = null;
512 66
        $this->regex = null;
513 66
        $this->variables = null;
514
515
        if (!$pattern || $pattern[0] !== '[') {
516 61
            $pattern = '/' . trim($pattern, '/');
517
        }
518
519 66
        $this->pattern = $this->prefix . $pattern;
520
521 66
        return $this;
522
    }
523
524
    /**
525
     * Returns the route's token structures.
526
     *
527
     * @return array A collection route's token structure.
528
     */
529
    public function token(): array
530
    {
531
        if ($this->token === null) {
532 56
            $parser = $this->classes['parser'];
533 56
            $this->token = [];
534 56
            $this->regex = null;
535 56
            $this->variables = null;
536 56
            $this->token = $parser::tokenize($this->pattern, '/');
537
        }
538
539 56
        return $this->token;
540
    }
541
542
    /**
543
     * Gets the route's regular expression pattern.
544
     *
545
     * @return string the route's regular expression pattern.
546
     */
547
    public function regex(): string
548
    {
549
        if ($this->regex !== null) {
550 15
            return $this->regex;
551
        }
552 40
        $this->compile();
553
554 40
        return $this->regex;
555
    }
556
557
    /**
558
     * Gets the route's variables and their associated pattern in case of array variables.
559
     *
560
     * @return array The route's variables and their associated pattern.
561
     */
562
    public function variables(): array
563
    {
564
        if ($this->variables !== null) {
565 36
            return $this->variables;
566
        }
567 1
        $this->compile();
568
569 1
        return $this->variables;
570
    }
571
572
    /**
573
     * Compiles the route's patten.
574
     */
575
    protected function compile(): void
576
    {
577 41
        $parser = $this->classes['parser'];
578 41
        $rule = $parser::compile($this->token());
579 41
        $this->regex = $rule[0];
580 41
        $this->variables = $rule[1];
581
    }
582
583
    /**
584
     * Gets the routes handler
585
     *
586
     * @return mixed
587
     */
588
    public function handler()
589
    {
590 4
        return $this->handler;
591
    }
592
593
    /**
594
     * Gets/sets the route's handler.
595
     *
596
     * @param mixed $handler The route handler.
597
     * @return self
598
     */
599
    public function setHandler($handler): RouteInterface
600
    {
601
        if (!is_callable($handler) && !is_string($handler) && $handler !== null) {
602
            throw new InvalidArgumentException('Handler must be a callable, string or null');
603
        }
604
605 66
        $this->handler = $handler;
606
607 66
        return $this;
608
    }
609
610
    /**
611
     * Checks if the route instance matches a request.
612
     *
613
     * @param  array $request a request.
614
     * @return bool
615
     */
616
    public function match($request, &$variables = null, &$hostVariables = null): bool
617
    {
618 39
        $hostVariables = [];
619
620
        if (($host = $this->host()) && !$host->match($request, $hostVariables)) {
0 ignored issues
show
Bug introduced by
$hostVariables of type array is incompatible with the type string expected by parameter $hostVariables of Lead\Router\HostInterface::match(). ( Ignorable by Annotation )

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

620
        if (($host = $this->host()) && !$host->match($request, /** @scrutinizer ignore-type */ $hostVariables)) {
Loading history...
Bug introduced by
$request of type array is incompatible with the type string expected by parameter $request of Lead\Router\HostInterface::match(). ( Ignorable by Annotation )

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

620
        if (($host = $this->host()) && !$host->match(/** @scrutinizer ignore-type */ $request, $hostVariables)) {
Loading history...
621 3
            return false;
622
        }
623
624 38
        $path = isset($request['path']) ? $request['path'] : '';
625 38
        $method = isset($request['method']) ? $request['method'] : '*';
626
627
        if (!isset($this->methods['*']) && $method !== '*' && !isset($this->methods[$method])) {
628
            if ($method !== 'HEAD' && !isset($this->methods['GET'])) {
629
                return false;
630
            }
631
        }
632
633 38
        $path = '/' . trim($path, '/');
634
635
        if (!preg_match('~^' . $this->regex() . '$~', $path, $matches)) {
636 10
            return false;
637
        }
638 35
        $variables = $this->_buildVariables($matches);
639 35
        $this->params = $hostVariables + $variables;
640
641 35
        return true;
642
    }
643
644
    /**
645
     * Combines route's variables names with the regex matched route's values.
646
     *
647
     * @param  array $varNames The variable names array with their corresponding pattern segment when applicable.
648
     * @param  array $values   The matched values.
649
     * @return array           The route's variables.
650
     */
651
    protected function _buildVariables(array $values): array
652
    {
653 35
        $variables = [];
654 35
        $parser = $this->classes['parser'];
655
656 35
        $i = 1;
657
        foreach ($this->variables() as $name => $pattern) {
658
            if (!isset($values[$i])) {
659 8
                $variables[$name] = !$pattern ? null : [];
660 8
                continue;
661
            }
662
            if (!$pattern) {
663 17
                $variables[$name] = $values[$i] ?: null;
664
            } else {
665 2
                $token = $parser::tokenize($pattern, '/');
666 2
                $rule = $parser::compile($token);
667
                if (preg_match_all('~' . $rule[0] . '~', $values[$i], $parts)) {
668
                    foreach ($parts[1] as $value) {
669
                        if (strpos($value, '/') !== false) {
670 1
                            $variables[$name][] = explode('/', $value);
671
                        } else {
672 2
                            $variables[$name][] = $value;
673
                        }
674
                    }
675
                } else {
676 1
                    $variables[$name] = [];
677
                }
678
            }
679 18
            $i++;
680
        }
681
682 35
        return $variables;
683
    }
684
685
    /**
686
     * Dispatches the route.
687
     *
688
     * @param  mixed $response The outgoing response.
689
     * @return mixed The handler return value.
690
     */
691
    public function dispatch($response = null)
692
    {
693 4
        $this->response = $response;
694 4
        $request = $this->request;
695
696 4
        $generator = $this->middleware();
697
698
        $next = function () use ($request, $response, $generator, &$next) {
699 4
            $handler = $generator->current();
700 4
            $generator->next();
701
702 4
            return $handler($request, $response, $next);
703
        };
704
705 4
        return $next();
706
    }
707
708
    /**
709
     * Middleware generator.
710
     *
711
     * @return \Generator
712
     */
713
    public function middleware(): Generator
714
    {
715
        foreach ($this->middleware as $middleware) {
716 1
            yield $middleware;
717
        }
718
719 4
        $scope = $this->scope();
720
        if ($scope !== null) {
721
            foreach ($scope->middleware() as $middleware) {
722 2
                yield $middleware;
723
            }
724
        }
725
726
        yield function () {
727 4
            $handler = $this->handler();
728
            if ($handler === null) {
729
                return null;
730
            }
731
732 4
            return $handler($this, $this->response);
733
        };
734
    }
735
736
    /**
737
     * Adds a middleware to the list of middleware.
738
     *
739
     * @param object|Closure A callable middleware.
0 ignored issues
show
Bug introduced by
The type Lead\Router\A was not found. Did you mean A? If so, make sure to prefix the type with \.
Loading history...
740
     * @return $this
741
     */
742
    public function apply($middleware)
743
    {
744
        foreach (func_get_args() as $mw) {
745 1
            array_unshift($this->middleware, $mw);
746
        }
747
748 1
        return $this;
749
    }
750
751
    /**
752
     * Returns the route's link.
753
     *
754
     * @param  array $params  The route parameters.
755
     * @param  array $options Options for generating the proper prefix. Accepted values are:
756
     *                        - `'absolute'` _boolean_: `true` or `false`. - `'scheme'`
757
     *                        _string_ : The scheme. - `'host'`     _string_ : The host
758
     *                        name. - `'basePath'` _string_ : The base path. - `'query'`
759
     *                        _string_ : The query string. - `'fragment'` _string_ : The
760
     *                        fragment string.
761
     * @return string          The link.
762
     */
763
    public function link(array $params = [], array $options = []): string
764
    {
765
        $defaults = [
766
            'absolute' => false,
767
            'basePath' => '',
768
            'query' => '',
769
            'fragment' => ''
770 17
        ];
771
772
        $options = array_filter(
773
            $options,
774
            function ($value) {
775 6
                return $value !== '*';
776
            }
777
        );
778 17
        $options += $defaults;
779
780 17
        $params = $params + $this->params;
781
782 17
        $link = $this->_link($this->token(), $params);
783
784 13
        $basePath = trim($options['basePath'], '/');
785
        if ($basePath) {
786 3
            $basePath = '/' . $basePath;
787
        }
788 13
        $link = isset($link) ? ltrim($link, '/') : '';
789 13
        $link = $basePath . ($link ? '/' . $link : $link);
790 13
        $query = $options['query'] ? '?' . $options['query'] : '';
791 13
        $fragment = $options['fragment'] ? '#' . $options['fragment'] : '';
792
793
        if ($options['absolute']) {
794
            if ($this->host !== null) {
795 3
                $link = $this->host->link($params, $options) . "{$link}";
796
            } else {
797
                $scheme = !empty($options['scheme']) ? $options['scheme'] . '://' : '//';
798
                $host = isset($options['host']) ? $options['host'] : 'localhost';
799
                $link = "{$scheme}{$host}{$link}";
800
            }
801
        }
802
803 13
        return $link . $query . $fragment;
804
    }
805
806
    /**
807
     * Helper for `Route::link()`.
808
     *
809
     * @param  array $token  The token structure array.
810
     * @param  array $params The route parameters.
811
     * @return string The URL path representation of the token structure array.
812
     */
813
    protected function _link(array $token, array $params): string
814
    {
815 17
        $link = '';
816
        foreach ($token['tokens'] as $child) {
817
            if (is_string($child)) {
818 17
                $link .= $child;
819 17
                continue;
820
            }
821
            if (isset($child['tokens'])) {
822
                if ($child['repeat']) {
823 5
                    $name = $child['repeat'];
824 5
                    $values = isset($params[$name]) && $params[$name] !== null ? (array)$params[$name] : [];
825
                    if (!$values && !$child['optional']) {
826 1
                        throw new RouterException("Missing parameters `'{$name}'` for route: `'{$this->name}#{$this->pattern}'`.");
827
                    }
828
                    foreach ($values as $value) {
829 4
                        $link .= $this->_link($child, [$name => $value] + $params);
830
                    }
831
                } else {
832 5
                    $link .= $this->_link($child, $params);
833
                }
834 7
                continue;
835
            }
836
837
            if (!isset($params[$child['name']])) {
838
                if (!$token['optional']) {
839
                    throw new RouterException("Missing parameters `'{$child['name']}'` for route: `'{$this->name}#{$this->pattern}'`.");
840
                }
841
842 3
                return '';
843
            }
844
845
            if ($data = $params[$child['name']]) {
846 16
                $parts = is_array($data) ? $data : [$data];
847
            } else {
848
                $parts = [];
849
            }
850
            foreach ($parts as $key => $value) {
851 16
                $parts[$key] = rawurlencode((string)$value);
852
            }
853 16
            $value = join('/', $parts);
854
855
            if (!preg_match('~^' . $child['pattern'] . '$~', $value)) {
856 3
                throw new RouterException("Expected `'" . $child['name'] . "'` to match `'" . $child['pattern'] . "'`, but received `'" . $value . "'`.");
857
            }
858 14
            $link .= $value;
859
        }
860
861 14
        return $link;
862
    }
863
}
864