-
Notifications
You must be signed in to change notification settings - Fork 35
Routing
Routing в переводе на русский "Марштрутиризация". Однако, что бы не путать с [Маршутиризацией] (https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%80%D1%88%D1%80%D1%83%D1%82%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) из области "компьютерные сети", будем его так и называть - "Роутинг".
Ваше приложение (сайт) представляет из себя программу которая за 1 запуск получает 1 набор входных данных и формирует 1 ответ.
Пример (консольное приложение суммирующее 2 числа):
$ sum 3 5
> 8
Хотите сложить два других числа? Вызывайте еще раз.
$ sum 10 2
> 12
Как вы заметили вызов программ не зависит от предыдущих вызовов. (Это соотвествует стандарту HTTP и назывется [stateless] (http://en.wikipedia.org/wiki/Stateless_protocol) протокол.)
Допустим ваше приложение может выполнять несколько операций, например: sum, subtract, multiple, divide. Тогда наш пример модифицируется
$ calculate sum 10 2
> 12
или
$ calculate subtract 10 2
> 8
Получается, что операции: sum, subtract, multiple, divide передаются в наше приложение как параметр.
HTTP имеет обязательный параметр [URI] (https://ru.wikipedia.org/wiki/URI). URI это адрес ресурса к которому отправлен запрос.
Добавить URI в качестве параметра к предыдущему примеру имеет смысл если наше приложение производит операции над "банковским счетом".
Условимся, что у нас есть ресурсы со следующими URI
- / - Список типов ресурсов
- /account - Список ресурсов типа банковский счет
- /pocket - Список ресурсов типа кошелек с наличностью счет
- /account/{id} - Банковский счет № id
- /pocket/{id} - Кошелек с наличностью счет № id
Добавим, к существующим методам, метод view который будет выводить значение ресурса.
$ calculate '/' view
> /account
> /pocket
$ calculate '/account' view
> /account/1
> /account/2
> /account/3
> /account/4
$ calculate '/pocket' view
> /pocket/1
> /pocket/2
$ calculate '/pocket/1' view
> 5
Тогда операции над объектами будут выглядеть вот так
$ calculate '/pocket/1' sum 15
> 20
$ calculate '/pocket/1' view
> 20
$ calculate '/pocket/1' divide 10
> 2
$ calculate '/pocket/1' view
> 2
Обратите внимание, что запросы до сих пор stateless, потому, что они не обмениваются данными друг с другом. Можно возразить, что хранение состояния происходит через ресурс. Однако это не считается. Тут можно вспомнить о понятиях [идемпотентных и безопасных методах] (http://www.restapitutorial.ru/lessons/idempotency.html).
Попробуем привести наш пример ближе к стандарту HTTP.
Ваше приложение (сайт) в качестве входных данных получает 1 [HTTP запрос] (https://ru.wikipedia.org/wiki/HTTP#.D0.A1.D1.82.D1.80.D1.83.D0.BA.D1.82.D1.83.D1.80.D0.B0_.D0.BF.D1.80.D0.BE.D1.82.D0.BE.D0.BA.D0.BE.D0.BB.D0.B0). И на каждый HTTP запрос формирут [HTTP ответ] (https://ru.wikipedia.org/wiki/HTTP#.D0.9F.D1.80.D0.B8.D0.BC.D0.B5.D1.80.D1.8B_.D0.B4.D0.B8.D0.B0.D0.BB.D0.BE.D0.B3.D0.BE.D0.B2_HTTP).
Как вы помните в стандарте HTTP есть [методы] (https://ru.wikipedia.org/wiki/HTTP#.D0.9C.D0.B5.D1.82.D0.BE.D0.B4.D1.8B) GET - получить POST - создать\изменить PUT - заменить DELETE - удалить и т.д.
Мы пишем веб приложение, и вы конечно помните, что HTML поддерживает только [GET и POST] (http://stackoverflow.com/questions/8054165/using-put-method-in-html-form). [Тут можно почитать рассужение почему так] (http://programmers.stackexchange.com/questions/114156/why-there-are-no-put-and-delete-methods-in-html-forms) Условимся, что методы GET - возвращают представление объекта, POST - модифицирует объект.
Тогда метод GET является аналогом метода view.
$ calculate GET '/'
> /account
> /pocket
$ calculate GET '/account'
> /account/1
> /account/2
> /account/3
> /account/4
$ calculate GET '/pocket'
> /pocket/1
> /pocket/2
$ calculate GET '/pocket/1'
> 2
С модифицирующими методами интереснее, потому что теперь модифицирующий метод у нас 1 - POST. Значит тип модификации нужно передовать каким то дополнительным образом.
$ calculate POST '/pocket/1/sum' 15
> 17
$ calculate POST '/pocket/1' ?op=sum 15
> 17
$ calculate POST '/pocket/1' op=sum 15
> 17
Задача роутинга в зависимости от входных данных (обычно URI) вызвать тот код, который должен выполнять действие.
// Певращаем URI в массив.
// Например /pockets/1/sum в массив ['pockets', 1, 'sum']
$parts = explode('/', $_SERVER['REQUEST_URI'] );
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
if ($parts[0] == 'pockets') {
if (is_number($parts[1]) && pocket_exists($parts[1])) {
if ($parts[2] == 'sum') {
pocket_sum($parts[1], $_POST['param']);
}
elseif ($parts[2] == 'divide') {
pocket_divide($parts[1], $_POST['param']);
}
elseif ($parts[2] == 'divide') {
.......
}
}
}
elseif ($parts[1] == 'account') {
......
}
}
elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
.....
}
Данный код сложен для восприятия, по причине множества ветвлений if\else.
Отрефакторим c использование switch
// Певращаем URI в массив.
// Например /pockets/1/sum в массив ['pockets', 1, 'sum']
$parts = explode('/', $_SERVER['REQUEST_URI'] );
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
switch ($parts[0]) {
case 'pockets':
route_pockets($parts);
break;
case 'accounts':
route_accounts($parts);
break;
default:
route_list($parts);
break;
}
}
elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
.....
}
function route_pockets($parts) {
if (is_number($parts[1]) && pocket_exists($parts[1])) {
switch ($parts[2]) {
case 'sum':
pocket_sum($parts[1], $_POST['param']);
break;
case 'divide':
pocket_divide($parts[1], $_POST['param']);
break;
.....
}
}
else {
show_error();
}
}
Используем регулярные выражения, которые оптимизируют разбор URI.
// Объявление роутов
$routes = array();
$routes['GET']['^\/$'] = 'home';
$routes['GET']['^\/pocket(\/?)$'] = 'pockets_list';
$routes['GET']['^\/pocket\/(\d+)$'] = 'pocket_view';
$routes['POST']['^\/pocket\/(\d+)\/sum$'] = 'pocket_sum';
$routes['POST']['^\/pocket\/(\d+)\/divide$'] = 'pocket_divide';
$routes['POST']['^\/pocket\/(\d+)\/subtract$'] = 'pocket_subtract';
$routes['POST']['^\/pocket\/(\d+)\/multiply$'] = 'pocket_multiply';
$routes['GET']['^\/account(\/?)$'] = 'accounts_list';
$routes['GET']['^\/account\/(\d+)$'] = 'account_view';
$routes['POST']['^\/account\/(\d+)\/sum$'] = 'account_sum';
$routes['POST']['^\/account\/(\d+)\/divide$'] = 'account_divide';
$routes['POST']['^\/account\/(\d+)\/subtract$'] = 'account_subtract';
$routes['POST']['^\/account\/(\d+)\/multiply$'] = 'account_multiply';
// Выполнение роутинга
// Используем роуты $routes['GET'] или $routes['POST'] в зависимости от метода HTTP.
$active_routes = $routes[$_SERVER['REQUEST_METHOD']];
// Для всех роутов
foreach ($active_routes as $pattern => $callback) {
// Если REQUEST_URI соответствует шаблону - вызываем функцию
if (preg_match_all("/$pattern/", $_SERVER['REQUEST_URI'], $matches) !== false) {
// вызываем callback
$callback();
// выходим из цикла
break;
}
$matches = array();
}
Данный код не документирует использование массива $routes, позволяет писать туда недопустимые значения. Для исправления данных проблем перепишем данный код в виде класса Router инкапсулировав в нем реализацию.
// Описание класса
class Router {
private $routes = null;
public __constructor() {
$this->routes = array();
}
public function get($pattern, $callback) {
$this->set('GET', $pattern, $callback);
}
public function post($pattern, $callback) {
$this->set('POST', $pattern, $callback);
}
private function set($type, $pattern, $callback) {
if (!function_exists($callback)) {
new Exception("Method $callback not exists");
}
$this->routes[$type][$pattern] = $callback;
}
public function process($method, $uri) {
if (in_array($method, array('GET', 'POST'))) {
new Exception("Request method should be GET or POST");
}
// Выполнение роутинга
// Используем роуты $routes['GET'] или $routes['POST'] в зависимости от метода HTTP.
$active_routes = $this->routes[$method];
// Для всех роутов
foreach ($active_routes as $pattern => $callback) {
// Если REQUEST_URI соответствует шаблону - вызываем функцию
if (preg_match_all("/$pattern/", $uri, $matches) !== false) {
// вызываем callback
$callback();
// выходим из цикла
break;
}
$matches = array();
}
}
}
$r = new Router();
$r->get('^\/$', 'home');
$r->get('^\/pocket(\/?)$', 'pockets_list');
$r->get('^\/pocket\/(\d+)$', 'pocket_view');
$r->post('^\/pocket\/(\d+)\/sum$', 'pocket_sum');
$r->post('^\/pocket\/(\d+)\/divide$', 'pocket_divide');
$r->post('^\/pocket\/(\d+)\/subtract$', 'pocket_subtract');
$r->post('^\/pocket\/(\d+)\/multiply$', 'pocket_multiply');
$r->get('^\/account(\/?)$', 'account_list');
$r->get('^\/account\/(\d+)$', 'account_view');
$r->post('^\/account\/(\d+)\/sum$', 'account_sum');
$r->post('^\/account\/(\d+)\/divide$', 'account_divide');
$r->post('^\/account\/(\d+)\/subtract$', 'account_subtract');
$r->post('^\/account\/(\d+)\/multiply$', 'account_multiply');
$r->process($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
Данный код плох тем, что объявление роутов возможно только в 1 точке. Добавление каких либо подсистем вынудит добавлять роутинг к страницам которые данная система предоставляет в описаной выше точке. Решением данной проблемы может быть превращение данного класса в статический или использование паттерна [Singleton] (http://ru.wikipedia.org/wiki/%D0%9E%D0%B4%D0%B8%D0%BD%D0%BE%D1%87%D0%BA%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F))
// ----------- Router.php
class Router {
private $routes = null;
public __constructor() {
$this->routes = array();
}
public function get($pattern, $callback) {
$this->set('GET', $pattern, $callback);
}
public function post($pattern, $callback) {
$this->set('POST', $pattern, $callback);
}
private function set($type, $pattern, $callback) {
if (!function_exists($callback)) {
new Exception("Method $callback not exists");
}
$this->routes[$type][$pattern] = $callback;
}
public function process($method, $uri) {
if (in_array($method, array('GET', 'POST'))) {
new Exception("Request method should be GET or POST");
}
// Выполнение роутинга
// Используем роуты $routes['GET'] или $routes['POST'] в зависимости от метода HTTP.
$active_routes = $this->routes[$method];
// Для всех роутов
foreach ($active_routes as $pattern => $callback) {
// Если REQUEST_URI соответствует шаблону - вызываем функцию
if (preg_match_all("/$pattern/", $uri, $matches) !== false) {
// вызываем callback
$callback();
// выходим из цикла
break;
}
$matches = array();
}
}
}
// ------ index.php-------------------------
include_once 'Router.php';
// вызывает {module}.routes.php
modules_load_routes();
$r = new Router();
$r->process($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
// -----------home.routes.php------------------------------------
$r = new Router();
$r->get('^\/$', 'home');
// -----------pocket.routes.php------------------------------------
$r = new Router();
$r->get('^\/pocket(\/?)$', 'pockets_list');
$r->get('^\/pocket\/(\d+)$', 'pocket_view');
$r->post('^\/pocket\/(\d+)\/sum$', 'pocket_sum');
$r->post('^\/pocket\/(\d+)\/divide$', 'pocket_divide');
$r->post('^\/pocket\/(\d+)\/subtract$', 'pocket_subtract');
$r->post('^\/pocket\/(\d+)\/multiply$', 'pocket_multiply');
// -----------account.routes.php------------------------------------
$r = new Router();
$r->get('^\/account(\/?)$', 'account_list');
$r->get('^\/account\/(\d+)$', 'account_view');
$r->post('^\/account\/(\d+)\/sum$', 'account_sum');
$r->post('^\/account\/(\d+)\/divide$', 'account_divide');
$r->post('^\/account\/(\d+)\/subtract$', 'account_subtract');
$r->post('^\/account\/(\d+)\/multiply$', 'account_multiply');
Данный код не будет работать, потому мы создаем 4 объекта класса Route. Метод process вызывается только у одного из них, при том у того объекта нет определенных роутов.
Перепишим объек Router, таким образом, что бы он допускал создание только 1 объекта.
// ----------- Router.php
class Router {
private $routes = null;
private static $_instances = null;
private __constructor() {
$this->routes = array();
}
public static function Instance() {
if (is_null(self::$_instance)) {
self::$_instance = new Router();
}
return self::$_instance;
}
public function get($pattern, $callback) {
$this->set('GET', $pattern, $callback);
}
public function post($pattern, $callback) {
$this->set('POST', $pattern, $callback);
}
private function set($type, $pattern, $callback) {
if (!function_exists($callback)) {
new Exception("Method $callback not exists");
}
$this->routes[$type][$pattern] = $callback;
}
public function process($method, $uri) {
if (in_array($method, array('GET', 'POST'))) {
new Exception("Request method should be GET or POST");
}
// Выполнение роутинга
// Используем роуты $routes['GET'] или $routes['POST'] в зависимости от метода HTTP.
$active_routes = $this->routes[$method];
// Для всех роутов
foreach ($active_routes as $pattern => $callback) {
// Если REQUEST_URI соответствует шаблону - вызываем функцию
if (preg_match_all("/$pattern/", $uri, $matches) !== false) {
// вызываем callback
$callback();
// выходим из цикла
break;
}
$matches = array();
}
}
}
// ------ index.php-------------------------
include_once 'Router.php';
// вызывает {module}.routes.php
modules_load_routes();
$r = Router::Instance();
$r->process($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
// -----------home.routes.php------------------------------------
$r = Router::Instance();
$r->get('^\/$', 'home');
// -----------pocket.routes.php------------------------------------
$r = Router::Instance();
$r->get('^\/pocket(\/?)$', 'pockets_list');
$r->get('^\/pocket\/(\d+)$', 'pocket_view');
$r->post('^\/pocket\/(\d+)\/sum$', 'pocket_sum');
$r->post('^\/pocket\/(\d+)\/divide$', 'pocket_divide');
$r->post('^\/pocket\/(\d+)\/subtract$', 'pocket_subtract');
$r->post('^\/pocket\/(\d+)\/multiply$', 'pocket_multiply');
// -----------account.routes.php------------------------------------
$r = Router::Instance();
$r->get('^\/account(\/?)$', 'account_list');
$r->get('^\/account\/(\d+)$', 'account_view');
$r->post('^\/account\/(\d+)\/sum$', 'account_sum');
$r->post('^\/account\/(\d+)\/divide$', 'account_divide');
$r->post('^\/account\/(\d+)\/subtract$', 'account_subtract');
$r->post('^\/account\/(\d+)\/multiply$', 'account_multiply');
// ----------- Router.php
class Router {
private static $routes = array();
public __constructor() {}
public function get($pattern, $callback) {
$this->set('GET', $pattern, $callback);
}
public function post($pattern, $callback) {
$this->set('POST', $pattern, $callback);
}
private function set($type, $pattern, $callback) {
if (!function_exists($callback)) {
new Exception("Method $callback not exists");
}
self::$routes[$type][$pattern] = $callback;
}
public function process($method, $uri) {
if (in_array($method, array('GET', 'POST'))) {
new Exception("Request method should be GET or POST");
}
// Выполнение роутинга
// Используем роуты $routes['GET'] или $routes['POST'] в зависимости от метода HTTP.
$active_routes = self::$routes[$method];
// Для всех роутов
foreach ($active_routes as $pattern => $callback) {
// Если REQUEST_URI соответствует шаблону - вызываем функцию
if (preg_match_all("/$pattern/", $uri, $matches) !== false) {
// вызываем callback
$callback();
// выходим из цикла
break;
}
$matches = array();
}
}
}
// ------ index.php-------------------------
include_once 'Router.php';
// вызывает {module}.routes.php
modules_load_routes();
$r = new Router();
$r->process($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
// -----------home.routes.php------------------------------------
$r = new Router();
$r->get('^\/$', 'home');
// -----------pocket.routes.php------------------------------------
$r = new Router();
$r->get('^\/pocket(\/?)$', 'pockets_list');
$r->get('^\/pocket\/(\d+)$', 'pocket_view');
$r->post('^\/pocket\/(\d+)\/sum$', 'pocket_sum');
$r->post('^\/pocket\/(\d+)\/divide$', 'pocket_divide');
$r->post('^\/pocket\/(\d+)\/subtract$', 'pocket_subtract');
$r->post('^\/pocket\/(\d+)\/multiply$', 'pocket_multiply');
// -----------account.routes.php------------------------------------
$r = new Router();
$r->get('^\/account(\/?)$', 'account_list');
$r->get('^\/account\/(\d+)$', 'account_view');
$r->post('^\/account\/(\d+)\/sum$', 'account_sum');
$r->post('^\/account\/(\d+)\/divide$', 'account_divide');
$r->post('^\/account\/(\d+)\/subtract$', 'account_subtract');
$r->post('^\/account\/(\d+)\/multiply$', 'account_multiply');
// ----------- Router.php
class Router {
private static $routes = array();
private __constructor() {}
public static function get($pattern, $callback) {
self::set('GET', $pattern, $callback);
}
public static function post($pattern, $callback) {
self::set('POST', $pattern, $callback);
}
private static function set($type, $pattern, $callback) {
if (!function_exists($callback)) {
new Exception("Method $callback not exists");
}
self::$routes[$type][$pattern] = $callback;
}
public static function process($method, $uri) {
if (in_array($method, array('GET', 'POST'))) {
new Exception("Request method should be GET or POST");
}
// Выполнение роутинга
// Используем роуты $routes['GET'] или $routes['POST'] в зависимости от метода HTTP.
$active_routes = self::$routes[$method];
// Для всех роутов
foreach ($active_routes as $pattern => $callback) {
// Если REQUEST_URI соответствует шаблону - вызываем функцию
if (preg_match_all("/$pattern/", $uri, $matches) !== false) {
// вызываем callback
$callback();
// выходим из цикла
break;
}
$matches = array();
}
}
}
// ------ index.php-------------------------
include_once 'Router.php';
// вызывает {module}.routes.php
modules_load_routes();
Router::process($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
// -----------home.routes.php------------------------------------
Router::get('^\/$', 'home');
// -----------pocket.routes.php------------------------------------
Router::get('^\/pocket(\/?)$', 'pockets_list');
Router::get('^\/pocket\/(\d+)$', 'pocket_view');
Router::post('^\/pocket\/(\d+)\/sum$', 'pocket_sum');
Router::post('^\/pocket\/(\d+)\/divide$', 'pocket_divide');
Router::post('^\/pocket\/(\d+)\/subtract$', 'pocket_subtract');
Router::post('^\/pocket\/(\d+)\/multiply$', 'pocket_multiply');
// -----------account.routes.php------------------------------------
Router::get('^\/account(\/?)$', 'account_list');
Router::get('^\/account\/(\d+)$', 'account_view');
Router::post('^\/account\/(\d+)\/sum$', 'account_sum');
Router::post('^\/account\/(\d+)\/divide$', 'account_divide');
Router::post('^\/account\/(\d+)\/subtract$', 'account_subtract');
Router::post('^\/account\/(\d+)\/multiply$', 'account_multiply');