Completed
Push — master ( 0c3913...67a509 )
by Tomasz
03:16
created

RouteDetector::addRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.6666
cc 2
eloc 5
nc 2
nop 2
crap 2
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
        $parentClass = get_parent_class($className);
155 17
        if ($parentClass) {
156 15
            $parentDetection = $this->doDetect($parentClass, false);
157 15
            if ($parentDetection instanceof ClassDetection) {
158 4
                return $parentDetection;
159
            }
160 11
        }
161
        //Check the class interfaces
162 13
        if ($performInterfaceDetection) {
163 13
            $definition = $this->doDetectByInterfaces($className);
164 13
            if ($definition) {
165 7
                return $definition;
166
            }
167 6
        }
168
169 13
        return new DefaultDetection($this->defaultRoute);
170
    }
171
172
    /**
173
     * Perform detection based on class name and routes registered for this class.
174
     *
175
     * @param string $className
176
     *
177
     * @return ClassDetection|null
178
     */
179 23
    private function doDetectByRoutes($className)
180
    {
181
        //If we have entry for a command class, we should always return it, as it is most specific.
182 23
        if (!empty($this->simpleRoutes[$className])) {
183 15
            return new ClassDetection($this->simpleRoutes[$className]);
184
        }
185
        //Now, we should check, if we have 'regexp' entry for class
186 19
        foreach ($this->regexpRoutes as $regexpRoute => $poolName) {
187 2
            if (preg_match($regexpRoute, $className)) {
188 2
                return new ClassDetection($poolName);
189
            }
190 17
        }
191 17
    }
192
193
    /**
194
     * Perform detection based on class interfaces.
195
     *
196
     * @param string $className
197
     *
198
     * @return DetectionInterface|null
199
     */
200 13
    private function doDetectByInterfaces($className)
201
    {
202 13
        $interfaces = $this->getOrderedInterfaces($className);
203 13
        foreach ($interfaces as $interface) {
204 12
            $candidate = $this->doDetectByRoutes($interface);
205 12
            if ($candidate) {
206 7
                return $candidate;
207
            }
208 10
        }
209
210 6
        return;
211
    }
212
213
    /**
214
     * Get a list of class interfaces ordered by plase of interface declaration.
215
     *
216
     * The list is ordered by place of interface declaration. Interfaces declared on most child class are first,
217
     * while those in base class(es) - last.
218
     *
219
     * @param string $className
220
     *
221
     * @return array
222
     */
223 13
    private function getOrderedInterfaces($className)
224
    {
225 13
        $interfacesArr = $this->prepareClassTreeInterfaces($className);
226 13
        $interfaces = array();
227 13
        foreach ($interfacesArr as $classInterfaces) {
228 12
            foreach ($classInterfaces as $interface) {
229 12
                if (array_search($interface, $interfaces) === false) {
230 12
                    array_unshift($interfaces, $interface);
231 12
                }
232 12
            }
233 13
        }
234
235 13
        return $interfaces;
236
    }
237
    
238
    /**
239
     * Prepares all interfaces for given class and its parents.
240
     * 
241
     * @param string $className
242
     * @return array
243
     */
244 13
    private function prepareClassTreeInterfaces($className)
245
    {
246 13
        $interfacesArr = array();
247 13
        $reflection = new ReflectionClass($className);
248 13
        $interfacesArr[] = $reflection->getInterfaceNames();
249 13
        if (empty($interfacesArr[0])) {
250 1
            return array();
251
        }
252 12
        $classParents = class_parents($className);
253 12
        foreach ($classParents as $parentClass) {
254 11
            $reflection = new ReflectionClass($parentClass);
255 11
            $tmpInterfaces = $reflection->getInterfaceNames();
256 11
            if (empty($tmpInterfaces)) {
257 8
                return $interfacesArr;
258
            }
259 10
            array_unshift($interfacesArr, $tmpInterfaces);
260 11
        }
261 4
        return $interfacesArr;
262
    }
263
}
264