Completed
Push — master ( 047943...ada7af )
by Lee
05:25
created

Matcher::getMatchRegexPattern()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.0327

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 16
ccs 11
cts 13
cp 0.8462
rs 9.4285
cc 3
eloc 11
nc 4
nop 1
crap 3.0327
1
<?php
2
/**
3
 * This file is part of the Drest package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @author Lee Davis
9
 * @copyright Copyright (c) Lee Davis <@leedavis81>
10
 * @link https://github.com/leedavis81/drest/blob/master/LICENSE
11
 * @license http://opensource.org/licenses/MIT The MIT X License (MIT)
12
 */
13
namespace Drest\Route;
14
15
use Drest\Mapping\RouteMetaData;
16
use DrestCommon\Request\Request;
17
18
19
/**
20
 * Class Matcher takes a request object and finds matching routes
21
 * @package Drest\Route
22
 */
23
class Matcher
24
{
25
26
    /**
27
     * The route meta data this matcher should operate on
28
     * @var RouteMetaData $routeMetaData
29
     */
30
    protected $routeMetaData;
31
32
    /**
33
     * Key-value array of URL parameter names
34
     * @var array $param_names
35
     */
36
    protected $param_names = [];
37
38
    /**
39
     * Key-value array of URL parameters with + at the end
40
     * @var array $param_names_path
41
     */
42
    protected $param_names_path = [];
43
44
    /**
45
     * Key-value array of URL parameters populated after a match has been successful
46
     * - or directly by using available setter
47
     * @var array $route_params
48
     */
49
    protected $route_params;
50
51
    /**
52
     * An index array of URL parameters that exist but didn't match a route pattern parameter
53
     * Eg: pattern: /user/:id+  with url: /user/1/some/additional/params.
54
     * The value id => 1 will go into $route_params
55
     * All the rest will go in here.
56
     * @var array $unmapped_route_params
57
     */
58
    protected $unmapped_route_params;
59
60
    /**
61
     * Route meta data to test a match on
62
     * @param RouteMetaData|null $routeMetaData
63
     */
64 30
    public function __construct(RouteMetaData $routeMetaData = null)
65
    {
66 30
        if (!is_null($routeMetaData))
67 30
        {
68 30
            $this->setRouteMetaData($routeMetaData);
69 30
        }
70 30
    }
71
72
    /**
73
     * Set the route meta data
74
     * @param RouteMetaData $routeMetaData
75
     */
76 30
    public function setRouteMetaData(RouteMetaData $routeMetaData)
77
    {
78 30
        $this->routeMetaData = $routeMetaData;
79 30
    }
80
81
    /**
82
     * Does this request match the route pattern
83
     * @param  Request $request
84
     * @param  boolean $matchVerb - Whether you want to match the route using the request HTTP verb
85
     *                            - useful for OPTIONS requests to provide route info
86
     * @param  string  $basePath  - add a base path to the route pattern
87
     * @return boolean $result
88
     */
89 30
    public function matches(Request $request, $matchVerb = true, $basePath = null)
90
    {
91
        // If we're matching the verb and we've defined them, ensure the method used is in our list of registered verbs
92 30
        if ($matchVerb &&
93 30
            $this->routeMetaData->usesHttpVerbs() &&
94 30
            !$this->methodIsInOurListOfAllowedVerbs($request->getHttpMethod())) {
95 28
            return false;
96
        }
97
98 29
        $patternAsRegex = $this->getMatchRegexPattern($basePath);
99
100
        //Cache URL params' names and values if this route matches the current HTTP request
101 29
        if (!preg_match('#^' . $patternAsRegex . '$#', $request->getPath(), $paramValues)) {
102 26
            return false;
103
        }
104
105
        // Process the param names and save them on the route params
106 27
        $this->processRouteParams($paramValues);
107
108
        // Check the route conditions
109 27
        if (!$this->routeConditionsAreValid())
110 27
        {
111
            return false;
112
        }
113
114 27
        return true;
115
    }
116
117
    /**
118
     * Get the determined route parameters
119
     * @return array
120
     */
121 27
    public function getRouteParams()
122
    {
123 27
        return $this->route_params;
124
    }
125
126
127
    /**
128
     * Get the param names
129
     * @return array
130
     */
131 27
    public function getParamNames()
132
    {
133 27
        return $this->param_names;
134
    }
135
136
    /**
137
     * Get the params names path
138
     * @return array
139
     */
140 27
    public function getParamNamesPath()
141
    {
142 27
        return $this->param_names_path;
143
    }
144
145
    /**
146
     * Get any unmapped route parameters
147
     * @return array $params
148
     */
149 27
    public function getUnmappedRouteParams()
150
    {
151 27
        return $this->unmapped_route_params;
152
    }
153
154
    /**
155
     * Get the regex pattern to match the request path
156
     * @param $basePath
157
     * @return string
158
     */
159 29
    protected function getMatchRegexPattern($basePath)
160
    {
161
        // Convert URL params into regex patterns, construct a regex for this route, init params
162 29
        $routePattern = (is_null($basePath))
163 29
            ? (string) $this->routeMetaData->getRoutePattern()
164 29
            : '/' . $basePath . '/' . ltrim((string) $this->routeMetaData->getRoutePattern(), '/');
165 29
        $patternAsRegex = preg_replace_callback(
166 29
            '#:([\w]+)\+?#',
167 29
            [$this, 'matchesCallback'],
168 29
            str_replace(')', ')?', $routePattern)
169 29
        );
170 29
        if (substr($this->routeMetaData->getRoutePattern(), -1) === '/') {
171
            $patternAsRegex .= '?';
172
        }
173 29
        return $patternAsRegex;
174
    }
175
176
177
    /**
178
     * Convert a URL parameter (e.g. ":id", ":id+") into a regular expression
179
     * @param array - url parameters
180
     * @return string - Regular expression for URL parameter
181
     */
182 28
    protected function matchesCallback($m)
183
    {
184 28
        $this->param_names[] = $m[1];
185
186 28
        if (substr($m[0], -1) === '+') {
187
            $this->param_names_path[$m[1]] = 1;
188
189
            return '(?P<' . $m[1] . '>.+)';
190
        }
191
192 28
        return '(?P<' . $m[1] . '>[^/]+)';
193
    }
194
195
    /**
196
     * Process the route names and add them as parameters
197
     * @param array $paramValues
198
     */
199 27
    protected function processRouteParams(array $paramValues)
200
    {
201 27
        foreach ($this->param_names as $name) {
202 11
            if (isset($paramValues[$name])) {
203 11
                if (isset($this->param_names_path[$name])) {
204
                    $parts = explode('/', urldecode($paramValues[$name]));
205
                    $this->route_params[$name] = array_shift($parts);
206
                    $this->unmapped_route_params = $parts;
207
                } else {
208 11
                    $this->route_params[$name] = urldecode($paramValues[$name]);
209
                }
210 11
            }
211 27
        }
212 27
    }
213
214
    /**
215
     * Are the given route conditions matching
216
     * @return bool
217
     */
218 27
    protected function routeConditionsAreValid()
219
    {
220 27
        foreach ($this->routeMetaData->getRouteConditions() as $key => $condition) {
0 ignored issues
show
Bug introduced by
The expression $this->routeMetaData->getRouteConditions() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
221 3
            if (!preg_match('/^' . $condition . '$/', $this->route_params[$key])) {
222
                $this->param_names_path = $this->route_params = $this->unmapped_route_params = [];
223
                return false;
224
            }
225 27
        }
226 27
        return true;
227
    }
228
229
    /**
230
     * Ensure our method is in out list of allowed verbs
231
     * @param $httpMethod
232
     * @return bool
233
     */
234 28
    protected function methodIsInOurListOfAllowedVerbs($httpMethod)
235
    {
236 28
        if (!in_array($httpMethod, $this->routeMetaData->getVerbs())) {
237 28
            return false;
238
        }
239 25
        return true;
240
    }
241
}