Router   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 364
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 0
dl 0
loc 364
ccs 0
cts 150
cp 0
rs 8.3206
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
B setAsProperties() 0 17 5
B map() 0 18 6
C routeToParamsMatcher() 0 38 13
A setAction() 0 9 3
A getAction() 0 13 4
A getActions() 0 4 1
A setParams() 0 4 1
A getParams() 0 4 1
A setParam() 0 4 1
A getParam() 0 10 2
A hasParam() 0 4 1
A setController() 0 4 1
A getController() 0 4 1
A setMethod() 0 4 1
A getMethod() 0 4 1
A getName() 0 4 2
A getPath() 0 4 2
A setName() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Router 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 *
5
 * This file is part of the Apix Project.
6
 *
7
 * (c) Franck Cassedanne <franck at ouarz.net>
8
 *
9
 * @license     http://opensource.org/licenses/BSD-3-Clause  New BSD License
10
 *
11
 */
12
13
namespace Apix;
14
15
/**
16
 * Apix Router class
17
 *
18
 * @example     $rules = array('/resource/:keyname/:id' => array('controller'=>'someController', 'action'=>'someAction'));
19
 *              $router = Router($rules);
20
 *              $router->init( $_SERVER['REQUEST_URI'] ); // execute router
21
 */
22
class Router
23
{
24
    /**
25
     * Holds the controller string.
26
     * @var	string
27
     */
28
    protected $controller = null;
29
30
    /**
31
     * Holds the current method.
32
     * @var string
33
     */
34
    protected $method = null;
35
36
    /**
37
     * Holds the current action.
38
     * @var	string
39
     */
40
    protected $action = null;
41
42
    /**
43
     * Holds the current action.
44
     * @var string
45
     */
46
    protected $name = null;
47
48
    /**
49
     * Holds all the actions (HTTP methods to CRUD verbs).
50
     * TODO: refactor this!!
51
     * @var array
52
     */
53
    public static $actions = array(
54
        'POST'      => 'onCreate',
55
        'GET'       => 'onRead',
56
        'PUT'       => 'onUpdate',
57
        'DELETE'    => 'onDelete',
58
        'PATCH'     => 'onModify',
59
        'OPTIONS'   => 'onHelp',
60
        'HEAD'      => 'onTest',
61
        'TRACE'     => 'onTrace'
62
    );
63
64
    /**
65
     * Holds an array of router params
66
     * @var	array
67
     */
68
    protected $params = array();
69
70
    private $_rules = array();
71
    private $_defaults = array();
72
73
    /**
74
     * The constructor, set the default routing rules.
75
     *
76
     * @param  array                     $rules
77
     * @param  array                     $defaults
78
     * @throws \InvalidArgumentException
79
     */
80
    public function __construct(array $rules=array(), array $defaults=array())
81
    {
82
        foreach ($rules as $key => $rule) {
83
            if ( is_int($key) ) {
84
                throw new \InvalidArgumentException(
85
                        'Invalid rules array specified (not associative)'
86
                    );
87
            }
88
            $this->_rules[$key] = $rule;
89
        }
90
91
        // merges defaults with required props
92
        $this->_defaults = $defaults+array('controller'=>null, 'action'=>null);
93
94
        // set default properties
95
        foreach ($this->_defaults as $prop => $value) {
96
            $this->$prop = $value;
97
        }
98
    }
99
100
    /**
101
     * Sets the public properties e.g. controller, action, method and params
102
     * in order as per the following:
103
     *      - router rules,
104
     *      - router params,
105
     *      - then router defauls.
106
     *
107
     * @param  string $route
0 ignored issues
show
Bug introduced by
There is no parameter named $route. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
108
     * @param  array  $rules
109
     * @param  array  $params
110
     * @return void
111
     */
112
    public function setAsProperties(array $rules, array $params)
113
    {
114
        foreach (array_keys($this->_defaults) as $k) {
115
            $value = isset($rules[$k]) ? $rules[$k]    // rules
116
                : (isset($params[$k]) ? $params[$k]    // params
117
                : $this->_defaults[$k]);            // defaults
118
119
            if (property_exists($this, $k)) {
120
                $this->$k = $value;
121
            }
122
            // else {
123
            //     echo 'TEMP';
124
            //     $this->temp->$k = $value;
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
125
            // }
126
        }
127
        $this->params = $params;
128
    }
129
130
    /**
131
     * Maps an URI against the routing table.
132
     *
133
     * @param  string $uri
134
     * @param  array  $params Additional params to merge with the current set (optional)
135
     * @return void
136
     */
137
    public function map($uri, array $params=null)
138
    {
139
        if (!is_null($params)) {
140
            // merge with existing, precedence!
141
            $this->setParams( $this->params+$params );
142
        }
143
144
        foreach ($this->_rules as $route => $rules) {
145
            $params = $this->routeToParamsMatcher($route, $uri);
146
            if ($uri == $route || $params) {
147
                $this->name = $route;
0 ignored issues
show
Documentation Bug introduced by
It seems like $route can also be of type integer. However, the property $name 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...
148
149
                if(is_object($rules)) $rules = $rules->toArray();
150
151
                return $this->setAsProperties($rules, $params);
0 ignored issues
show
Security Bug introduced by
It seems like $params defined by $this->routeToParamsMatcher($route, $uri) on line 145 can also be of type false; however, Apix\Router::setAsProperties() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
152
            }
153
        }
154
    }
155
156
    /**
157
     * Tries to match a route to an URI params (version with REGEX).
158
     *
159
     * @param  string $route
160
     * @param  string $uri
161
     * @return array
162
     */
163
    public function routeToParamsMatcher($route, $uri)
164
    {
165
        $bits = explode('/', $route);
166
        $paths = explode('/', $uri);
167
        $result = array();
168
169
        // match 1st URI element not a param
170
        if (count($paths) == 2 && count($bits) >2 ) {
171
            if ($paths[1] == $bits[1]) {
172
                return array($paths[1]);
173
            }
174
        }
175
176
        // params
177
        foreach ($bits as $key => $value) {
178
179
            if (
180
                preg_match('/^:[\w]+$/', $value)) {
181
                if (isset($paths[$key])) {
182
                    $value = substr($value, 1); // rm ':'
183
                    $result[$value] = $paths[$key];
184
                }
185
            //} elseif (!isset($paths[$key]) || strcmp($value, $paths[$key]) != 0) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
71% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
186
            } elseif ( //Regex based!
187
                isset($paths[$key])
188
                && preg_match('/^:(?P<key>[\w]+)(?:<(?P<regex>.+)>)?/', $value, $m)
189
                && isset($m['regex'])
190
                && preg_match('/^'.$m['regex'].'$/', $paths[$key], $n)
191
            ) {
192
                if(count($n)>1) array_shift($n);
193
                $result[$m['key']] = implode('', $n);
194
            } elseif (strcmp($value, $paths[$key]) != 0) {
195
                return false;
196
            }
197
        }
198
199
        return $result;
200
    }
201
202
    /**
203
     * Sets the current action from a specified method
204
     * or use the method in the current scope.
205
     *
206
     * @param  string $method The method to set the action (optional)
207
     * @return void
208
     */
209
    public function setAction($method=null)
210
    {
211
        if (!is_null($method)) {
212
            $this->setMethod($method);
213
        }
214
        $this->action = isset(self::$actions[$this->method])
215
            ? self::$actions[$this->method]
216
            : null;
217
    }
218
219
    /**
220
     * Returns the current action, or as specified.
221
     *
222
     * @param  string $method A method key (optional)
223
     * @return string
224
     */
225
    public function getAction($method=null)
226
    {
227
        if (isset($method)) {
228
            return isset(self::$actions[$method])
229
                   ? self::$actions[$method]
230
                   : null;
231
        }
232
        if (null === $this->action) {
233
            $this->setAction();
234
        }
235
236
        return $this->action;
237
    }
238
239
    /**
240
     * Returns all the actions.
241
     *
242
     * @return array
243
     */
244
    public function getActions()
245
    {
246
        return self::$actions;
247
    }
248
249
    /**
250
     * Sets the router's params.
251
     *
252
     * @param  array $params
253
     * @return void
254
     */
255
    public function setParams(array $params)
256
    {
257
        $this->params = $params;
258
    }
259
260
    /**
261
     * Returns all the router's params.
262
     *
263
     * @return array
264
     */
265
    public function getParams()
266
    {
267
        return $this->params;
268
    }
269
270
    /**
271
     * Sets the specified router param.
272
     *
273
     * @param  string $key
274
     * @param  string $value
275
     * @return void
276
     */
277
    public function setParam($key, $value)
278
    {
279
        $this->params[$key] = $value;
280
    }
281
282
    /**
283
     * Returns the specified router param.
284
     *
285
     * @param  string                    $key
286
     * @return array
287
     * @throws \InvalidArgumentException
288
     */
289
    public function getParam($key)
290
    {
291
        if (isset($this->params[$key])) {
292
            return $this->params[$key];
293
        }
294
295
        throw new \InvalidArgumentException(
296
                    sprintf('Invalid parameter "%s" requested.', $key)
297
                );
298
    }
299
300
    /**
301
     * Checks a specified router param exists.
302
     *
303
     * @param  string $key A key to check
304
     * @return bolean
305
     */
306
    public function hasParam($key)
307
    {
308
        return isset($this->params[$key]);
309
    }
310
311
    /**
312
     * Sets the controller.
313
     *
314
     * @param  string $controller
315
     * @return void
316
     */
317
    public function setController($controller)
318
    {
319
        $this->controller = $controller;
320
    }
321
322
    /**
323
     * Returns the current controller.
324
     *
325
     * @return string
326
     */
327
    public function getController()
328
    {
329
        return $this->controller;
330
    }
331
332
    /**
333
     * Sets the method.
334
     *
335
     * @param  string $method
336
     * @return void
337
     */
338
    public function setMethod($method)
339
    {
340
        $this->method = strtoupper($method);
341
    }
342
343
    /**
344
     * Returns the current method.
345
     *
346
     * @return string
347
     */
348
    public function getMethod()
349
    {
350
        return $this->method;
351
    }
352
353
    /**
354
     * Returns, if set, the current route name.
355
     * Alternatively, return the current path in scope.
356
     *
357
     * @return string|null
358
     */
359
    public function getName()
360
    {
361
        return isset($this->name) ? $this->name : $this->getPath();
362
    }
363
364
    /**
365
     * Returns, if set, the current route path.
366
     *
367
     * @return string|null
368
     */
369
    public function getPath()
370
    {
371
        return isset($this->path) ? $this->path : null;
0 ignored issues
show
Bug introduced by
The property path does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
372
    }
373
374
    /**
375
     * Sets the route name.
376
     *
377
     * @param  $name
378
     * @return string|null
379
     */
380
    public function setName($name)
381
    {
382
        $this->name = $name;
383
    }
384
385
}
386