├── .State ├── .gitignore ├── CHANGELOG.md ├── Cli.php ├── Documentation ├── En │ └── Index.xyl └── Fr │ └── Index.xyl ├── Exception ├── Exception.php └── NotFound.php ├── Generic.php ├── Http ├── Dav.php └── Http.php ├── README.md ├── Router.php └── composer.json /.State: -------------------------------------------------------------------------------- 1 | finalized 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.17.01.14 2 | 3 | * Quality: Happy new year! (Alexis von Glasow, 2017-01-09T21:36:54+01:00) 4 | * Documentation: Use TLS if possible. (Ivan Enderlin, 2016-10-21T17:10:15+02:00) 5 | * Documentation: New `README.md` file. (Ivan Enderlin, 2016-10-21T17:08:26+02:00) 6 | * Documentation: Update `support` properties. (Ivan Enderlin, 2016-10-11T16:53:26+02:00) 7 | 8 | # 3.16.01.14 9 | 10 | * Composer: New stable library. (Ivan Enderlin, 2016-01-14T22:10:57+01:00) 11 | 12 | # 3.16.01.11 13 | 14 | * Quality: Drop PHP5.4. (Ivan Enderlin, 2016-01-11T09:15:26+01:00) 15 | * Quality: Run devtools:cs. (Ivan Enderlin, 2016-01-09T09:08:14+01:00) 16 | * Core: Remove `Hoa\Core`. (Ivan Enderlin, 2016-01-09T08:23:46+01:00) 17 | * Parameters: Use `Hoa\Zformat`. (Ivan Enderlin, 2016-01-08T17:15:09+01:00) 18 | * Consistency: Use `Hoa\Consistency`. (Ivan Enderlin, 2015-12-08T21:47:38+01:00) 19 | * Exception: Use `Hoa\Exception`. (Ivan Enderlin, 2015-11-20T13:09:03+01:00) 20 | 21 | # 2.15.10.21 22 | 23 | * Documentation: Fix typographical error(s) (orthographic-pedant, 2015-10-07T14:45:46-04:00) 24 | * Add a `.gitignore` file. (Stéphane HULARD, 2015-08-03T11:44:52+02:00) 25 | 26 | # 2.15.08.03 27 | 28 | * `getDomain` uses `HTTP_HOST` instead of `SERVER_NAME`. (camael24, 2015-03-20T15:01:51+01:00) 29 | * Filter protected variables when unrouting. (Ivan Enderlin, 2015-07-30T16:46:01+02:00) 30 | * Fix CS. (Ivan Enderlin, 2015-07-30T08:11:37+02:00) 31 | * Append unused variables to the querystring when unrouting rules (Kévin Gomez, 2015-07-16T23:02:02+02:00) 32 | 33 | # 2.15.05.29 34 | 35 | * Move to PSR-1 and PSR-2. (Ivan Enderlin, 2015-05-20T09:27:10+02:00) 36 | 37 | # 2.15.02.26 38 | 39 | * Decode URI after having splitted subdomain and URI. (Ivan Enderlin, 2015-02-26T11:28:50+01:00) 40 | * Add the `CHANGELOG.md` file. (Ivan Enderlin, 2015-02-26T09:02:35+01:00) 41 | * Complete HTTP methods (match IANA list). (Ivan Enderlin, 2015-02-17T16:41:42+01:00) 42 | * Sort HTTP methods to ease the reading. (Ivan Enderlin, 2015-02-17T16:26:10+01:00) 43 | * Add the `Hoa\Router\Http\Dav` router! (Ivan Enderlin, 2015-02-17T14:16:31+01:00) 44 | * Allow to define new HTTP methods in sub-classes. (Ivan Enderlin, 2015-02-17T14:24:45+01:00) 45 | * Move `Hoa\Router\Http` to `Hoa\Router\Http\Http`. (Ivan Enderlin, 2015-02-17T14:15:47+01:00) 46 | * Remove a useless line. (Ivan Enderlin, 2015-01-29T14:08:19+01:00) 47 | * Remove `from`/`import` and update to PHP5.4. (Matthieu de Laubrie, 2015-01-26T14:35:26+01:00) 48 | * Update schemas and links in the documentation. (Ivan Enderlin, 2015-01-23T19:26:29+01:00) 49 | 50 | # 2.15.01.23 51 | 52 | * Fix when the `_method` router variable is missing. (Ivan Enderlin, 2015-01-09T14:48:03+01:00) 53 | * Add the `_uri` and `_method` special variables in the HTTP router. (Ivan Enderlin, 2015-01-05T23:46:55+01:00) 54 | * Happy new year! (Ivan Enderlin, 2015-01-05T15:07:49+01:00) 55 | 56 | # 2.14.12.10 57 | 58 | * Move to PSR-4. (Ivan Enderlin, 2014-12-09T18:44:18+01:00) 59 | * Format documentation. (Ivan Enderlin, 2014-12-04T15:40:09+01:00) 60 | * Fix documentation. (Samuel Laulhau, 2014-12-04T09:52:51+01:00) 61 | 62 | # 2.14.11.09 63 | 64 | * Add links around `hoa://`. (Ivan Enderlin, 2014-09-26T10:44:17+02:00) 65 | 66 | # 2.14.09.23 67 | 68 | * Add `branch-alias`. (Stéphane PY, 2014-09-23T11:50:57+02:00) 69 | 70 | # 2.14.09.17 71 | 72 | * Drop PHP5.3. (Ivan Enderlin, 2014-09-17T17:15:07+02:00) 73 | * Add the installation section. (Ivan Enderlin, 2014-09-17T17:14:56+02:00) 74 | 75 | (first snapshot) 76 | -------------------------------------------------------------------------------- /Cli.php: -------------------------------------------------------------------------------- 1 | _parameters = new Zformat\Parameter( 76 | $this, 77 | [], 78 | [ 79 | 'rules.public' => [], 80 | 'rules.private' => [] 81 | ] 82 | ); 83 | $this->_parameters->setParameters($parameters); 84 | 85 | foreach ($this->_parameters->getParameter('rules.public') as $id => $rule) { 86 | @list($methods, $pattern, $call, $able, $variables) 87 | = $rule; 88 | 89 | if (null === $variables) { 90 | $variables = []; 91 | } 92 | 93 | $this->addRule($id, $methods, $pattern, $call, $able, $variables); 94 | } 95 | 96 | foreach ($this->_parameters->getParameter('rules.private') as $id => $rule) { 97 | @list($methods, $pattern, $call, $able, $variables) 98 | = $rule; 99 | 100 | if (null === $variables) { 101 | $variables = []; 102 | } 103 | 104 | $this->addPrivateRule( 105 | $id, $methods, $pattern, $call, $able, $variables 106 | ); 107 | } 108 | 109 | return; 110 | } 111 | 112 | /** 113 | * Get parameters. 114 | * 115 | * @return \Hoa\Zformat\Parameter 116 | */ 117 | public function getParameters() 118 | { 119 | return $this->_parameters; 120 | } 121 | 122 | /** 123 | * Fallback for add*Rule() methods. 124 | * 125 | * @param int $visibility Visibility (please, see 126 | * Router::VISIBILITY_* constants). 127 | * @param string $id ID. 128 | * @param array $methods HTTP methods allowed by the rule. 129 | * @param string $pattern Pattern (on-subdomain@on-request). 130 | * @param mixed $call Call (first part). 131 | * @param mixed $able Able (second part). 132 | * @param array $variables Variables (default or additional values). 133 | * @return \Hoa\Router\Http 134 | * @throws \Hoa\Router\Exception 135 | */ 136 | protected function _addRule( 137 | $visibility, 138 | $id, 139 | array $methods, 140 | $pattern, 141 | $call, $able, 142 | array $variables 143 | ) { 144 | if (true === $this->ruleExists($id)) { 145 | throw new Exception( 146 | 'Cannot add rule %s because it already exists.', 147 | 0, 148 | $id 149 | ); 150 | } 151 | 152 | array_walk($methods, function (&$method) { 153 | 154 | $method = strtolower($method); 155 | }); 156 | $diff = array_diff($methods, self::$_methods); 157 | 158 | if (!empty($diff)) { 159 | throw new Exception( 160 | (1 == count($diff) 161 | ? 'Method %s is' 162 | : 'Methods %s are') . 163 | ' invalid for the rule %s (valid methods are: %s).', 164 | 1, 165 | [ 166 | implode(', ', $diff), 167 | $id, 168 | implode(', ', self::$_methods) 169 | ] 170 | ); 171 | } 172 | 173 | $this->_rules[$id] = [ 174 | Router::RULE_VISIBILITY => $visibility, 175 | Router::RULE_ID => $id, 176 | Router::RULE_METHODS => $methods, 177 | Router::RULE_PATTERN => $pattern, 178 | Router::RULE_CALL => $call, 179 | Router::RULE_ABLE => $able, 180 | Router::RULE_VARIABLES => $variables 181 | ]; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Find the appropriated rule. 188 | * Special variables: _call, _able and _tail. 189 | * 190 | * @param string $uri URI. If null, it will be deduce. 191 | * @return \Hoa\Router\Cli 192 | * @throws \Hoa\Router\Exception\NotFound 193 | */ 194 | public function route($uri = null) 195 | { 196 | if (null === $uri) { 197 | $uri = static::getURI(); 198 | } 199 | 200 | $method = $this->getMethod(); 201 | $rules = array_filter( 202 | $this->getRules(), 203 | function ($rule) use (&$method) { 204 | if (Router::VISIBILITY_PUBLIC != $rule[Router::RULE_VISIBILITY]) { 205 | return false; 206 | } 207 | 208 | if (false === in_array($method, $rule[Router::RULE_METHODS])) { 209 | return false; 210 | } 211 | 212 | return true; 213 | } 214 | ); 215 | 216 | $gotcha = false; 217 | 218 | foreach ($rules as $rule) { 219 | $pattern = $rule[Router::RULE_PATTERN]; 220 | 221 | if (0 !== preg_match('#^' . $pattern . '$#i', $uri, $muri)) { 222 | $gotcha = true; 223 | 224 | break; 225 | } 226 | } 227 | 228 | if (false === $gotcha) { 229 | throw new Exception\NotFound( 230 | 'Cannot found an appropriated rule to route %s.', 231 | 2, 232 | $uri 233 | ); 234 | } 235 | 236 | array_shift($muri); 237 | $rule[Router::RULE_VARIABLES]['_method'] = 'get'; 238 | $rule[Router::RULE_VARIABLES]['_call'] = &$rule[Router::RULE_CALL]; 239 | $rule[Router::RULE_VARIABLES]['_able'] = &$rule[Router::RULE_ABLE]; 240 | 241 | $caseless = 0 === preg_match( 242 | '#\(\?\-[imsxUXJ]+\)#', 243 | $rule[Router::RULE_PATTERN] 244 | ); 245 | 246 | foreach ($muri as $key => $value) { 247 | if (!is_string($key)) { 248 | continue; 249 | } 250 | 251 | if (true === $caseless) { 252 | $key = mb_strtolower($key); 253 | } 254 | 255 | if (isset($rule[Router::RULE_VARIABLES][$key]) && empty($value)) { 256 | continue; 257 | } 258 | 259 | $rule[Router::RULE_VARIABLES][$key] = $value; 260 | } 261 | 262 | $this->_rule = $rule; 263 | 264 | return $this; 265 | } 266 | 267 | /** 268 | * Unroute a rule (i.e. route()^-1). 269 | * 270 | * @param string $id ID. 271 | * @param array $variables Variables. 272 | * @return string 273 | */ 274 | public function unroute($id, array $variables = []) 275 | { 276 | $rule = $this->getRule($id); 277 | $pattern = $rule[Router::RULE_PATTERN]; 278 | $variables = array_merge($rule[Router::RULE_VARIABLES], $variables); 279 | $out = preg_replace_callback( 280 | '#\(\?\<([^>]+)>[^\)]*\)[\?\*\+]{0,2}#', 281 | function (array $matches) use (&$variables) { 282 | $m = strtolower($matches[1]); 283 | 284 | if (!isset($variables[$m])) { 285 | return ''; 286 | } 287 | 288 | return $variables[$m]; 289 | }, 290 | preg_replace('#\(\?\-?[imsxUXJ]+\)#', '', $pattern) 291 | ); 292 | 293 | return str_replace( 294 | [ 295 | '\.', '\\\\', '\+', '\*', '\?', '\[', '\]', '\^', '\$', '\(', 296 | '\)', '\{', '\}', '\=', '\!', '\<', '\>', '\|', '\:', '\-' 297 | ], 298 | [ 299 | '.', '\\', '+', '*', '?', '[', ']', '^', '$', '(', 300 | ')', '{', '}', '=', '!', '<', '>', '|', ':', '-' 301 | ], 302 | $out 303 | ); 304 | } 305 | 306 | /** 307 | * Get HTTP method. 308 | * 309 | * @return string 310 | */ 311 | public function getMethod() 312 | { 313 | return 'get'; 314 | } 315 | 316 | /** 317 | * Whether the router is called asynchronously or not. 318 | * 319 | * @return bool 320 | */ 321 | public function isAsynchronous() 322 | { 323 | return false; 324 | } 325 | 326 | /** 327 | * Get URI. 328 | * 329 | * @return string 330 | */ 331 | public static function getURI() 332 | { 333 | if (!isset($_SERVER['argv'])) { 334 | return null; 335 | } 336 | 337 | $out = null; 338 | $_argv = $_SERVER['argv']; 339 | array_shift($_argv); 340 | 341 | foreach ($_argv as $arg) { 342 | if ('-' === $arg[0] && false !== strpos($arg, '=')) { 343 | if (false !== strpos($arg, '"')) { 344 | $arg = str_replace( 345 | '=', 346 | '="', 347 | str_replace('"', '\\"', $arg) 348 | ) . '"'; 349 | } elseif (false !== strpos($arg, '\'')) { 350 | $arg = str_replace( 351 | '=', 352 | '=\'', 353 | str_replace('\'', '\\\'', $arg) 354 | ) . '\''; 355 | } elseif (false !== strpos($arg, ' ')) { 356 | $arg = str_replace('=', '="', $arg) . '"'; 357 | } 358 | } elseif (false !== strpos($arg, ' ')) { 359 | $arg = '"' . str_replace('"', '\\"', $arg) . '"'; 360 | } elseif (false !== strpos($arg, '"')) { 361 | $arg = '"' . str_replace('"', '\\"', $arg) . '"'; 362 | } elseif (false !== strpos($arg, '\'')) { 363 | $arg = '"' . $arg . '"'; 364 | } 365 | 366 | $out .= ' ' . $arg; 367 | } 368 | 369 | return ltrim($out); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /Documentation/En/Index.xyl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

This chapter is not yet translated. Contributions are 7 | welcomed!

8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /Documentation/Fr/Index.xyl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Un programme doit gérer beaucoup de requêtes, et une tâche 7 | essentielle est de savoir les router, 8 | c'est à dire de savoir où les acheminer. C'est le rôle qui 9 | incombe à la bibliothèque Hoa\Router.

10 | 11 |

Table des matières

12 | 13 | 14 | 15 |

Introduction

16 | 17 |

Un routeur a une tâche assez simple : il reçoit une 18 | requête et il doit trouver où l'acheminer. 19 | Pour cela, il dispose d'une série de règles. Il cherche 20 | quelle règle parmi toutes celles dont il dispose correspond à la requête. Si 21 | une correspondance entre une requête et une règle existe, 22 | alors des données seront extraites de cette requête. Ces 23 | données peuvent être utilisées pour acheminer la requête quelque part. C'est 24 | le rôle de Hoa\Dispatcher, 25 | qui fonctionne de paire avec Hoa\Router.

26 |

Une règle doit être vue comme une succession de filtres. Si tous les 27 | filtres laissent passer la requête, alors la règle sera retenue pour acheminer 28 | la requête. Une requête est traitée de la façon suivante :

29 |
30 | 47 |

Tout d'abord, une règle a une visibilité qui contrôle la 48 | provenance de la requête. Il y a deux visibilités possibles : 49 | publique, qui est validée par les requêtes provenant de 50 | l'intérieur du programme comme de l'extérieur, et privée, qui 51 | n'est validée que par les requêtes provenant du programme lui-même. Par 52 | exemple, si la requête est extérieure au programme (typiquement, un client 53 | envoie une requête sur un serveur) et que la règle a une visibilité privée, 54 | elle ne sera pas retenue. En revanche, si une requête est interne au 55 | programme, une règle publique ou privée pourra être retenue. Ensuite, une 56 | règle définit des méthodes qui vérifient le type de la 57 | requête. Par exemple, dans le cas d'une requête HTTP, nous pouvons avoir la 58 | méthode GET : toutes les règles ayant au moins la méthode 59 | GET peuvent être retenues. Enfin, une règle impose un 60 | motif sous la forme d'une expression régulière (basée sur les 61 | PCRE). La requête doit correspondre à ce motif 62 | pour qu'elle soit retenue. Ce motif permet aussi d'extraire des données de la 63 | requête, ce qui pourra aider à son acheminement. Notons par ailleurs que 64 | toutes les règles portent un identifiant unique.

65 | 66 |

Écrire des règles

67 | 68 |

Nous avons vu qu'une règle est composée d'une visibilité, 69 | de méthodes et d'un motif. Mais nous savons 70 | également que lorsqu'une règle a été choisie, son motif sera utilisé pour 71 | extraire des données de la requête, qui seront ensuite placées dans des 72 | variables. Une règle est donc également composée de 73 | variables. Nous avons au total quatre éléments.

74 |

Nous trouvons deux méthodes pour ajouter des règles sur un routeur : 75 | Hoa\Router\Router::addRule pour ajouter une règle publique et 76 | Hoa\Router\Router::addPrivateRule pour ajouter une règle privée. 77 | Les deux méthodes ont la même en-tête :

78 |
    79 |
  • l'identifiant de la règle ;
  • 80 |
  • la liste non-ordonnée des méthodes acceptées par la 81 | règle ;
  • 82 |
  • le motif de la requête ;
  • 83 |
  • un callable ;
  • 84 |
  • des variables.
  • 85 |
86 |

Les deux derniers paramètres sont optionnels. Nous verrons par la suite que 87 | le callable est en fait une variable, ce qui réduit notre 88 | liste aux quatre éléments énoncés précédemment.

89 |

Prenons un exemple avec le routeur HTTP représenté par la classe 90 | Hoa\Router\Http :

91 |
$router = new Hoa\Router\Http();
 92 | $router->addRule('h', ['get'],         '/hello')
 93 |        ->addRule('l', ['get', 'post'], '/login');
94 |

Nous avons déclarés deux règles : h et l. La 95 | règle h n'est accessible qu'à travers la méthode HTTP 96 | GET et seule la requête (l'URI) /hello correspond. 97 | La règle l n'est accessible qu'à travers les méthodes HTTP 98 | GET et POST et seule la requête /login 99 | correspond. Toutes les deux sont des règles publiques.

100 |

Il existe un raccourci pour ajouter plus rapidement et plus facilement des 101 | règles. Nous pouvons ainsi écrire :

102 |
$router->get('h',      '/hello')
103 |        ->get_post('l', '/login');
104 |

La liste des méthodes peut être concaténées par le symbole 105 | « _ », puis utilisée comme nom de méthode sur le routeur. L'ordre 106 | des méthodes n'a toujours pas d'importance. Si nous voulons représenter toutes 107 | les méthodes, nous pourrons utiliser any :

108 |
$router->any(…);
109 |

Et enfin, pour représenter une règle privée, le nom devra commencer par le 110 | symbole « _ ». Ainsi, ces deux déclarations sont strictement 111 | équivalentes :

112 |
$router->addPrivateRule('f', ['get', 'post'], '/foobar');
113 | $router->_get_post('f', '/foobar');
114 |

Notons que nous pouvons supprimer à tout moment une règle avec la méthode 115 | Hoa\Router\Router::removeRule à laquelle nous passons 116 | l'identifiant d'une règle. Nous sommes également capable de vérifier qu'une 117 | règle existe avec la méthode Hoa\Router\Router::ruleExists et un 118 | identifiant de règle.

119 | 120 |

Router

121 | 122 |

Maintenant que nous avons des règles, voyons à laquelle 123 | correspond une requête. Pour cela, nous allons utiliser la 124 | méthode Hoa\Router\Router::route. L'en-tête de cette méthode 125 | dépend du routeur. Nous allons nous concentrer sur le router HTTP pour 126 | illustrer le concept, soit la méthode Hoa\Router\Http::route. Le 127 | premier argument est l'URI, soit notre requête (ce paramètre est optionnel 128 | mais nous le verrons plus tard). Nous allons chercher la règle associée à 129 | l'URI /hello. Si aucune exception 130 | Hoa\Router\Exception\NotFound n'est levée, alors nous pouvons 131 | appeler la méthode Hoa\Router\Router::getTheRule pour obtenir les 132 | informations sur la règle choisie ; voyons plutôt :

133 |
$router->route('/hello');
134 | print_r($router->getTheRule());
135 | 
136 | /**
137 |  * Will output:
138 |  *     Array
139 |  *     (
140 |  *         [0] => 0
141 |  *         [1] => h
142 |  *         [2] => Array
143 |  *             (
144 |  *                 [0] => get
145 |  *             )
146 |  *
147 |  *         [3] => /hello
148 |  *         [4] => 
149 |  *         [5] => 
150 |  *         [6] => Array
151 |  *             (
152 |  *                 [_uri] => hello
153 |  *                 [_method] => get
154 |  *                 [_domain] => 
155 |  *                 [_subdomain] => 
156 |  *                 [_call] => 
157 |  *                 [_able] => 
158 |  *                 [_request] => Array
159 |  *                     (
160 |  *                     )
161 |  *
162 |  *             )
163 |  *
164 |  *     )
165 |  */
166 |

Les indices du tableau sont donnés par les constantes sur 167 | Hoa\Router\Router suivantes :

168 |
    169 |
  • RULE_VISIBILITY, pour la visibilité de la 170 | règle (VISIBILITY_PUBLIC ou 171 | VISIBILITY_PRIVATE) ;
  • 172 |
  • RULE_ID, pour l'identifiant ;
  • 173 |
  • RULE_METHODS, pour les méthodes ;
  • 174 |
  • RULE_PATTERN, pour le motif ;
  • 175 |
  • RULE_CALL et RULE_ABLE, pour le 176 | callable ;
  • 177 |
  • RULE_VARIABLES, pour les variables.
  • 178 |
179 |

Ainsi, si nous voulons toutes les variables de la règle choisie, nous 180 | écrirons $theRule[$router::RULE_VARIABLES]. 181 | C'est aussi simple que ça.

182 |

L'exception Hoa\Router\Exception\NotFound signifie que la 183 | requête ne correspond à aucune règle. Par exemple :

184 |
try {
185 |     $router->route('/foobar');
186 | } catch (Hoa\Router\Exception\NotFound $e) {
187 |     echo $e->getMessage();
188 | }
189 | 
190 | /**
191 |  * Will output:
192 |  *     Cannot found an appropriated rule to route foobar.
193 |  */
194 |

Elle peut être levée à différentes étapes de la méthode 195 | Hoa\Router\Router::route, avec des messages différents.

196 | 197 |

Motifs et variables

198 | 199 |

Jusqu'à maintenant, les motifs utilisés dans nos exemples 200 | sont constants : il n'y a pas de plages de caractères non-définis, 201 | pas de captures etc. Les motifs sont écrits avec des expressions régulières de 202 | type PCRE, ce qui nous permet de définir des règles avec des parties 203 | partiellement définies. Par exemple :

204 |
$router->get('h', '/hello_(?&lt;who>\w+)');
205 |

Cela signifie que les requêtes correspondantes à la règle h 206 | sont de la forme /hello_word. La valeur de 207 | word sera placée dans la variable who. 208 | Voyons plutôt :

209 |
$router->route('/hello_gordon');
210 | $theRule = $router->getTheRule();
211 | print_r($theRule[$router::RULE_VARIABLES]);
212 | 
213 | /**
214 |  * Will output:
215 |  *     Array
216 |  *     (
217 |  *         [_uri] => hello_gordon
218 |  *         [_method] => get
219 |  *         [_domain] =>
220 |  *         [_subdomain] =>
221 |  *         [_call] =>
222 |  *         [_able] =>
223 |  *         [_request] => Array
224 |  *             (
225 |  *             )
226 |  *
227 |  *         [who] => gordon
228 |  *     )
229 |  */
230 |

Nous retrouvons notre variable who qui vaut 231 | gordon. Nous remarquons que le nom de certaines variables 232 | commence par le symbole « _ », comme _domain ou 233 | _request. Cela signifie que ce sont des variables déclarées par 234 | le routeur et non pas par l'utilisateur. 235 | Elles sont dites réservées. Chaque routeur a ses propres 236 | variables réservées. Notons que rien ne nous empêche d'utiliser leur nom dans 237 | une règle. Le routeur leur donne des valeurs « par défaut », c'est tout.

238 |

Nous avons extrait des données de la requête choisie. Le 239 | nombre de variables n'est pas limité. Ainsi :

240 |
$router->get('h', '/hello_(?&lt;who>\w+)(?&lt;format>\.[a-z]+)');
241 |

Avec /hello_gordon.html, la variable who sera 242 | égale à gordon et format sera égale à 243 | .html. Avec /hello_gordon.42, une exception 244 | Hoa\Router\Exception\NotFound sera levée car .42 245 | n'est pas reconnu par le motif \.[a-z]+ et la requêe ne 246 | correspond à aucune autre règle.

247 |

Nous l'aurons compris, le motif est une expression régulière classique et 248 | nous utilisons les sous-masques nommés 249 | pour définir le nom des variables à extraire. Nous nous servons de son pouvoir 250 | d'expression pour filtrer (ou valider) les requêtes finement.

251 |

Quand nous précisons des variables lors d'une déclaration de règle avec 252 | Hoa\Router\Router::addRule (ou sa sœur 253 | Hoa\Router\Router::addPrivateRule), il est possible de définir 254 | des valeurs par défaut pour les variables. Par exemple, si la 255 | partie format devient optionnelle, nous voudrions que sa valeur 256 | par défaut soit .txt :

257 |
$router->get(
258 |     'h',
259 |     '/hello_(?&lt;who>\w+)(?&lt;format>\.[a-z]+)?',
260 |     null,
261 |     null,
262 |     ['format' => '.txt']
263 | );
264 | 
265 | $router->route('/hello_gordon');
266 | $theRule = $router->getTheRule();
267 | var_dump($theRule[$router::RULE_VARIABLES]['format']);
268 | 
269 | /**
270 |  * Will output:
271 |  *     string(4) ".txt"
272 |  */
273 | 
274 | $router->route('/hello_gordon.html');
275 | $theRule = $router->getTheRule();
276 | var_dump($theRule[$router::RULE_VARIABLES]['format']);
277 | 
278 | /**
279 |  * Will output:
280 |  *     string(5) ".html"
281 |  */
282 |

Il est important de savoir que le routeur traite les requêtes et les règles 283 | sans tenir compte de la casse, c'est à dire de la différence 284 | entre majuscule et minuscule. D'ailleurs, les données extraites des variables 285 | sont passées en minuscules selon les routeurs (ce qui est le 286 | cas de Hoa\Router\Http par exemple). En effet, dans la plupart 287 | des situations, il est souhaitable que le routeur soit insensible à la casse. 288 | En revanche, il existe certains cas rares où la casse est importante. Par 289 | exemple avec un moteur de recherche où les mots-clés de la recherche sont 290 | contenus dans l'URI.

291 |

Les PCRE définissent les internal options permettant de 292 | changer les options d'une expression à la volée et à l'intérieur même d'une 293 | expression. Par exemple : les chaînes foo/bar/baz ou 294 | FOO/bAr/BaZ correspondent à l'expression 295 | #foo/bar/baz#i car l'option globale i rend 296 | l'expression entièrement insensible à la casse. Si nous voulons que seulement 297 | bar soit sensible à la casse, nous écrirons : 298 | #foo/(?-i)bar(?i)/baz#i. Alors FOO/bar/BaZ sera 299 | valide, tout comme foo/bar/baz mais pas FOO/bAr/BaZ. 300 | Les options internes supportées par Hoa\Router sont de la forme 301 | (?options) pour activer des options et 302 | (?-options) pour désactiver des options. Dès qu'une 303 | option interne désactive la casse, les données extraites de 304 | toutes les variables ne seront pas passées en minuscule si c'était le cas 305 | avant.

306 |

Par exemple, si nous voulons que tout ce qui suit /hello_ soit 307 | sensible à la casse, nous écrirons :

308 |
$router->get('h', '/hello_(?-i)(?&lt;who>\w+)');
309 | $router->route('/hello_GorDON');
310 | $theRule = $router->getTheRule();
311 | var_dump($theRule[$router::RULE_VARIABLES]['who']);
312 | 
313 | /**
314 |  * Will output:
315 |  *     string(6) "GorDON"
316 |  */
317 |

Il arrivera rarement que nous ayons besoin des options internes mais il est 318 | important de les comprendre.

319 | 320 |

Dérouter

321 | 322 |

L'opération inverse de 323 | Hoa\Router\Router::route est 324 | Hoa\Router\Router::unroute. Au minimum, il est demandé 325 | l'identifiant de la règle et une liste de 326 | variables. Cette méthode va construire une requête à partir 327 | d'une règle. Par exemple, nous aimerions produire la requête correspondante à 328 | la règle h avec comme valeur alyx pour 329 | who et rien pour le format (la valeur par défaut sera utilisée). 330 | Alors, nous écrirons :

331 |
var_dump($router->unroute('h', ['who' => 'alyx']));
332 | 
333 | /**
334 |  * Will output:
335 |  *     string(15) "/hello_alyx.txt"
336 |  */
337 |

Cela implique que les requêtes, liens et autres URI de ressources peuvent 338 | être abstraits à partir d'identifiants et de variables. La 339 | syntaxe finale peut changer à tout moment sans casser l'application. Par 340 | exemple, changeons la règle h pour :

341 |
$router->get('h', '/users/(?&lt;who>\w+)/hello(?&lt;format>\.[a-z]+)?');
342 | var_dump($router->unroute('h', ['who' => 'alyx']));
343 | 
344 | /**
345 |  * Will output:
346 |  *     string(21) "/users/alyx/hello.txt"
347 |  */
348 |

La souplesse d'un tel mécanisme permet de réduire considérablement la 349 | maintenance des applications ou d'augmenter leur modularité.

350 | 351 |

Informations sur les 352 | requêtes

353 | 354 |

Parmi les informations que nous retrouverons sur tous les routeurs, nous 355 | avons :

356 |
    357 |
  • Hoa\Router\Router::getMethod pour connaître la 358 | méthode détectée par le routeur ;
  • 359 |
  • Hoa\Router\Router::isAsynchronous pour connaître le type de 360 | communication détectée par le routeur (synchrone ou 361 | asynchrone).
  • 362 |
363 |

Certains routeurs exposent d'autres informations, mais celles-ci sont 364 | standards.

365 | 366 |

Routeur HTTP

367 | 368 |

Passons maintenant aux spécificités des routeurs en commençant par le 369 | routeur HTTP, représenté par la classe 370 | Hoa\Router\Http.

371 |

Les méthodes supportées par le routeur sont un 372 | sous-ensemble des méthodes HTTP. Nous comptons : GET, 373 | POST, PUT, PATCH, DELETE, 374 | HEAD et OPTIONS. Les variables 375 | réservées pour la méthode Hoa\Router\Http::route sont :

376 |
    377 |
  • _uri, l'adresse ;
  • 378 |
  • _method, la méthode HTTP ;
  • 379 |
  • _domain, le domaine (de la forme 380 | domain.tld) ;
  • 381 |
  • _subdomain, le sous-domaine (que nous 382 | allons détailler) ;
  • 383 |
  • _call et _able, le 384 | callable ;
  • 385 |
  • _request, la partie requête de l'URI, soit 386 | le contenu de la variable 387 | $_REQUEST.
  • 388 |
389 |

Quand nous voudrons router une requête avec la méthode 390 | Hoa\Router\Http::route, nous allons travailler sur deux données : 391 | l'URI et son préfixe. L'URI est à comprendre 392 | au sens HTTP, c'est le chemin vers une ressource. Par 393 | exemple, considérons la requête HTTP suivante :

394 |
GET /Foo/Bar.html
395 |

Ici, l'URI est /Foo/Bar.html (et la méthode est 396 | GET). Le nom de domaine n'est jamais considéré, tout comme le 397 | port. Si l'URI est manquante, la méthode statique 398 | Hoa\Router\Http::getURI sera appelée.

399 |

Le préfixe de l'URI permet de préciser quelle partie au 400 | début de l'URI ne devra pas être considérée durant l'analyse. 401 | Imaginons que votre application soit accessible depuis l'URI 402 | /Forum/ ; une URI peut alors être : 403 | /Forum/Help/Newpost.html. Le routeur n'est intéressé que par la 404 | partie /Help/Newpost.html. Dans ce cas, le préfixe est 405 | Forum (les slashes avant et après n'ont pas 406 | d'importance). Ainsi :

407 |
$router->route('/Forum/Help/Newpost.html', 'Forum');
408 |

Toutefois, nous pouvons définir un préfixe pour toutes les requêtes, avec 409 | la méthode Hoa\Router\Http::setPrefix :

410 |
$router->setPrefix('Forum');
411 | $router->route('/Forum/Help/Newpost.html');
412 |

Pour obtenir le préfixe, nous pouvons utiliser la méthode 413 | Hoa\Router\Http::getPrefix. Notons qu'avec la plupart des 414 | serveurs HTTP, Hoa\Router\Http sait détecter 415 | automatiquement le préfixe, vous n'aurez donc pas à vous 416 | soucier de cette problématique.

417 |

Nous avons également les méthodes Hoa\Router\Http::getPort 418 | pour obtenir le port et Hoa\Router\Http::isSecure pour savoir si 419 | la connexion est sécurisée ou pas.

420 | 421 |

Sous-domaines

422 | 423 |

La classe Hoa\Router\Http sait également router les 424 | sous-domaines. Commençons par les méthodes auxquelles nous 425 | avons accès :

426 |
    427 |
  • Hoa\Router\Http::getDomain pour avoir le domaine en 428 | entier (c'est à dire avec les sous-domaines) sans le 429 | port ;
  • 430 |
  • Hoa\Router\Http::getStrictDomain pour avoir 431 | uniquement le domaine, sans les sous-domaines ;
  • 432 |
  • Hoa\Router\Http::getSubdomain pour avoir 433 | uniquement les sous-domaines.
  • 434 |
435 |

Si nous accédons à l'hôte (au serveur) à travers une IP, les méthodes 436 | Hoa\Router\Http::getDomain et 437 | Hoa\Router\Http::getStrictDomain nous retourneront cette IP (sans 438 | le port, encore une fois). Prenons un exemple avec le domaine 439 | sub2.sub1.domain.tld :

440 |
var_dump(
441 |     $router->getDomain(),
442 |     $router->getStrictDomain(),
443 |     $router->getSubdomain()
444 | );
445 | 
446 | /**
447 |  * Will output:
448 |  *     string(20) "sub2.sub1.domain.tld"
449 |  *     string(10) "domain.tld"
450 |  *     string(9) "sub2.sub1"
451 |  */
452 |

À l'instar des préfixes pour les URI, Hoa\Router\Http 453 | ajoute la notion de suffixe sur les sous-domaines, 454 | c'est à dire une partie à ne pas considérer durant l'analyse, mais cette fois, 455 | c'est la fin. Imaginons que votre application soit accessible 456 | depuis le domaine app.domain.tld et que nous aimerions que le 457 | routeur reconnaisse les sous-domaines 458 | user.app.domain.tld. Dans ce cas, le suffixe est 459 | app. Nous utiliserons la méthode 460 | Hoa\Router\Http::setSubdomainSuffix pour définir ce suffixe. La 461 | méthode Hoa\Router\Http::getSubdomain retournera par défaut tous 462 | les sous-domaines, suffixe inclu. Nous devons lui donner false en 463 | seul argument pour ne pas avoir le suffixe. Prenons un exemple avec le domaine 464 | gordon.app.domain.tld :

465 |
$router->setSubdomainSuffix('app');
466 | var_dump(
467 |     $router->getSubdomain(),
468 |     $router->getSubdomain(false)
469 | );
470 | 
471 | /**
472 |  * Will output:
473 |  *     string(10) "gordon.app"
474 |  *     string(6) "gordon"
475 |  */
476 | 
477 |

Bien. Maintenant voyons comment dire à une règle de 478 | travailler sur les sous-domaines. Une règle est en réalité constituée de 479 | deux expressions régulières, concaténées par 480 | le symbole @ (arobase), c'est à dire 481 | [subdomain@]URI, avec la première partie qui 482 | est optionnelle. Si nous voulons reconnaître les sous-domaines de la forme 483 | user.domain.tld et les URI de la forme 484 | /Project/project.html, nous écrirons la règle 485 | p suivante (accessible avec la méthode GET 486 | uniquement) :

487 |
$router->get('p', '(?&lt;user>.*)@/Project/(?&lt;project>[^\.]+)\.html');
488 |

Si nous essayons de router la requête 489 | gordon.domain.tld/Project/Space-biker.html, nous obtiendrons 490 | ceci :

491 |
$router->route();
492 | print_r($router->getTheRule());
493 | 
494 | /**
495 |  * Will output:
496 |  *     Array
497 |  *     (
498 |  *         [0] => 0
499 |  *         [1] => p
500 |  *         [2] => Array
501 |  *             (
502 |  *                 [0] => get
503 |  *             )
504 |  *
505 |  *         [3] => (?&lt;user>.*)@/Project/(?&lt;project>[^\.]+)\.html
506 |  *         [4] => 
507 |  *         [5] => 
508 |  *         [6] => Array
509 |  *             (
510 |  *                 [_uri] =>  gordon.domain.tld/Project/Space-biker.html
511 |  *                 [_method] => get
512 |  *                 [_domain] => gordon.domain.tld
513 |  *                 [_subdomain] => gordon
514 |  *                 [_call] => 
515 |  *                 [_able] => 
516 |  *                 [_request] => Array
517 |  *                     (
518 |  *                     )
519 |  *
520 |  *                 [project] => space-biker
521 |  *                 [user] => gordon
522 |  *             )
523 |  *
524 |  *     )
525 |  */
526 |

Nous voyons bien nos deux variables : user et 527 | project respectivement définies à gordon et 528 | space-biker ! Nous retrouvons aussi le sous-domaine dans la 529 | variable réservée _subdomain, comme nous retrouvons également le 530 | domaine dans la variable réservée _domain.

531 |

Maintenant passons à l'opération inverse : dérouter. Nous 532 | utilisons la règle p et nous voulons construire la requête 533 | gordon.domain.tld/Project/Space-biker.html. Il n'y aura aucune 534 | différence avec ce que nous avons vu précédemment :

535 |
var_dump(
536 |     $router->unroute(
537 |         'p',
538 |         [
539 |             'user'    => 'gordon',
540 |             'project' => 'Space-biker'
541 |         ]
542 |     )
543 | );
544 | 
545 | /**
546 |  * Will output:
547 |  *     string(49) "http://gordon.domain.tld/Project/Space-biker.html"
548 |  */
549 |

La méthode Hoa\Router\Http::unroute a deux 550 | variables réservées. Nous allons nous intéresser à la 551 | première : _subdomain. Elle permet de définir la valeur du 552 | sous-domaine, elle écrasera complètement le sous-domaine mais 553 | le suffixe sera tout de même ajouté. Ainsi :

554 |
var_dump(
555 |     $router->unroute(
556 |         'p',
557 |         [
558 |             'project'    => 'Space-biker',
559 |             '_subdomain' => 'my-subdomain'
560 |         ]
561 |     )
562 | );
563 | 
564 | /**
565 |  * Will output:
566 |  *     string(55) "http://my-subdomain.domain.tld/Project/Space-biker.html"
567 |  */
568 |

Nous voyons que le sous-domaine est bien forcé à une valeur précise. La 569 | variable réservée _subdomain peut avoir comme valeur trois 570 | mots-clés, chacun étant associé à une opération sur les 571 | sous-domaines :

572 |
    573 |
  • __self__ représente tous les 574 | sous-domaines, suffixe compris ;
  • 575 |
  • __root__ supprime les sous-domaines, la 576 | requête n'aura que le suffixe et le domaine ;
  • 577 |
  • __shift__ permet de supprimer un 578 | sous-domaine (à gauche donc). Nous pouvons répéter cette opération 579 | n fois en écrivant __shift__ * n. Le suffixe 580 | sera toujours ajouté.
  • 581 |
582 |

Prenons des exemples, ce sera plus simple. Nous sommes sur le domaine 583 | sub3.sub2.sub1.domain.tld :

584 |
$router->get('s', '(?&lt;three>[^\.]+)\.(?&lt;two>[^\.]+)\.(?&lt;one>.+)@');
585 | var_dump(
586 |     // Normal.
587 |     $router->unroute('s', ['three' => 'foo', 'two' => 'bar', 'one' => 'baz']),
588 | 
589 |     // Force.
590 |     $router->unroute('s', ['_subdomain' => 'my-subdomain']),
591 | 
592 |     // Current subdomain.
593 |     $router->unroute('s', ['_subdomain' => '__self__']),
594 | 
595 |     // No subdomain.
596 |     $router->unroute('s', ['_subdomain' => '__root__']),
597 | 
598 |     // Shift only sub3.
599 |     $router->unroute('s', ['_subdomain' => '__shift__']),
600 | 
601 |     // Shift two sub-domains.
602 |     $router->unroute('s', ['_subdomain' => '__shift__ * 2'])
603 | );
604 | 
605 | /**
606 |  * Will output:
607 |  *     string(29) "http://foo.bar.baz.domain.tld"
608 |  *     string(30) "http://my-subdomain.domain.tld"
609 |  *     string(32) "http://sub3.sub2.sub1.domain.tld"
610 |  *     string(17) "http://domain.tld"
611 |  *     string(27) "http://sub2.sub1.domain.tld"
612 |  *     string(22) "http://sub1.domain.tld"
613 |  */
614 |

Notons que le symbole @ est présent à la fin de la règle. Ce 615 | serait une erreur de l'oublier, la règle s'appliquerait sur l'URI et non pas 616 | sur les sous-domaines.

617 |

Ces trois-mots clés nous permettent de faire face aux situations les plus 618 | courantes avec les sous-domaines. En effet, il arrive 619 | fréquemment de vouloir remonter d'un sous-domaine, ou de 620 | retourner à la racine directement, tout en conservant 621 | l'abstraction offerte par le routeur.

622 | 623 |

Fragments

624 | 625 |

Nous avons parlé de deux variables réservées pour la méthode 626 | Hoa\Router\Http::unroute. Nous avons évoqué 627 | _subdomain et c'est maintenant le tour de _fragment. 628 | Cette variable réservée permet de définir le fragment d'une 629 | URI, c'est à dire la partie après le symbole # (dièse). Par 630 | exemple dans /Project/Space-biker.html#Introduction, le fragment 631 | est Introduction. Ainsi :

632 |
var_dump(
633 |     $router->unroute(
634 |         'p',
635 |         [
636 |             'user'      => 'gordon',
637 |             'project'   => 'Space-biker',
638 |             '_fragment' => 'Introduction'
639 |         ]
640 |     )
641 | );
642 | 
643 | /**
644 |  * Will output:
645 |  *     string(62) "http://gordon.domain.tld/Project/Space-biker.html#Introduction"
646 |  */
647 | 648 | 649 | 650 |

Ports

651 | 652 |

Les ports HTTP par défaut sont 80 pour une connexion 653 | non-cryptée et 443 pour une connexion 654 | cryptée (avec TLS). Pour obtenir ces valeurs de ports, nous 655 | pouvons utiliser la méthode Hoa\Router\Http::getDefaultPort. 656 | Naturellement, nous aurons le port par défaut pour une connexion non-cryptée. 657 | En revanche, si nous donnons Hoa\Router\Http::SECURE en seul 658 | argument, nous aurons le port par défaut pour une connexion cryptée. 659 | Ainsi :

660 |
var_dump($router->getDefaultPort());
661 | 
662 | /**
663 |  * Will output:
664 |  *     int(80)
665 |  */
666 |

La valeur des ports par défaut se met à jour toute seule. 667 | Par exemple, si les requêtes arrivent sur une connexion non-cryptée à travers 668 | le port 8880, Hoa\Router\Http changera 80 par 8880 669 | automatiquement. Toutefois, pour modifier les ports par défaut manuellement, 670 | nous utiliserons la méthode Hoa\Router\Http::setDefaultPort, avec 671 | en premier argument la valeur du port et en second argument l'une des deux 672 | constantes : Hoa\Router\Http::SECURE ou 673 | Hoa\Router\Http::UNSECURE, indiquant si c'est pour une connexion 674 | cryptée ou pas.

675 |

Ces numéros de port par défaut sont importants quand nous appelons la 676 | méthode Hoa\Router\Http::unroute et qu'un domaine avec un port 677 | doit être reconstitué. C'est le cas, par exemple, si nous forçons à dérouter 678 | vers une connexion cryptée, à l'aide du troisième argument de cette méthode en 679 | lui donnant Hoa\Router\Http::SECURE :

680 |
var_dump(
681 |     $router->unroute(
682 |         'p',
683 |         ['user' => 'gordon', 'project' => 'Space-biker'],
684 |         true
685 |     )
686 | );
687 | $router->setDefaultPort(8443, Hoa\Router\Http::SECURE);
688 | var_dump(
689 |     $router->unroute(
690 |         …
691 |     )
692 | );
693 | 
694 | /**
695 |  * Will output:
696 |  *     string(50) "https://gordon.domain.tld/Project/Space-biker.html"
697 |  *     string(55) "https://gordon.domain.tld:8443/Project/Space-biker.html"
698 |  */
699 |

Nous remarquons que le protocole HTTPS est utilisé. Dans le premier cas, le 700 | port n'est pas affiché car sa valeur par défaut est 443 et c'est un standard. 701 | En revanche, quand nous modifions le port par défaut pour 8443, le port est 702 | bien affiché.

703 | 704 |

Routeur CLI

705 | 706 |

Le routeur CLI permet de manipuler des requêtes dans un 707 | terminal. Il est représenté par la classe 708 | Hoa\Router\Cli.

709 |

Ce routeur ne supporte qu'une seule méthode : 710 | GET. Les variables réservées pour la méthode 711 | Hoa\Router\Cli::route sont :

712 |
    713 |
  • _call et _able, le 714 | callable ;
  • 715 |
  • _tail, contient les options et les 716 | entrées d'une ligne de commande (optionnelle).
  • 717 |
718 |

La méthode Hoa\Router\Cli::route n'a qu'un seul argument : 719 | l'URI ; par exemple avec :

720 |
$ command --option value input
721 |

Dans ce contexte, l'URI est la ligne de commande, ou plus précisément, ce 722 | qui suit la commande, sans distinction ; soit --option value 723 | input. Nous pouvons nous en rendre compte en appelant la méthode 724 | statique Hoa\Router\Cli::getURI. Il n'y pas de notion de préfixe 725 | comme pour Hoa\Router\Http.

726 |

Prenons un exemple très courant qui consiste à avoir une ligne de commande 727 | la forme command group:subcommand 728 | options. Nous écrirons la règle g suivante dans 729 | le fichier Router.php :

730 |
$router = new Hoa\Router\Cli();
731 | $router->get(
732 |     'g',
733 |     '(?&lt;group>\w+):(?&lt;subcommand>\w+)(?&lt;_tail>.*?)'
734 | );
735 | 
736 | $router->route();
737 | $theRule = $router->getTheRule();
738 | print_r($theRule[$router::RULE_VARIABLES]);
739 |

Nous pouvons exécuter le fichier de la façon suivante :

740 |
$ php Router.php foo:bar baz
741 | Array
742 | (
743 |     [group] => foo
744 |     [subcommand] => bar
745 |     [_call] => 
746 |     [_able] => 
747 |     [_tail] =>  baz
748 | )
749 |

La variable _tail a une signification particulière. Il faut 750 | savoir que nous nous en servons pour capturer les options et les entrées d'une 751 | ligne de commande afin de les analyser avec 752 | Hoa\Console 753 | par la suite.

754 |

Nous pouvons avoir envie que group soit 755 | optionnel (avec default comme valeur par 756 | défaut), tout comme subcommand (avec la même valeur par 757 | défaut). Dans ce cas, la règle deviendrait :

758 |
$router->get(
759 |     'g',
760 |     '(?&lt;group>\w+)?(:(?&lt;subcommand>\w+))?(?&lt;_tail>.*?)',
761 |     null,
762 |     null,
763 |     [
764 |         'group'      => 'default',
765 |         'subcommand' => 'default'
766 |     ]
767 | );
768 |

Ainsi, nous pourrions avoir group, 769 | group:subcommand ou :subcommand. 770 | Testons avec subcommand absent dans un premier temps, 771 | puis avec group absent dans un second temps :

772 |
$ php Router.php foo baz
773 | Array
774 | (
775 |     [group] => foo
776 |     [subcommand] => default
777 |     [_call] =>
778 |     [_able] =>
779 |     [_tail] =>  baz
780 | )
781 | $ php Router.php :baz baz
782 | Array
783 | (
784 |     [group] => default
785 |     [subcommand] => bar
786 |     [_call] =>
787 |     [_able] =>
788 |     [_tail] =>  baz
789 | )
790 | 
791 |

Rien de plus simple avec Hoa\Router\Cli. Nous pouvons toujours 792 | regarder le script hoa (dans 793 | hoa://Library/Cli/Bin/Hoa.php) 794 | pour avoir un exemple concret.

795 | 796 |

Continuer avec un 797 | dispatcheur

798 | 799 |

Lorsque le routeur reçoit une requête et qu'il trouve une règle qui la 800 | reconnaît, il pourra en extraire des données. C'est ce que fait 801 | Hoa\Router\Router::route. Nous obtenons les informations de la 802 | règle avec Hoa\Router\Router::getTheRule. Nous remarquons que 803 | tous les routeurs ont les variables réservées 804 | _call et _able : elles permettent de définir un 805 | callable. Ces variables peuvent être alors 806 | réutilisées pour dispatcher la requête. C'est exactement le 807 | rôle de Hoa\Dispatcher et 808 | c'est la suite logique de Hoa\Router.

809 | 810 |

Conclusion

811 | 812 |

La bibliothèque Hoa\Router offre une 813 | interface et un fonctionnement commun à des 814 | routeurs. Un routeur est capable de reconnaître une requête à 815 | partir de plusieurs règles et d'en extraire des 816 | données. Actuellement, deux routeurs sont proposés : 817 | Hoa\Router\Http et Hoa\Router\Cli.

818 |
819 |
820 | -------------------------------------------------------------------------------- /Exception/Exception.php: -------------------------------------------------------------------------------- 1 | _addRule( 117 | Router::VISIBILITY_PUBLIC, 118 | $id, 119 | $methods, 120 | $pattern, 121 | $call, 122 | $able, 123 | $variables 124 | ); 125 | } 126 | 127 | /** 128 | * Add a private rule. 129 | * 130 | * @param string $id ID. 131 | * @param array $methods HTTP methods allowed by the rule. 132 | * @param string $pattern Pattern (on-subdomain@on-request). 133 | * @param mixed $call Call (first part). 134 | * @param mixed $able Able (second part). 135 | * @param array $variables Variables (default or additional values). 136 | * @return \Hoa\Router\Generic 137 | * @throws \Hoa\Router\Exception 138 | */ 139 | public function addPrivateRule( 140 | $id, 141 | array $methods, 142 | $pattern, 143 | $call = null, 144 | $able = null, 145 | array $variables = [] 146 | ) { 147 | return $this->_addRule( 148 | Router::VISIBILITY_PRIVATE, 149 | $id, 150 | $methods, 151 | $pattern, 152 | $call, 153 | $able, 154 | $variables 155 | ); 156 | } 157 | 158 | /** 159 | * Helper for adding rules. 160 | * Methods are concatenated by _. If prefixed by _, it's a private rule. In 161 | * addition, the keyword “any” takes place for all methods. 162 | * Examples: 163 | * get(…) : addRule(…, array('get'), …); 164 | * get_post(…) : addRule(…, array('get', 'post'), …); 165 | * post_get(…) : same that above; 166 | * _get(…) : addPrivateRule(…, array('get'), …); 167 | * any(…) : addRule(…, array(), …); 168 | * head_delete(…): addRule(…, array('head', 'delete'), …). 169 | * 170 | * @param string $name Please, see API documentation. 171 | * @param array $arguments Arguments for add*Rule() methods. 172 | * @return \Hoa\Router\Generic 173 | * @throws \Hoa\Router\Exception 174 | */ 175 | public function __call($name, $arguments) 176 | { 177 | if ('_' === $name[0]) { 178 | $name = substr($name, 1); 179 | $method = 'addPrivateRule'; 180 | } else { 181 | $method = 'addRule'; 182 | } 183 | 184 | if ('any' === $name) { 185 | array_unshift($arguments, static::$_methods); 186 | } else { 187 | array_unshift($arguments, explode('_', $name)); 188 | } 189 | 190 | $handle = $arguments[0]; 191 | $arguments[0] = $arguments[1]; 192 | $arguments[1] = $handle; 193 | 194 | return call_user_func_array([$this, $method], $arguments); 195 | } 196 | 197 | /** 198 | * Remove a rule. 199 | * 200 | * @param string $id ID. 201 | * @return void 202 | */ 203 | public function removeRule($id) 204 | { 205 | unset($this->_rules[$id]); 206 | 207 | return; 208 | } 209 | 210 | /** 211 | * Check whether a rule exists. 212 | * 213 | * @param string $id ID. 214 | * @return bool 215 | */ 216 | public function ruleExists($id) 217 | { 218 | return isset($this->_rules[$id]); 219 | } 220 | 221 | /** 222 | * Get all rules. 223 | * 224 | * @return array 225 | */ 226 | public function getRules() 227 | { 228 | return $this->_rules; 229 | } 230 | 231 | /** 232 | * Get a specific rule. 233 | * 234 | * @param string $id ID. 235 | * @return array 236 | * @throws \Hoa\Router\Exception 237 | */ 238 | public function getRule($id) 239 | { 240 | if (false === $this->ruleExists($id)) { 241 | throw new Exception('Rule %s does not exist.', 0, $id); 242 | } 243 | 244 | return $this->_rules[$id]; 245 | } 246 | 247 | /** 248 | * Get the selected rule after routing. 249 | * 250 | * @return array 251 | */ 252 | public function &getTheRule() 253 | { 254 | return $this->_rule; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Http/Dav.php: -------------------------------------------------------------------------------- 1 | _parameters = new Zformat\Parameter( 141 | $this, 142 | [], 143 | [ 144 | 'prefix' => null, 145 | 'rules.public' => [], 146 | 'rules.private' => [] 147 | ] 148 | ); 149 | $this->_parameters->setParameters($parameters); 150 | 151 | if (null === $prefix = $this->_parameters->getParameter('prefix')) { 152 | $this->setPrefix( 153 | ('\\' === $_ = dirname($this->getBootstrap())) ? '/' : $_ 154 | ); 155 | } else { 156 | $this->setPrefix($prefix); 157 | } 158 | 159 | foreach ($this->_parameters->getParameter('rules.public') as $id => $rule) { 160 | @list($methods, $pattern, $call, $able, $variables) 161 | = $rule; 162 | 163 | if (null === $variables) { 164 | $variables = []; 165 | } 166 | 167 | $this->addRule($id, $methods, $pattern, $call, $able, $variables); 168 | } 169 | 170 | foreach ($this->_parameters->getParameter('rules.private') as $id => $rule) { 171 | @list($methods, $pattern, $call, $able, $variables) 172 | = $rule; 173 | 174 | if (null === $variables) { 175 | $variables = []; 176 | } 177 | 178 | $this->addPrivateRule( 179 | $id, $methods, $pattern, $call, $able, $variables 180 | ); 181 | } 182 | 183 | $this->setDefaultPort(static::getPort(), static::isSecure()); 184 | 185 | return; 186 | } 187 | 188 | /** 189 | * Get parameters. 190 | * 191 | * @return \Hoa\Zformat\Parameter 192 | */ 193 | public function getParameters() 194 | { 195 | return $this->_parameters; 196 | } 197 | 198 | /** 199 | * Fallback for add*Rule() methods. 200 | * 201 | * @param int $visibility Visibility (please, see 202 | * Router::VISIBILITY_* constants). 203 | * @param string $id ID. 204 | * @param array $methods HTTP methods allowed by the rule. 205 | * @param string $pattern Pattern (on-subdomain@on-request). 206 | * @param mixed $call Call (first part). 207 | * @param mixed $able Able (second part). 208 | * @param array $variables Variables (default or additional values). 209 | * @return \Hoa\Router\Http 210 | * @throws \Hoa\Router\Exception 211 | */ 212 | protected function _addRule( 213 | $visibility, 214 | $id, 215 | array $methods, 216 | $pattern, 217 | $call, 218 | $able, 219 | array $variables 220 | ) { 221 | if (true === $this->ruleExists($id)) { 222 | throw new Router\Exception( 223 | 'Cannot add rule %s because it already exists.', 224 | 0, 225 | $id 226 | ); 227 | } 228 | 229 | array_walk($methods, function (&$method) { 230 | $method = strtolower($method); 231 | }); 232 | $diff = array_diff($methods, static::$_methods); 233 | 234 | if (!empty($diff)) { 235 | throw new Router\Exception( 236 | (1 == count($diff) 237 | ? 'Method %s is' 238 | : 'Methods %s are') . 239 | ' invalid for the rule %s (valid methods are: %s).', 240 | 1, 241 | [ 242 | implode(', ', $diff), 243 | $id, 244 | implode(', ', static::$_methods) 245 | ] 246 | ); 247 | } 248 | 249 | if (_static === $this->_subdomainStack && 250 | false !== strpos($pattern, '@')) { 251 | $this->_subdomainStack = _dynamic; 252 | } 253 | 254 | $this->_rules[$id] = [ 255 | Router::RULE_VISIBILITY => $visibility, 256 | Router::RULE_ID => $id, 257 | Router::RULE_METHODS => $methods, 258 | Router::RULE_PATTERN => $pattern, 259 | Router::RULE_CALL => $call, 260 | Router::RULE_ABLE => $able, 261 | Router::RULE_VARIABLES => $variables 262 | ]; 263 | 264 | return $this; 265 | } 266 | 267 | /** 268 | * Find the appropriated rule. 269 | * Special variables: _domain, _subdomain, _call, _able and _request. 270 | * 271 | * @param string $uri URI. If null, it will be deduced. Can contain 272 | * subdomain. 273 | * @param string $prefix Path prefix. If null, it will be deduced. 274 | * @return \Hoa\Router\Http 275 | * @throws \Hoa\Router\Exception\NotFound 276 | */ 277 | public function route($uri = null, $prefix = null) 278 | { 279 | if (null === $uri) { 280 | $uri = static::getURI(); 281 | $subdomain = $this->getSubdomain(); 282 | } else { 283 | if (false !== $pos = strpos($uri, '@')) { 284 | list($subdomain, $uri) = explode('@', $uri, 2); 285 | } else { 286 | $subdomain = $this->getSubdomain(); 287 | } 288 | 289 | $uri = ltrim(urldecode($uri), '/'); 290 | } 291 | 292 | if (null === $prefix) { 293 | $prefix = $this->getPrefix(); 294 | } 295 | 296 | if (!empty($prefix)) { 297 | $prefix = ltrim($prefix, '/'); 298 | 299 | if (0 === preg_match('#^' . $prefix . '(.*)?$#', $uri, $matches)) { 300 | throw new Router\Exception\NotFound( 301 | 'Cannot match the path prefix %s in the URI %s.', 302 | 2, 303 | [$prefix, $uri] 304 | ); 305 | } 306 | 307 | $uri = ltrim($matches[1], '/'); 308 | } 309 | 310 | // Please, see http://php.net/language.variables.external, section “Dots 311 | // in incoming variable names”. 312 | unset($_REQUEST[$_uri = str_replace('.', '_', $uri)]); 313 | unset($_GET[$_uri]); 314 | 315 | $method = $this->getMethod(); 316 | $subdomainStack = $this->getSubdomainStack(); 317 | $subdomainSuffix = $this->getSubdomainSuffix(); 318 | 319 | if (null !== $subdomainSuffix) { 320 | $subdomainSuffix = '\.' . $subdomainSuffix; 321 | } 322 | 323 | $rules = array_filter( 324 | $this->getRules(), 325 | function ($rule) use ( 326 | &$method, 327 | &$subdomain, 328 | &$subdomainStack, 329 | &$subdomainSuffix 330 | ) { 331 | if (Router::VISIBILITY_PUBLIC != $rule[Router::RULE_VISIBILITY]) { 332 | return false; 333 | } 334 | 335 | if (false === in_array($method, $rule[Router::RULE_METHODS])) { 336 | return false; 337 | } 338 | 339 | if (false !== $pos = strpos($rule[Router::RULE_PATTERN], '@')) { 340 | if (empty($subdomain)) { 341 | return false; 342 | } else { 343 | return 0 !== preg_match( 344 | '#^' . 345 | substr($rule[Router::RULE_PATTERN], 0, $pos) . 346 | $subdomainSuffix . 347 | '$#i', 348 | $subdomain 349 | ); 350 | } 351 | } 352 | 353 | return 354 | _dynamic == $subdomainStack 355 | ? empty($subdomain) 356 | : true; 357 | } 358 | ); 359 | 360 | if (empty($rules)) { 361 | throw new Router\Exception\NotFound( 362 | 'No rule to apply to route %s.', 363 | 3, 364 | $uri 365 | ); 366 | } 367 | 368 | $gotcha = false; 369 | 370 | foreach ($rules as $rule) { 371 | $pattern = $rule[Router::RULE_PATTERN]; 372 | 373 | if (false !== $pos = strpos($pattern, '@')) { 374 | $pattern = substr($pattern, $pos + 1); 375 | } 376 | 377 | $pattern = ltrim($pattern, '/'); 378 | 379 | if (0 !== preg_match('#^' . $pattern . '$#i', $uri, $muri)) { 380 | $gotcha = true; 381 | 382 | break; 383 | } 384 | } 385 | 386 | if (false === $gotcha) { 387 | throw new Router\Exception\NotFound( 388 | 'Cannot found an appropriated rule to route %s.', 389 | 4, 390 | $uri 391 | ); 392 | } 393 | 394 | if (false !== $pos) { 395 | preg_match( 396 | '#^' . 397 | substr($rule[Router::RULE_PATTERN], 0, $pos) . 398 | $subdomainSuffix . 399 | '$#i', 400 | $subdomain, 401 | $msubdomain 402 | ); 403 | } else { 404 | $msubdomain = []; 405 | } 406 | 407 | array_shift($muri); 408 | $sub = array_shift($msubdomain) ?: null; 409 | $rule[Router::RULE_VARIABLES]['_uri'] = $uri; 410 | $rule[Router::RULE_VARIABLES]['_method'] = $method; 411 | $rule[Router::RULE_VARIABLES]['_domain'] = static::getDomain(); 412 | $rule[Router::RULE_VARIABLES]['_subdomain'] = $sub; 413 | $rule[Router::RULE_VARIABLES]['_call'] = &$rule[Router::RULE_CALL]; 414 | $rule[Router::RULE_VARIABLES]['_able'] = &$rule[Router::RULE_ABLE]; 415 | $rule[Router::RULE_VARIABLES]['_request'] = $_REQUEST; 416 | 417 | $caseless = 0 === preg_match( 418 | '#\(\?\-[imsxUXJ]+\)#', 419 | $rule[Router::RULE_PATTERN] 420 | ); 421 | 422 | foreach (array_merge($muri, $msubdomain) as $key => $value) { 423 | if (!is_string($key)) { 424 | continue; 425 | } 426 | 427 | if (true === $caseless) { 428 | $key = mb_strtolower($key); 429 | } 430 | 431 | if (isset($rule[Router::RULE_VARIABLES][$key]) && empty($value)) { 432 | continue; 433 | } 434 | 435 | if (true === $caseless) { 436 | $value = mb_strtolower($value); 437 | } 438 | 439 | $rule[Router::RULE_VARIABLES][$key] = $value; 440 | } 441 | 442 | $this->_rule = $rule; 443 | 444 | return $this; 445 | } 446 | 447 | /** 448 | * Unroute a rule (i.e. route()^-1). 449 | * Special variables: _subdomain and _fragment. 450 | * _subdomain accepts 3 keywords: 451 | * * __root__ to go back to the root (with the smallest subdomain); 452 | * * __self__ to copy the current subdomain (useful if you want a 453 | * complete URL with protocol etc., not only the query part); 454 | * * __shift__ to shift a subdomain part, i.e. going to the upper 455 | * domain; if you want to shift x times, just type __shift__ * x. 456 | * 457 | * @param string $id ID. 458 | * @param array $variables Variables. 459 | * @param bool $secure Whether the connection is secured. If 460 | * null, will use the self::isSecure() value. 461 | * @param string $prefix Path prefix. If null, it will be deduced. 462 | * @return string 463 | * @throws \Hoa\Router\Exception 464 | */ 465 | public function unroute( 466 | $id, 467 | array $variables = [], 468 | $secured = null, 469 | $prefix = null 470 | ) { 471 | if (null === $prefix) { 472 | $prefix = $this->getPrefix(); 473 | } 474 | 475 | $suffix = $this->getSubdomainSuffix(); 476 | $rule = $this->getRule($id); 477 | $pattern = $rule[Router::RULE_PATTERN]; 478 | 479 | foreach ($variables as $KeY => $value) { 480 | if ($KeY != $key = strtolower($KeY)) { 481 | unset($variables[$KeY]); 482 | $variables[$key] = $value; 483 | } 484 | } 485 | 486 | $variables = array_merge($rule[Router::RULE_VARIABLES], $variables); 487 | $anchor = !empty($variables['_fragment']) 488 | ? '#' . $variables['_fragment'] 489 | : null; 490 | unset($variables['_fragment']); 491 | 492 | $self = $this; 493 | $prependPrefix = function ($unroute) use (&$prefix) { 494 | if (0 !== preg_match('#^https?://#', $unroute)) { 495 | return $unroute; 496 | } 497 | 498 | return $prefix . $unroute; 499 | }; 500 | $getPort = function ($secure) use ($self) { 501 | $defaultPort = $self->getDefaultPort($secure); 502 | 503 | if (static::UNSECURE === $secure) { 504 | return 80 !== $defaultPort ? ':' . $defaultPort : ''; 505 | } 506 | 507 | return 443 !== $defaultPort ? ':' . $defaultPort : ''; 508 | }; 509 | 510 | if (true === array_key_exists('_subdomain', $variables) && 511 | null !== $variables['_subdomain']) { 512 | if (empty($variables['_subdomain'])) { 513 | throw new Router\Exception( 514 | 'Subdomain is empty, cannot unroute the rule %s properly.', 515 | 5, 516 | $id 517 | ); 518 | } 519 | 520 | $secure = null === $secured ? static::isSecure() : $secured; 521 | 522 | if (false !== $pos = strpos($pattern, '@')) { 523 | $pattern = substr($pattern, $pos + 1); 524 | } 525 | 526 | $subdomain = $variables['_subdomain']; 527 | $handle = strtolower($subdomain); 528 | 529 | switch ($handle) { 530 | case '__self__': 531 | $subdomain = $this->getSubdomain(); 532 | 533 | break; 534 | 535 | case '__root__': 536 | $subdomain = ''; 537 | 538 | break; 539 | 540 | default: 541 | if (0 !== preg_match('#__shift__(?:\s*\*\s*(\d+))?#', $handle, $m)) { 542 | $repetition = isset($m[1]) ? (int) $m[1] : 1; 543 | $subdomain = $this->getSubdomain(); 544 | 545 | for (; $repetition >= 1; --$repetition) { 546 | if (false === $pos = strpos($subdomain, '.')) { 547 | $subdomain = ''; 548 | 549 | break; 550 | } 551 | 552 | $subdomain = substr($subdomain, $pos + 1); 553 | } 554 | 555 | break; 556 | } 557 | 558 | if (null !== $suffix) { 559 | $subdomain .= '.' . $suffix; 560 | } 561 | 562 | break; 563 | } 564 | 565 | if (!empty($subdomain)) { 566 | $subdomain .= '.'; 567 | } 568 | 569 | return 570 | (true === $secure ? 'https://' : 'http://') . 571 | $subdomain . 572 | $this->getStrictDomain() . 573 | $getPort($secure) . 574 | $prependPrefix($this->_unroute($id, $pattern, $variables)) . 575 | $anchor; 576 | } 577 | 578 | if (false !== $pos = strpos($pattern, '@')) { 579 | $subPattern = substr($pattern, 0, $pos); 580 | $pattern = substr($pattern, $pos + 1); 581 | 582 | if (null !== $suffix) { 583 | $subPattern .= '.' . $suffix; 584 | } 585 | 586 | if ($suffix === $subPattern) { 587 | return 588 | $prependPrefix($this->_unroute($id, $pattern, $variables)) . 589 | $anchor; 590 | } 591 | 592 | $secure = null === $secured ? static::isSecure() : $secured; 593 | 594 | return 595 | (true === $secure ? 'https://' : 'http://') . 596 | $this->_unroute($id, $subPattern, $variables, false) . 597 | '.' . $this->getStrictDomain() . 598 | $getPort($secure) . 599 | $prependPrefix($this->_unroute($id, $pattern, $variables)) . 600 | $anchor; 601 | } 602 | 603 | return 604 | $prependPrefix($this->_unroute($id, $pattern, $variables)) . 605 | $anchor; 606 | } 607 | 608 | /** 609 | * Real unroute method. 610 | * 611 | * @param string $id ID. 612 | * @param string $pattern Pattern. 613 | * @param array $variables Variables. 614 | * @param bool $allowEmpty Whether allow empty variables. 615 | * @return string 616 | * @throws \Hoa\Router\Exception 617 | */ 618 | protected function _unroute( 619 | $id, 620 | $pattern, 621 | array $variables, 622 | $allowEmpty = true 623 | ) { 624 | $unusedVariables = []; 625 | 626 | foreach ($variables as $key => $variable) { 627 | if ('_' !== $key[0]) { 628 | $unusedVariables[$key] = $variable; 629 | } 630 | } 631 | 632 | // (?…) 633 | $out = preg_replace_callback( 634 | '#\(\?\<([^>]+)>[^\)]*\)[\?\*\+]{0,2}#', 635 | function (array $matches) use (&$id, &$variables, &$allowEmpty, &$unusedVariables) { 636 | $m = strtolower($matches[1]); 637 | 638 | if (!isset($variables[$m]) || '' === $variables[$m]) { 639 | if (true === $allowEmpty) { 640 | return ''; 641 | } else { 642 | throw new Router\Exception( 643 | 'Variable %s is empty and it is not allowed when ' . 644 | 'unrouting rule %s.', 645 | 6, 646 | [$m, $id] 647 | ); 648 | } 649 | } 650 | 651 | unset($unusedVariables[$m]); 652 | 653 | return $variables[$m]; 654 | }, 655 | // (-…) 656 | preg_replace('#\(\?\-?[imsxUXJ]+\)#', '', $pattern) 657 | ); 658 | 659 | // (?: 660 | $out = preg_replace('#(?', '\|', '\:', '\-' 675 | ], 676 | [ 677 | '.', '\\', '+', '*', '?', '[', ']', '^', '$', '(', 678 | ')', '{', '}', '=', '!', '<', '>', '|', ':', '-' 679 | ], 680 | $out 681 | ); 682 | 683 | return 684 | $out . 685 | (!empty($unusedVariables) 686 | ? '?' . http_build_query($unusedVariables) 687 | : ''); 688 | } 689 | 690 | /** 691 | * Get HTTP method. 692 | * 693 | * @return string 694 | */ 695 | public function getMethod() 696 | { 697 | if ('cli' === php_sapi_name()) { 698 | return 'get'; 699 | } 700 | 701 | return strtolower($_SERVER['REQUEST_METHOD']); 702 | } 703 | 704 | /** 705 | * Whether the router is called asynchronously or not. 706 | * 707 | * @return bool 708 | */ 709 | public function isAsynchronous() 710 | { 711 | if (!isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { 712 | return false; 713 | } 714 | 715 | return 'xmlhttprequest' == strtolower($_SERVER['HTTP_X_REQUESTED_WITH']); 716 | } 717 | 718 | /** 719 | * Get URI. 720 | * 721 | * @return string 722 | * @throws \Hoa\Router\Exception 723 | */ 724 | public static function getURI() 725 | { 726 | if ('cli' === php_sapi_name()) { 727 | return ltrim(@$_SERVER['argv'][1] ?: '', '/'); 728 | } 729 | 730 | if (!isset($_SERVER['REQUEST_URI'])) { 731 | throw new Router\Exception('Cannot find URI so we cannot route.', 7); 732 | } 733 | 734 | $uri = ltrim(urldecode($_SERVER['REQUEST_URI']), '/'); 735 | 736 | if (false !== $pos = strpos($uri, '?')) { 737 | $uri = substr($uri, 0, $pos); 738 | } 739 | 740 | return $uri; 741 | } 742 | 743 | /** 744 | * Get query. 745 | * 746 | * @return array 747 | */ 748 | public static function getQuery() 749 | { 750 | if ('cli' === php_sapi_name()) { 751 | return []; 752 | } 753 | 754 | if (!isset($_SERVER['REQUEST_URI'])) { 755 | throw new Router\Exception('Cannot find URI so we cannot get query.', 8); 756 | } 757 | 758 | $uri = $_SERVER['REQUEST_URI']; 759 | 760 | if (false === $pos = strpos($uri, '?')) { 761 | return []; 762 | } 763 | 764 | parse_str(substr($uri, $pos + 1), $out); 765 | 766 | return $out; 767 | } 768 | 769 | /** 770 | * Get domain (with subdomain if exists). 771 | * 772 | * @return string 773 | */ 774 | public static function getDomain() 775 | { 776 | static $domain = null; 777 | 778 | if (null === $domain) { 779 | if ('cli' === php_sapi_name()) { 780 | return $domain = ''; 781 | } 782 | 783 | $domain = $_SERVER['HTTP_HOST']; 784 | 785 | if (empty($domain)) { 786 | $domain = $_SERVER['SERVER_ADDR']; 787 | } 788 | 789 | if (0 !== preg_match('#^(.+):' . static::getPort() . '$#', $domain, $m)) { 790 | $domain = $m[1]; 791 | } 792 | } 793 | 794 | return $domain; 795 | } 796 | 797 | /** 798 | * Get strict domain (i.e. without subdomain). 799 | * 800 | * @return string 801 | */ 802 | public function getStrictDomain() 803 | { 804 | $sub = $this->getSubdomain(); 805 | 806 | if (empty($sub)) { 807 | return static::getDomain(); 808 | } 809 | 810 | return substr(static::getDomain(), strlen($sub) + 1); 811 | } 812 | 813 | /** 814 | * Get subdomain. 815 | * 816 | * @param bool $withSuffix With or without suffix. 817 | * @return string 818 | */ 819 | public function getSubdomain($withSuffix = true) 820 | { 821 | static $subdomain = null; 822 | 823 | if (null === $subdomain) { 824 | $domain = static::getDomain(); 825 | 826 | if (empty($domain)) { 827 | return null; 828 | } 829 | 830 | if ($domain == long2ip(ip2long($domain))) { 831 | return null; 832 | } 833 | 834 | if (2 > substr_count($domain, '.', 1)) { 835 | return null; 836 | } 837 | 838 | $subdomain = substr( 839 | $domain, 840 | 0, 841 | strrpos( 842 | $domain, 843 | '.', 844 | -(strlen($domain) - strrpos($domain, '.') + 1) 845 | ) 846 | ); 847 | } 848 | 849 | if (true === $withSuffix) { 850 | return $subdomain; 851 | } 852 | 853 | $suffix = $this->getSubdomainSuffix(); 854 | 855 | if (null === $suffix) { 856 | return $subdomain; 857 | } 858 | 859 | return substr($subdomain, 0, -strlen($suffix) - 1); 860 | } 861 | 862 | /** 863 | * Set subdomain stack: static or dynamic. 864 | * 865 | * @param int $stack Stack: _static or _dynamic constants. 866 | * @return int 867 | */ 868 | public function setSubdomainStack($stack) 869 | { 870 | $old = $this->_subdomainStack; 871 | $this->_subdomainStack = $stack; 872 | 873 | return $old; 874 | } 875 | 876 | /** 877 | * Get subdomain stack. 878 | * 879 | * @return int 880 | */ 881 | public function getSubdomainStack() 882 | { 883 | return $this->_subdomainStack; 884 | } 885 | 886 | /** 887 | * Set subdomain suffix. 888 | * 889 | * @param string $suffix Suffix. 890 | * @return string 891 | */ 892 | public function setSubdomainSuffix($suffix) 893 | { 894 | $old = $this->_subdomainSuffix; 895 | $this->_subdomainSuffix = preg_quote($suffix); 896 | 897 | return $old; 898 | } 899 | 900 | /** 901 | * Get subdomain suffix. 902 | * 903 | * @return string 904 | */ 905 | public function getSubdomainSuffix() 906 | { 907 | return $this->_subdomainSuffix; 908 | } 909 | 910 | /** 911 | * Get port. 912 | * 913 | * @return int 914 | */ 915 | public static function getPort() 916 | { 917 | if ('cli' === php_sapi_name()) { 918 | return 80; 919 | } 920 | 921 | return (int) $_SERVER['SERVER_PORT']; 922 | } 923 | 924 | /** 925 | * Get bootstrap (script name). 926 | * 927 | * @return string 928 | */ 929 | public static function getBootstrap() 930 | { 931 | $sapi = php_sapi_name(); 932 | 933 | if ('cli' === $sapi || 'cli-server' === $sapi) { 934 | return ''; 935 | } 936 | 937 | return $_SERVER['SCRIPT_NAME']; 938 | } 939 | 940 | /** 941 | * Set path prefix. 942 | * 943 | * @param string $prefix Path prefix. 944 | * @return string 945 | */ 946 | public function setPrefix($prefix) 947 | { 948 | $old = $this->_pathPrefix; 949 | $this->_pathPrefix = preg_quote(rtrim($prefix, '/')); 950 | 951 | return $old; 952 | } 953 | 954 | /** 955 | * Get path prefix (aka “base”). 956 | * 957 | * @return string 958 | */ 959 | public function getPrefix() 960 | { 961 | return $this->_pathPrefix; 962 | } 963 | 964 | /** 965 | * Set port. 966 | * 967 | * @param int $port Port. 968 | * @param bool $secure Whether the connection is secured. 969 | * @return int 970 | */ 971 | public function setDefaultPort($port, $secure = self::UNSECURE) 972 | { 973 | if (static::UNSECURE === $secure) { 974 | $old = $this->_httpPort; 975 | $this->_httpPort = $port; 976 | } else { 977 | $old = $this->_httpsPort; 978 | $this->_httpsPort = $port; 979 | } 980 | 981 | return $old; 982 | } 983 | 984 | /** 985 | * Get HTTP port. 986 | * 987 | * @param bool $secure Whether the connection is secured. 988 | * @return int 989 | */ 990 | public function getDefaultPort($secure = self::UNSECURE) 991 | { 992 | if (static::UNSECURE === $secure) { 993 | return $this->_httpPort; 994 | } 995 | 996 | return $this->_httpsPort; 997 | } 998 | 999 | /** 1000 | * Whether the connection is secure. 1001 | * 1002 | * @return bool 1003 | */ 1004 | public static function isSecure() 1005 | { 1006 | if (!isset($_SERVER['HTTPS'])) { 1007 | return static::UNSECURE; 1008 | } 1009 | 1010 | return 1011 | (!empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS']) 1012 | ? static::SECURE 1013 | : static::UNSECURE; 1014 | } 1015 | } 1016 | 1017 | /** 1018 | * Flex entity. 1019 | */ 1020 | Consistency::flexEntity('Hoa\Router\Http\Http'); 1021 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hoa 3 |

4 | 5 | --- 6 | 7 |

8 | Build status 9 | Code coverage 10 | Packagist 11 | License 12 |

13 |

14 | Hoa is a modular, extensible and 15 | structured set of PHP libraries.
16 | Moreover, Hoa aims at being a bridge between industrial and research worlds. 17 |

18 | 19 | # Hoa\Router 20 | 21 | [![Help on IRC](https://img.shields.io/badge/help-%23hoaproject-ff0066.svg)](https://webchat.freenode.net/?channels=#hoaproject) 22 | [![Help on Gitter](https://img.shields.io/badge/help-gitter-ff0066.svg)](https://gitter.im/hoaproject/central) 23 | [![Documentation](https://img.shields.io/badge/documentation-hack_book-ff0066.svg)](https://central.hoa-project.net/Documentation/Library/Router) 24 | [![Board](https://img.shields.io/badge/organisation-board-ff0066.svg)](https://waffle.io/hoaproject/router) 25 | 26 | This library allows to find an appropriated route and extracts data from a 27 | request. Conversely, given a route and data, this library is able to build a 28 | request. 29 | 30 | For now, we have two routers: HTTP (routes understand URI and subdomains) and 31 | CLI (routes understand a full command-line). 32 | 33 | [Learn more](https://central.hoa-project.net/Documentation/Library/Router). 34 | 35 | ## Installation 36 | 37 | With [Composer](https://getcomposer.org/), to include this library into 38 | your dependencies, you need to 39 | require [`hoa/router`](https://packagist.org/packages/hoa/router): 40 | 41 | ```sh 42 | $ composer require hoa/router '~3.0' 43 | ``` 44 | 45 | For more installation procedures, please read [the Source 46 | page](https://hoa-project.net/Source.html). 47 | 48 | ## Testing 49 | 50 | Before running the test suites, the development dependencies must be installed: 51 | 52 | ```sh 53 | $ composer install 54 | ``` 55 | 56 | Then, to run all the test suites: 57 | 58 | ```sh 59 | $ vendor/bin/hoa test:run 60 | ``` 61 | 62 | For more information, please read the [contributor 63 | guide](https://hoa-project.net/Literature/Contributor/Guide.html). 64 | 65 | ## Quick usage 66 | 67 | We propose a quick overview of two usages: in a HTTP context and in a CLI 68 | context. 69 | 70 | ### HTTP 71 | 72 | We consider the following routes: 73 | 74 | * `/hello`, only accessible with the `GET` and `POST` method; 75 | * `/bye`, only accessible with the `GET` method; 76 | * `/hello_` only accessible with the `GET` method. 77 | 78 | There are different ways to declare routes but the more usual is as follows: 79 | 80 | ```php 81 | $router = new Hoa\Router\Http(); 82 | $router 83 | ->get('u', '/hello', function () { 84 | echo 'world!', "\n"; 85 | }) 86 | ->post('v', '/hello', function (Array $_request) { 87 | echo $_request['a'] + $_request['b'], "\n"; 88 | }) 89 | ->get('w', '/bye', function () { 90 | echo 'ohh :-(', "\n"; 91 | }) 92 | ->get('x', '/hello_(?\w+)', function ($nick) { 93 | echo 'Welcome ', ucfirst($nick), '!', "\n"; 94 | }); 95 | ``` 96 | 97 | We can use a basic dispatcher to call automatically the associated callable of 98 | the appropriated rule: 99 | 100 | ```php 101 | $dispatcher = new Hoa\Dispatcher\Basic(); 102 | $dispatcher->dispatch($router); 103 | ``` 104 | 105 | Now, we will use [cURL](http://curl.haxx.se/) to test our program that listens 106 | on `127.0.0.1:8888`: 107 | 108 | ```sh 109 | $ curl 127.0.0.1:8888/hello 110 | world! 111 | $ curl -X POST -d a=3\&b=39 127.0.0.1:8888/hello 112 | 42 113 | $ curl 127.0.0.1:8888/bye 114 | ohh :-( 115 | $ curl -X POST 127.0.0.1:8888/bye 116 | // error 117 | $ curl 127.0.0.1:8888/hello_gordon 118 | Welcome Gordon! 119 | $ curl 127.0.0.1:8888/hello_alyx 120 | Welcome Alyx! 121 | ``` 122 | 123 | This simple API hides a modular mechanism that can be foreseen by typing 124 | `print_r($router->getTheRule())`. 125 | 126 | To unroute, i.e. make the opposite operation, we can do this: 127 | 128 | ```php 129 | var_dump($router->unroute('x', array('nick' => 'gordon'))); 130 | // string(13) "/hello_gordon" 131 | ``` 132 | 133 | ### CLI 134 | 135 | We would like to recognize the following route `[:]? 136 | ` in the `Router.php` file: 137 | 138 | ```php 139 | $router = new Hoa\Router\Cli(); 140 | $router->get( 141 | 'g', 142 | '(?\w+):(?\w+)(?.*?)' 143 | function ($group, $subcommand, $options) { 144 | echo 145 | 'Group : ', $group, "\n", 146 | 'Subcommand: ', $subcommand, "\n", 147 | 'Options : ', trim($options), "\n"; 148 | } 149 | ); 150 | ``` 151 | 152 | We can use a basic dispatcher to call automatically the associated callable: 153 | 154 | ```php 155 | $dispatcher = new Hoa\Dispatcher\Basic(); 156 | $dispatcher->dispatch($router); 157 | ``` 158 | 159 | And now, testing time: 160 | 161 | ```sh 162 | $ php Router.php foo:bar --some options 163 | Group : foo 164 | Subcommand: bar 165 | Options : --some options 166 | ``` 167 | 168 | The use of the [`Hoa\Console` 169 | library](https://central.hoa-project.net/Resource/Library/Console) would be a 170 | good idea to interprete the options and getting some comfortable services for 171 | the terminal. 172 | 173 | ## Documentation 174 | 175 | The 176 | [hack book of `Hoa\Router`](https://central.hoa-project.net/Documentation/Library/Router) 177 | contains detailed information about how to use this library and how it works. 178 | 179 | To generate the documentation locally, execute the following commands: 180 | 181 | ```sh 182 | $ composer require --dev hoa/devtools 183 | $ vendor/bin/hoa devtools:documentation --open 184 | ``` 185 | 186 | More documentation can be found on the project's website: 187 | [hoa-project.net](https://hoa-project.net/). 188 | 189 | ## Getting help 190 | 191 | There are mainly two ways to get help: 192 | 193 | * On the [`#hoaproject`](https://webchat.freenode.net/?channels=#hoaproject) 194 | IRC channel, 195 | * On the forum at [users.hoa-project.net](https://users.hoa-project.net). 196 | 197 | ## Contribution 198 | 199 | Do you want to contribute? Thanks! A detailed [contributor 200 | guide](https://hoa-project.net/Literature/Contributor/Guide.html) explains 201 | everything you need to know. 202 | 203 | ## License 204 | 205 | Hoa is under the New BSD License (BSD-3-Clause). Please, see 206 | [`LICENSE`](https://hoa-project.net/LICENSE) for details. 207 | -------------------------------------------------------------------------------- /Router.php: -------------------------------------------------------------------------------- 1 |