1
|
|
|
<?php namespace Comodojo\Dispatcher\Router; |
2
|
|
|
|
3
|
|
|
use \Monolog\Logger; |
4
|
|
|
use \Exception; |
5
|
|
|
|
6
|
|
|
/** |
7
|
|
|
* @package Comodojo Dispatcher |
8
|
|
|
* @author Marco Giovinazzi <[email protected]> |
9
|
|
|
* @author Marco Castiello <[email protected]> |
10
|
|
|
* @license GPL-3.0+ |
11
|
|
|
* |
12
|
|
|
* LICENSE: |
13
|
|
|
* |
14
|
|
|
* This program is free software: you can redistribute it and/or modify |
15
|
|
|
* it under the terms of the GNU Affero General Public License as |
16
|
|
|
* published by the Free Software Foundation, either version 3 of the |
17
|
|
|
* License, or (at your option) any later version. |
18
|
|
|
* |
19
|
|
|
* This program is distributed in the hope that it will be useful, |
20
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
21
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
22
|
|
|
* GNU Affero General Public License for more details. |
23
|
|
|
* |
24
|
|
|
* You should have received a copy of the GNU Affero General Public License |
25
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
26
|
|
|
*/ |
27
|
|
|
|
28
|
|
|
class RoutingTable implements RoutingTableInterface { |
29
|
|
|
|
30
|
|
|
private $routes = array(); |
31
|
|
|
private $logger; |
32
|
|
|
|
33
|
|
|
public function __construct(Logger $logger) { |
34
|
|
|
|
35
|
|
|
$this->logger = $logger; |
36
|
|
|
|
37
|
|
|
} |
38
|
|
|
|
39
|
|
View Code Duplication |
public function put($route, $type, $class, $parameters = array()) { |
|
|
|
|
40
|
|
|
|
41
|
|
|
$folders = explode("/", $route); |
42
|
|
|
|
43
|
|
|
$regex = $this->readpath($folders); |
44
|
|
|
|
45
|
|
|
if (!isset($this->routes[$regex])) { |
46
|
|
|
|
47
|
|
|
$this->add($folders, $type, $class, $parameters); |
48
|
|
|
|
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
} |
52
|
|
|
|
53
|
|
View Code Duplication |
public function set($route, $type, $class, $parameters = array()) { |
|
|
|
|
54
|
|
|
|
55
|
|
|
$folders = explode("/", $route); |
56
|
|
|
|
57
|
|
|
$regex = $this->readpath($folders); |
58
|
|
|
|
59
|
|
|
if (isset($this->routes[$regex])) { |
60
|
|
|
|
61
|
|
|
$this->add($folders, $type, $class, $parameters); |
62
|
|
|
|
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
} |
66
|
|
|
|
67
|
|
View Code Duplication |
public function get($route) { |
|
|
|
|
68
|
|
|
|
69
|
|
|
$folders = explode("/", $route); |
70
|
|
|
|
71
|
|
|
$regex = $this->readpath($folders); |
72
|
|
|
|
73
|
|
|
if (isset($this->routes[$regex])) |
74
|
|
|
return $this->routes[$regex]; |
75
|
|
|
else |
76
|
|
|
return null; |
77
|
|
|
|
78
|
|
|
} |
79
|
|
|
|
80
|
|
View Code Duplication |
public function remove($route) { |
|
|
|
|
81
|
|
|
|
82
|
|
|
$folders = explode("/", $route); |
83
|
|
|
|
84
|
|
|
$regex = $this->readpath($folders); |
85
|
|
|
|
86
|
|
|
if (isset($this->routes[$regex])) unset($this->routes[$regex]); |
87
|
|
|
|
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
public function routes() { |
91
|
|
|
|
92
|
|
|
return $this->routes; |
93
|
|
|
|
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
public function defaultRoute() { |
97
|
|
|
|
98
|
|
|
return $this->get('/'); |
99
|
|
|
|
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
// This method read the route (folder by folder recursively) and build |
103
|
|
|
// the global regular expression against which all the request URI will be compared |
104
|
|
|
private function readpath($folders = array(), &$value = null, $regex = '') { |
105
|
|
|
|
106
|
|
|
// if the first 'folder' is empty is removed |
107
|
|
|
while (!empty($folders) && empty($folders[0])) { |
108
|
|
|
|
109
|
|
|
array_shift($folders); |
110
|
|
|
|
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
// if the 'folder' array is empty, the route has been fully analyzed |
114
|
|
|
// this is the exit condition from the recursive loop. |
115
|
|
|
if (empty($folders)) { |
116
|
|
|
|
117
|
|
|
return '^'.$regex.'[\/]?$'; |
118
|
|
|
|
119
|
|
|
} else { |
120
|
|
|
|
121
|
|
|
// The first element of the array 'folders' is taken in order to be analyzed |
122
|
|
|
$folder = array_shift($folders); |
123
|
|
|
|
124
|
|
|
// All the parameters of the route must be json strings |
125
|
|
|
$decoded = json_decode($folder, true); |
126
|
|
|
|
127
|
|
|
if (!is_null($decoded) && is_array($decoded)) { |
128
|
|
|
|
129
|
|
|
$param_regex = ''; |
130
|
|
|
|
131
|
|
|
$param_required = false; |
132
|
|
|
|
133
|
|
|
/* All the folders can include more than one parameter |
134
|
|
|
* Eg: /service_name/{'param1': 'regex1', 'param2': 'regex2'}/ |
135
|
|
|
* /calendar/{'ux_timestamp*': '\d{10}', 'microseconds': '\d{4}'}/ |
136
|
|
|
* |
137
|
|
|
* The '*' at the end of the paramerter name implies that the parameter is required |
138
|
|
|
* This example can be read as a calendar service that accepts both |
139
|
|
|
* timestamps in unix or javascript format. |
140
|
|
|
* |
141
|
|
|
* This is the reason of the following 'foreach' |
142
|
|
|
*/ |
143
|
|
|
foreach ($decoded as $key => $string) { |
144
|
|
|
|
145
|
|
|
$this->logger->debug("PARAMETER KEY: " . $key); |
146
|
|
|
|
147
|
|
|
$this->logger->debug("PARAMETER STRING: " . $string); |
148
|
|
|
|
149
|
|
|
/* The key and the regex of every paramater is passed to the 'readparam' |
150
|
|
|
* method which will build an appropriate regular expression and will understand |
151
|
|
|
* if the parameter is required and will build the $value['query'] object |
152
|
|
|
*/ |
153
|
|
|
$param_regex .= $this->readparam($key, $string, $param_required, $value); |
154
|
|
|
|
155
|
|
|
$this->logger->debug("PARAMETER REGEX: " . $param_regex); |
156
|
|
|
|
157
|
|
|
} |
158
|
|
|
// Once the parameter is analyzed, the result is passed to the next iteration |
159
|
|
|
$this->readpath( |
160
|
|
|
$folders, |
161
|
|
|
$value, |
162
|
|
|
$regex.'(?:\/'.$param_regex.')'. (($param_required)?'{1}':'?') |
163
|
|
|
); |
164
|
|
|
|
165
|
|
|
} else { |
166
|
|
|
// if the element is not a json string, I assume it's the service name |
167
|
|
|
array_push($value['service'], $folder); |
168
|
|
|
|
169
|
|
|
$this->readpath( |
170
|
|
|
$folders, |
171
|
|
|
$value, |
172
|
|
|
$regex.'\/'.$folder |
173
|
|
|
); |
174
|
|
|
|
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
// This method read a single parameter and build the regular expression |
182
|
|
|
private function readparam($key, $string, &$param_required, &$value) { |
183
|
|
|
|
184
|
|
|
$field_required = false; |
185
|
|
|
|
186
|
|
|
// If the field name ends with a '*', the parameter is considered as required |
187
|
|
|
if (preg_match('/^(.+)\*$/', $key, $bits)) { |
188
|
|
|
|
189
|
|
|
$key = $bits[1]; |
190
|
|
|
$field_required = true; |
191
|
|
|
$param_required = true; |
192
|
|
|
|
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// The $value['query'] field contains all regex which will be used by the collector to parse the route fields |
196
|
|
|
if (!is_null($value)) { |
197
|
|
|
|
198
|
|
|
$value['query'][$key] = array( |
199
|
|
|
'regex' => $string, |
200
|
|
|
'required' => $required |
|
|
|
|
201
|
|
|
); |
202
|
|
|
|
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/* Every parameter can include it's own logic into the regular expression, |
206
|
|
|
* it can use backreferences and it's expected to be used against a single parameter. |
207
|
|
|
* This means that it can't be used as is to build the route regular expression, |
208
|
|
|
* Backreferences are not useful at this point and can make the regular expression more time consuming |
209
|
|
|
* and resource hungry. This is why they are replaced with the grouping parenthesis. |
210
|
|
|
* Eg: (value) changes in (?:value) |
211
|
|
|
* |
212
|
|
|
* Delimiting characters like '^' and '$' are also meaningless in the complete regular expression and |
213
|
|
|
* need to be removed. Contrariwise, wildcards must be delimited in order to keet the whole regular |
214
|
|
|
* expression consistent, hence a '?' is added to all the '.*' or '.+' that don't already have one. |
215
|
|
|
*/ |
216
|
|
|
$string = preg_replace('/(?<!\\)\((?!\?)/', '(?:', $string); |
217
|
|
|
$string = preg_replace('/\.([\*\+])(?!\?)/', '.\${1}?', $string); |
218
|
|
|
$string = preg_replace('/^[\^]/', '', $string); |
219
|
|
|
$string = preg_replace('/[\$]$/', '', $string); |
220
|
|
|
|
221
|
|
|
/* The produced regular expression is grouped and associated with its key (this means that the 'preg_match' |
222
|
|
|
* function will generate an associative array where the key/value association is preserved). |
223
|
|
|
* If the field is required, the regular expression is completed with a '{1}' (which make it compulsory), |
224
|
|
|
* otherwise a '?' is added. |
225
|
|
|
*/ |
226
|
|
|
return '(?P<' . $key . '>' . $string . ')' . (($field_required)?'{1}':'?'); |
227
|
|
|
|
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
// This method add a route to the supported list |
231
|
|
|
private function add($folders, $type, $class, $parameters) { |
232
|
|
|
|
233
|
|
|
// The values associated with a route are as follows: |
234
|
|
|
$value = array( |
235
|
|
|
"type" => $type, // Type of route |
236
|
|
|
"class" => $class, // Class to be invoked |
237
|
|
|
"service" => array(), // Service name (it can be a list of namespaces plus a final service name) |
238
|
|
|
"parameters" => $parameters, // Parameters passed via the composer.json configuration (cache, ttl, etc...) |
239
|
|
|
"query" => array() // List of parameters with their regular expression that must be added among the query parameters |
240
|
|
|
); |
241
|
|
|
|
242
|
|
|
$this->logger->debug("ROUTE: " . $route); |
|
|
|
|
243
|
|
|
|
244
|
|
|
$this->logger->debug("PARAMETERS: " . var_export($value, true)); |
245
|
|
|
|
246
|
|
|
// This method generate a global regular expression which will be able to match all the URI supported by the route |
247
|
|
|
$regex = $this->readpath($folders, $value); |
248
|
|
|
|
249
|
|
|
$this->logger->debug("ROUTE: " . $regex); |
250
|
|
|
|
251
|
|
|
$this->logger->debug("PARAMETERS: " . var_export($value, true)); |
252
|
|
|
|
253
|
|
|
$this->routes[$regex] = $value; |
254
|
|
|
|
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
} |
258
|
|
|
|
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.