Skip to content
goruha edited this page Nov 10, 2014 · 22 revisions

Введение

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

Попробуем привести наш пример ближе к стандарту 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. Значит тип модификации нужно передовать каким то дополнительным образом.

Вариант 1: Часть URI

$ calculate POST '/pocket/1/sum' 15
> 17

Вариант 2: URI query

$ calculate POST '/pocket/1' ?op=sum 15
> 17

Вариант 3: POST body param

$ calculate POST '/pocket/1' op=sum 15
> 17

Routing

Задача роутинга в зависимости от входных данных (обычно 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.

Роутинг с использованием switch

Отрефакторим 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))

Реализация через Singleton

// ----------- 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');

Реализация в виде статического класса - вариант 1.

// ----------- 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');

Реализация в виде статического класса - вариант 2.

// ----------- 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');