Completed
Push — master ( 926a3d...0c3913 )
by Tomasz
03:27
created

RouteDetector::addSimpleRoute()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 3
eloc 5
nc 2
nop 2
crap 3
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 25
    public function __construct($defaultRoute = '')
43
    {
44 25
        $this->setDefault($defaultRoute);
45 25
    }
46
47
    /**
48
     * Add new route.
49
     *
50
     * @param string $expression Either simple expression, or RegExp describing route.
51
     * @param string $route Route name.
52
     *
53
     * @return bool True, if route has been set, false otherwise.
54
     */
55 17
    public function addRoute($expression, $route)
56
    {
57
        //Detect command expression
58 17
        if (strpos($expression, '*') !== false) {
59 3
            return $this->addRegexpRoute($expression, $route);
60
        } else {
61 15
            return $this->addSimpleRoute($expression, $route);
62
        }
63
    }
64
    
65
    /**
66
     * Add new regexp route.
67
     *
68
     * @param string $expression RegExp describing route.
69
     * @param string $route Route name.
70
     *
71
     * @return bool True, if route has been set, false otherwise.
72
     */
73 3
    private function addRegexpRoute($expression, $route)
74
    {
75 3
        $expression = '|^'.str_replace(array('*', '\\'), array('.*', '\\\\'), $expression).'$|i';
76 3
        if (array_key_exists($expression, $this->regexpRoutes) && $this->regexpRoutes[$expression] == $route) {
77 1
            return false;
78
        }
79 3
        $this->regexpRoutes[$expression] = (string) $route;
80 3
        return true;
81
    }
82
83
    /**
84
     * Add new simple route.
85
     *
86
     * @param string $expression Simple route expression.
87
     * @param string $route Route name.
88
     *
89
     * @return bool True, if route has been set, false otherwise.
90
     */
91 15
    private function addSimpleRoute($expression, $route)
92
    {
93 15
        if (array_key_exists($expression, $this->simpleRoutes) && $this->simpleRoutes[$expression] == $route) {
94 2
            return false;
95
        }
96 15
        $this->simpleRoutes[$expression] = (string) $route;
97 15
        return true;
98
    }
99
    
100
    /**
101
     * Set default route.
102
     *
103
     * @param string $route
104
     */
105 21
    public function setDefault($route)
106
    {
107 21
        $this->defaultRoute = (string) $route;
108 21
    }
109
110
    /**
111
     * Get default route.
112
     *
113
     * @return string
114
     */
115 13
    protected function getDefault()
116
    {
117 13
        return $this->defaultRoute;
118
    }
119
120
    /**
121
     * Detect correct route for given class.
122
     *
123
     * @param string $className
124
     *
125
     * @return string
126
     *
127
     * @throws InvalidArgumentException Thrown, if argument is not a class name.
128
     */
129 24
    public function detect($className)
130
    {
131 24
        if (!class_exists($className)) {
132 1
            throw new \InvalidArgumentexception('Given name is not a class');
133
        }
134
135 23
        return $this->doDetect($className)->getPoolName();
136
    }
137
138
    /**
139
     * Detect pool.
140
     *
141
     * @param string $className
142
     * @param boolean $performInterfaceDetection
143
     *
144
     * @return DetectionInterface
145
     */
146 23
    private function doDetect($className, $performInterfaceDetection = true)
147
    {
148 23
        $detection = $this->doDetectByRoutes($className);
149 23
        if ($detection) {
150 10
            return $detection;
151
        }
152
        //Nothing is found so far. We will check all of the class interfaces and base classes.
153
        //First - check base classes up to the 'root'
154 17
        $classDetection = $this->doDetectByClassParents($className);
155 17
        if ($classDetection) {
156 4
            return $classDetection;
157
        }
158
        //Check the class interfaces
159 13
        if ($performInterfaceDetection) {
160 13
            $definition = $this->doDetectByInterfaces($className);
161 13
            if ($definition) {
162 7
                return $definition;
163
            }
164 6
        }
165
166 13
        return new DefaultDetection($this->defaultRoute);
167
    }
168
    
169
    /**
170
     * Preform detection by class parents.
171
     * 
172
     * @param string $className
173
     * @return ClassDetection|null
174
     */
175 17
    private function doDetectByClassParents($className)
176
    {
177 17
        $parentClass = get_parent_class($className);
178 17
        if ($parentClass) {
179 15
            $parentDetection = $this->doDetect($parentClass, false);
180 15
            if ($parentDetection instanceof ClassDetection) {
181 4
                return $parentDetection;
182
            }
183 11
        }
184 13
    }
185
186
    /**
187
     * Perform detection based on class name and routes registered for this class.
188
     *
189
     * @param string $className
190
     *
191
     * @return ClassDetection|null
192
     */
193 23
    private function doDetectByRoutes($className)
194
    {
195
        //If we have entry for a command class, we should always return it, as it is most specific.
196 23
        if (!empty($this->simpleRoutes[$className])) {
197 15
            return new ClassDetection($this->simpleRoutes[$className]);
198
        }
199
        //Now, we should check, if we have 'regexp' entry for class
200 19
        foreach ($this->regexpRoutes as $regexpRoute => $poolName) {
201 2
            if (preg_match($regexpRoute, $className)) {
202 2
                return new ClassDetection($poolName);
203
            }
204 17
        }
205 17
    }
206
207
    /**
208
     * Perform detection based on class interfaces.
209
     *
210
     * @param string $className
211
     *
212
     * @return DetectionInterface|null
213
     */
214 13
    private function doDetectByInterfaces($className)
215
    {
216 13
        $interfaces = $this->getOrderedInterfaces($className);
217 13
        foreach ($interfaces as $interface) {
218 12
            $candidate = $this->doDetectByRoutes($interface);
219 12
            if ($candidate) {
220 7
                return $candidate;
221
            }
222 10
        }
223
224 6
        return;
225
    }
226
227
    /**
228
     * Get a list of class interfaces ordered by plase of interface declaration.
229
     *
230
     * The list is ordered by place of interface declaration. Interfaces declared on most child class are first,
231
     * while those in base class(es) - last.
232
     *
233
     * @param string $className
234
     *
235
     * @return array
236
     */
237 13
    private function getOrderedInterfaces($className)
238
    {
239 13
        $interfacesArr = $this->prepareClassTreeInterfaces($className);
240 13
        $interfaces = array();
241 13
        foreach ($interfacesArr as $classInterfaces) {
242 12
            foreach ($classInterfaces as $interface) {
243 12
                if (array_search($interface, $interfaces) === false) {
244 12
                    array_unshift($interfaces, $interface);
245 12
                }
246 12
            }
247 13
        }
248
249 13
        return $interfaces;
250
    }
251
    
252
    /**
253
     * Prepares all interfaces for given class and its parents.
254
     * 
255
     * @param string $className
256
     * @return array
257
     */
258 13
    private function prepareClassTreeInterfaces($className)
259
    {
260 13
        $interfacesArr = array();
261 13
        $reflection = new ReflectionClass($className);
262 13
        $interfacesArr[] = $reflection->getInterfaceNames();
263 13
        if (empty($interfacesArr[0])) {
264 1
            return array();
265
        }
266 12
        $classParents = class_parents($className);
267 12
        foreach ($classParents as $parentClass) {
268 11
            $reflection = new ReflectionClass($parentClass);
269 11
            $tmpInterfaces = $reflection->getInterfaceNames();
270 11
            if (empty($tmpInterfaces)) {
271 8
                return $interfacesArr;
272
            }
273 10
            array_unshift($interfacesArr, $tmpInterfaces);
274 11
        }
275 4
        return $interfacesArr;
276
    }
277
}
278