Test Failed
Branch dev (494019)
by Alex
03:13
created

collectControllerRoutes()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 25
rs 8.5806
cc 4
eloc 14
nc 3
nop 3
1
<?php
2
3
/**
4
 * Codeburner Framework.
5
 *
6
 * @author Alex Rohleder <[email protected]>
7
 * @copyright 2016 Alex Rohleder
8
 * @license http://opensource.org/licenses/MIT
9
 */
10
11
namespace Codeburner\Router\Collectors;
12
13
use Codeburner\Router\Collector;
14
use Codeburner\Router\Group;
15
use ReflectionClass;
16
use ReflectionMethod;
17
use ReflectionParameter;
18
use Reflector;
19
20
/**
21
 * Methods for enable the collector to make routes from a controller.
22
 *
23
 * @author Alex Rohleder <[email protected]>
24
 */
25
26
trait ControllerCollectorTrait
27
{
28
29
    abstract public function getWildcards();
30
    abstract public function set($method, $pattern, $action);
31
32
    /**
33
     * Define how controller actions names will be joined to form the route pattern.
34
     *
35
     * @var string
36
     */
37
38
    protected $controllerActionJoin = "/";
39
40
    /**
41
     * Maps all the controller methods that begins with a HTTP method, and maps the rest of
42
     * name as a path. The path will be the method name with slashes before every camelcased 
43
     * word and without the HTTP method prefix, and the controller name will be used to prefix
44
     * the route pattern. e.g. ArticlesController::getCreate will generate a route to: GET articles/create
45
     *
46
     * @param string $controller The controller name
47
     * @param string $prefix
48
     *
49
     * @throws \ReflectionException
50
     * @return Group
51
     */
52
53
    public function controller($controller, $prefix = null)
54
    {
55
        $controller = new ReflectionClass($controller);
56
        $prefix     = $prefix === null ? $this->getControllerPrefix($controller) : $prefix;
57
        $methods    = $controller->getMethods(ReflectionMethod::IS_PUBLIC);
58
        return $this->collectControllerRoutes($controller, $methods, "/$prefix/");
59
    }
60
61
    /**
62
     * Maps several controllers at same time.
63
     *
64
     * @param string[] $controllers Controllers name.
65
     * @throws \ReflectionException
66
     * @return Group
67
     */
68
69 View Code Duplication
    public function controllers(array $controllers)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
70
    {
71
        $group = [];
72
        foreach ($controllers as $controller)
73
            $group[] = $this->controller($controller);
74
        return new Group($group);
75
    }
76
77
    /**
78
     * Alias for Collector::controller but maps a controller without using the controller name as prefix.
79
     *
80
     * @param string $controller The controller name
81
     * @throws \ReflectionException
82
     * @return Group
83
     */
84
85
    public function controllerWithoutPrefix($controller)
86
    {
87
        $controller = new ReflectionClass($controller);
88
        $methods = $controller->getMethods(ReflectionMethod::IS_PUBLIC);
89
        return $this->collectControllerRoutes($controller, $methods, "/");
90
    }
91
92
    /**
93
     * Alias for Collector::controllers but maps a controller without using the controller name as prefix.
94
     *
95
     * @param string[] $controllers
96
     * @throws \ReflectionException
97
     * @return Group
98
     */
99
100 View Code Duplication
    public function controllersWithoutPrefix(array $controllers)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101
    {
102
        $group = [];
103
        foreach ($controllers as $controller)
104
            $group[] = $this->controllerWithoutPrefix($controller);
105
        return new Group($group);
106
    }
107
108
    /**
109
     * @param ReflectionClass $controller
110
     * @param string[] $methods
111
     * @param string $prefix
112
     *
113
     * @return Group
114
     */
115
116
    protected function collectControllerRoutes(ReflectionClass $controller, array $methods, $prefix)
117
    {
118
        $group = [];
119
        $controllerDefaultStrategy = $this->getAnnotatedStrategy($controller);
120
121
        /** @var ReflectionMethod $method */
122
        foreach ($methods as $method) {
123
            $name = preg_split("~(?=[A-Z])~", $method->name);
124
            $http = $name[0];
125
            unset($name[0]);
126
 
127
            if (strpos(Collector::HTTP_METHODS, $http) !== false) {
128
                $action  = $prefix . strtolower(implode($this->controllerActionJoin, $name));
129
                $dynamic = $this->getMethodConstraints($method);
0 ignored issues
show
Documentation introduced by
$method is of type string, but the function expects a object<ReflectionMethod>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
130
131
                /** @var \Codeburner\Router\Route $route */
132
                $route = $this->set($http, "$action$dynamic", [$controller->name, $method->name]);
133
                $route->setStrategy($this->getAnnotatedStrategy($method) || $controllerDefaultStrategy);
0 ignored issues
show
Documentation introduced by
$method is of type string, but the function expects a object<Reflector>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$this->getAnnotatedStrat...ntrollerDefaultStrategy is of type boolean, but the function expects a string|object<Codeburner...gies\StrategyInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug Best Practice introduced by
The expression $this->getAnnotatedStrategy($method) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $controllerDefaultStrategy of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
134
135
                $group[] = $route;
136
            }
137
        }
138
139
        return new Group($group);
140
    }
141
142
    /**
143
     * @param ReflectionClass $controller
144
     *
145
     * @return string
146
     */
147
148
    protected function getControllerPrefix(ReflectionClass $controller)
149
    {
150
        preg_match("~\@prefix\s([a-zA-Z\\\_]+)~i", (string) $controller->getDocComment(), $prefix);
151
        return isset($prefix[1]) ? $prefix[1] : str_replace("controller", "", strtolower($controller->getShortName()));
152
    }
153
154
    /**
155
     * @param \ReflectionMethod
156
     * @return string
157
     */
158
159
    protected function getMethodConstraints(ReflectionMethod $method)
160
    {
161
        $beginPath = "";
162
        $endPath = "";
163
164
        if ($parameters = $method->getParameters()) {
165
            $types = $this->getParamsConstraint($method);
166
167
            foreach ($parameters as $parameter) {
168
                if ($parameter->isOptional()) {
169
                    $beginPath .= "[";
170
                    $endPath .= "]";
171
                }
172
173
                $beginPath .= $this->getPathConstraint($parameter, $types);
174
            }
175
        }
176
177
        return $beginPath . $endPath;
178
    }
179
180
    /**
181
     * @param ReflectionParameter $parameter
182
     * @param string[] $types
183
     * @return string
184
     */
185
186
    protected function getPathConstraint(ReflectionParameter $parameter, $types)
187
    {
188
        $name = $parameter->name;
189
        $path = "/{" . $name;
190
        return isset($types[$name]) ? "$path:{$types[$name]}}" : "$path}";
191
    }
192
193
    /**
194
     * @param ReflectionMethod $method
195
     * @return string[]
196
     */
197
198
    protected function getParamsConstraint(ReflectionMethod $method)
199
    {
200
        $params = [];
201
        preg_match_all("~\@param\s(" . implode("|", array_keys($this->getWildcards())) . "|\(.+\))\s\\$([a-zA-Z0-1_]+)~i",
202
            $method->getDocComment(), $types, PREG_SET_ORDER);
203
204
        foreach ((array) $types as $type) {
205
            // if a pattern is defined on Match take it otherwise take the param type by PHPDoc.
206
            $params[$type[2]] = isset($type[4]) ? $type[4] : $type[1];
207
        }
208
209
        return $params;
210
    }
211
212
    /**
213
     * @param Reflector $reflector
214
     * @return string|null
215
     */
216
217
    protected function getAnnotatedStrategy(Reflector $reflector)
218
    {
219
        preg_match("~\@strategy\s([a-zA-Z\\\_]+)~i", (string) $reflector->getDocComment(), $strategy);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
220
        return isset($strategy[1]) ? $strategy[1] : null;
221
    }
222
223
    /**
224
     * Define how controller actions names will be joined to form the route pattern.
225
     * Defaults to "/" so actions like "getMyAction" will be "/my/action". If changed to
226
     * "-" the new pattern will be "/my-action".
227
     *
228
     * @param string $join
229
     */
230
231
    public function setControllerActionJoin($join)
232
    {
233
        $this->controllerActionJoin = $join;
234
    }
235
236
    /**
237
     * @return string
238
     */
239
240
    public function getControllerActionJoin()
241
    {
242
        return $this->controllerActionJoin;
243
    }
244
245
}
246