Completed
Push — master ( 933710...705382 )
by Tomasz
02:58
created

RouteDetector::getOrderedInterfaces()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 8.1239

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 14
ccs 4
cts 11
cp 0.3636
rs 9.2
c 1
b 0
f 0
cc 4
eloc 8
nc 4
nop 1
crap 8.1239
1
<?php
2
3
namespace Gendoria\CommandQueue\RouteDetection\Detector;
4
5
use Gendoria\CommandQueue\RouteDetection\Detection\ClassDetection;
6
use Gendoria\CommandQueue\RouteDetection\Detection\DefaultDetection;
7
use Gendoria\CommandQueue\RouteDetection\Detection\DetectionInterface;
8
use InvalidArgumentException;
9
use ReflectionClass;
10
11
/**
12
 * Detector class used for match class expressions with arbitrary routes.
13
 *
14
 * @author Tomasz Struczyński <[email protected]>
15
 */
16
class RouteDetector
17
{
18
    /**
19
     * Simple routes in a form of [Class] => [PoolName].
20
     *
21
     * @var array
22
     */
23
    private $simpleRoutes = array();
24
25
    /**
26
     * Expression routes in a form of [ClassExpression] => [PoolName].
27
     *
28
     * @var array
29
     */
30
    private $regexpRoutes = array();
31
32
    /**
33
     * Default route.
34
     *
35
     * @var string
36
     */
37
    private $defaultRoute = '';
38
39
    /**
40
     * Class constructor.
41
     */
42 22
    public function __construct($defaultRoute = '')
43
    {
44 22
        $this->setDefault($defaultRoute);
45 22
    }
46
47
    /**
48
     * Add new route.
49
     *
50
     * @param string $expression Either simple expression, or RegExp describing route.
51
     * @param string $route
52
     *
53
     * @return bool True, if route has been set, false otherwise.
54
     */
55 16
    public function addRoute($expression, $route)
56
    {
57
        //Detect command expression
58 16
        if (strpos($expression, '*') !== false) {
59 2
            $expression = '|'.str_replace(array('*', '\\'), array('.*', '\\\\'), $expression).'|i';
60 2
            if (array_key_exists($expression, $this->regexpRoutes) && $this->regexpRoutes[$expression] == $route) {
61
                return false;
62
            }
63 2
            $this->regexpRoutes[$expression] = (string) $route;
64 2
        } else {
65 14
            if (array_key_exists($expression, $this->simpleRoutes) && $this->simpleRoutes[$expression] == $route) {
66
                return false;
67
            }
68 14
            $this->simpleRoutes[$expression] = (string) $route;
69
        }
70
71 16
        return true;
72
    }
73
74
    /**
75
     * Set default route.
76
     *
77
     * @param string $route
78
     */
79 19
    public function setDefault($route)
80
    {
81 19
        $this->defaultRoute = (string) $route;
82 19
    }
83
84
    /**
85
     * Get default route.
86
     *
87
     * @return string
88
     */
89 12
    protected function getDefault()
90
    {
91 12
        return $this->defaultRoute;
92
    }
93
94
    /**
95
     * Detect correct route for given class.
96
     *
97
     * @param string $className
98
     *
99
     * @return string
100
     *
101
     * @throws InvalidArgumentException Thrown, if argument is not a class name.
102
     */
103 22
    public function detect($className)
104
    {
105 22
        if (!class_exists($className)) {
106 1
            throw new \InvalidArgumentexception('Given name is not a class');
107
        }
108
109 21
        return $this->doDetect($className)->getPoolName();
110
    }
111
112
    /**
113
     * Detect pool.
114
     *
115
     * @param string $className
116
     * @param boolean $performInterfaceDetection
117
     *
118
     * @return DetectionInterface
119
     */
120 21
    private function doDetect($className, $performInterfaceDetection = true)
121
    {
122 21
        $detection = $this->doDetectByRoutes($className);
123 21
        if ($detection) {
124 9
            return $detection;
125
        }
126
        //Nothing is found so far. We will check all of the class interfaces and base classes.
127
        //First - check base classes up to the 'root'
128 16
        $parentClass = get_parent_class($className);
129 16
        if ($parentClass) {
130 15
            $parentDetection = $this->doDetect($parentClass, false);
131 15
            if ($parentDetection instanceof ClassDetection) {
132 4
                return $parentDetection;
133
            }
134 11
        }
135
        //Check the class interfaces
136 12
        if ($performInterfaceDetection) {
137 12
            $definition = $this->doDetectByInterfaces($className);
138
            if ($definition) {
139
                return $definition;
140
            }
141
        }
142
143 11
        return new DefaultDetection($this->defaultRoute);
144
    }
145
146
    /**
147
     * Perform detection based on class name and routes registered for this class.
148
     *
149
     * @param string $className
150
     *
151
     * @return ClassDetection|null
152
     */
153 21
    private function doDetectByRoutes($className)
154
    {
155
        //If we have entry for a command class, we should always return it, as it is most specific.
156 21
        if (!empty($this->simpleRoutes[$className])) {
157 7
            return new ClassDetection($this->simpleRoutes[$className]);
158
        }
159
        //Now, we should check, if we have 'regexp' entry for class
160 18
        foreach ($this->regexpRoutes as $regexpRoute => $poolName) {
161 2
            if (preg_match($regexpRoute, $className)) {
162 2
                return new ClassDetection($poolName);
163
            }
164 16
        }
165 16
    }
166
167
    /**
168
     * Perform detection based on class interfaces.
169
     *
170
     * @param string $className
171
     *
172
     * @return DetectionInterface|null
173
     */
174 12
    private function doDetectByInterfaces($className)
175
    {
176 12
        $interfaces = $this->getOrderedInterfaces($className);
177
        foreach ($interfaces as $interface) {
178
            $candidate = $this->doDetectByRoutes($interface);
179
            if ($candidate) {
180
                return $candidate;
181
            }
182
        }
183
184
        return;
185
    }
186
187
    /**
188
     * Get a list of class interfaces ordered by plase of interface declaration.
189
     *
190
     * The list is ordered by place of interface declaration. Interfaces declared on most child class are first,
191
     * while those in base class(es) - last.
192
     *
193
     * @param string $className
194
     *
195
     * @return array
196
     */
197 12
    private function getOrderedInterfaces($className)
198
    {
199 12
        $interfacesArr = $this->prepareClassTreeInterfaces($className);
200 12
        $interfaces = array();
201 12
        foreach ($interfacesArr as $classInterfaces) {
0 ignored issues
show
Bug introduced by
The expression $interfacesArr 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...
202
            foreach ($classInterfaces as $interface) {
203
                if (array_search($interface, $interfaces) === false) {
204
                    array_unshift($interfaces, $interface);
205
                }
206
            }
207
        }
208
209
        return $interfaces;
210
    }
211
    
212
    /**
213
     * Prepares all interfaces for given class and its parents.
214
     * 
215
     * @param string $className
216
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be array|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
217
     */
218 12
    private function prepareClassTreeInterfaces($className)
219
    {
220 12
        $interfacesArr = array();
221 12
        $reflection = new ReflectionClass($className);
222 12
        $interfacesArr[] = $reflection->getInterfaceNames();
223 12
        if (empty($interfacesArr[0])) {
224
            return array();
225
        }
226 12
        $classParents = class_parents($className);
227 12
        foreach ($classParents as $parentClass) {
228 11
            $reflection = new ReflectionClass($parentClass);
229 11
            array_unshift($interfacesArr, $reflection->getInterfaceNames());
230 11
            if (empty($interfacesArr[0])) {
231 8
                break;
232
            }
233 12
        }
234 12
    }
235
}
236