Динамические адреса на основе правил роутинга
Материал из Wiki
Новый функционал роутинга для CodeIgniter
upd: Расширен функционал -- версия от 30 июля 2009.
Одно из слабых мест CodeIgniter, на мой взгляд, это функционал роутинга, а именно — формирование строки адреса в коде приложения.
Если сам по себе роутинг очень удобен и позволяет строить довольно замысловатые схемы перестройки адреса, то формирование строки адреса для ссылки на веб странице ложиться полностью на плечи программиста.
И вот здесь возникает основная проблема: Роутинг по приложению описывается компактно в одном файле, где все очень наглядно и понятно, а строки адресов для ссылок формируются в десятках файлов (контроллеры или библиотеки) и везде, программисту нужно строго формировать строку адреса, сегмент за сегментом, разделяя слэшами, опираясь при этом, на единый конфиг роутинга. И вот… наступил момент, когда приложение невероятно сильно разрослось и по какой-то причине понадобилось пересмотреть роутинг и тут я повторюсь — файл настройки роутинга один, а файлов, где формируются строки для ссылок — много… И вот тут начинаешь понимать слабость красивой функции site_url.
Для начала, что я подразумеваю под автоматическим формированием. Урл — это набор параметров для веб-приложения, у каждого параметра есть имя… Лень разглагольствовать — покажу пример конфига:
$route[] = array('main' => '(.+)/(\d\d)/(.+)',
'name' => 'base',
'url' => ':param/:page/:word',
'route' => 'welcome/index/$1/$3/$2');
И по частям:
- 'main' => '(.+)/(\d\d)/(.+)' — в качестве значения записывается стандартная формулировка формулы роутинга, то что пишется в оригинале ключом массива.
- 'name' => 'base' — имя конкретно этой записи роутинга.
- 'url' => ':param/:page/:word' — строка-формула создания адреса. Ведущее двоеточие означает, что за ним следует имя параметра, которое будет заменяться на соответствующее значение.
- 'route' => 'welcome/index/$1/$3/$2' — строка-формула измененного роутинга, в оригинале — значение в массиве роутинга.
Применение:
$url = $this->router->buildUrl('base', array('param' => $param, 'page' => $num, 'word' => 'ooops'));
//или
$url = site_url('base', array('param' => $param, 'page' => $num, 'word' => 'ooops'));
Таким образом, мы получаем возможность в случае необходимости, менять роутинг в своем приложении централизованно и не переживая о том, что мы в каком-то модуле не заметили (забыли/пропустили) место формирования строки адреса.
Да, еще замечу, что это именно надстройка, основной роутинг и функции, работают как и раньше, за исключением функции хелпера url site_url, но она по прежнему способна работать как и раньше, принимая простую строку. Функцию site_url библиотеки config я трогать не стал, но и её расширить не составляет труда, но я посчитал, что большинство, как и я предпочитают использовать хелпер url с его функцией site_url().
Код хелпера MY_url_helper.php
/**
* @copyright Артюх Антон 2009
* @site http://tovit.livejournal.com
*/
/**
* Build url
*
* @param string Name of rule or url-string
* @param array Array of params
* @return string URL
**/
function site_url($urlname, $params = NULL)
{
$CI = & get_instance();
if($params !== NULL && method_exists($CI->router, 'buildUrl'))
{
return $CI->router->buildUrl($urlname, $params);
} else
{
return $CI->config->site_url($urlname);
}
}
И библиотека MY_Router.php:
/**
* Расширяет базовый функционал роутинга, добавляя возможность автоматического формирования url используя функцию Функция buildUrl
*
* @version 1.5
* @author Артюх Антон * @site http://tovit.livejournal.com
*/
class MY_Router extends CI_Router {
const MAIN = 'main';
const ROUTE = 'route';
const NAME = 'name';
const URL = 'url';
/**
* Search in Array
*
* @param array $arr
* @param string $key
* @param string $value
* @return int index of element
*/
function _search_by_key(&$arr, $key, $value)
{
$ret = false;
foreach($arr as $k => $v)
{
if(!is_int($k)) continue;
if(is_array($v))
{
if($v[$key] == $value)
{
$ret = $k;
break;
}
}
}
return $ret;
}
/**
* Parse Routes
*
* This function matches any routes that may exist in
* the config/routes.php file against the URI to
* determine if the class/method need to be remapped.
*
* @access private
* @return void
*/
function _parse_routes()
{
// Do we even have any custom routing to deal with?
// There is a default scaffolding trigger, so we'll look just for 1
if (count($this->routes) == 1)
{
$this->_set_request($this->uri->segments);
return;
}
// Turn the segment array into a URI string
$uri = implode('/', $this->uri->segments);
// Is there a literal match? If so we're done
if (isset($this->routes[$uri]))
{
$this->_set_request(explode('/', $this->routes[$uri]));
return;
}
//Art
$i = $this->_search_by_key($this->routes, self::MAIN, $uri);
if ($i !== FALSE)
{
$this->_set_request(explode('/', $this->routes[$i][self::ROUTE]));
return;
}
// Loop through the route array looking for wild-cards
foreach ($this->routes as $key => $val)
{
if(is_int($key))
{
$key = $val[self::MAIN];
$val = $val[self::ROUTE];
}
// Convert wild-cards to RegEx
$key = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $key));
// Does the RegEx match?
if (preg_match('#^'.$key.'$#', $uri))
{
// Do we have a back-reference?
if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
{
$val = preg_replace('#^'.$key.'$#', $val, $uri);
}
$this->_set_request(explode('/', $val));
return;
}
}
// If we got this far it means we didn't encounter a
// matching route so we'll set the site default route
$this->_set_request($this->uri->segments);
}
/**
* Build the url using params in config by name of rule
*
* @param string Name of rule
* @param array associative array of params
* @return string url
*/
function buildUrl($name, $array = null)
{
$i = 0;
$i = $this->_search_by_key($this->routes, self::NAME, $name);
if($i === FALSE)
{
log_message('ERROR', 'Try to create undefined url with name: '.$name);
return $this->config->site_url();
}
$rule = $this->routes[$i];
if(is_null($array)){
return $rule[self::URL];
}
//v1.5 Наследование роутинга
if(preg_match_all("#\[([\w_]+)\]#", $rule[self::URL], $mas)){
$l = count($mas[1]);
for($i = 0; $i < $l; $i++){
$j = $this->_search_by_key($this->routes, self::NAME, $mas[1][$i]);
if($j !== FALSE){
$parent_rule = $this->routes[$j];
$rule[self::URL] = str_replace('[' . $mas[1][$i] . ']', $parent_rule[self::URL], $rule[self::URL]);
}
}
}
foreach($array as $k => $v)
{
$rule[self::URL] = str_replace(':'.$k . '/', $v . '/', $rule[self::URL]);
}
//исправление проблемных случаев, когда указание переменной не заканчивается слешем.
if(preg_match("@:\w+$@", $rule[self::URL])){
foreach($array as $k => $v){
$rule[self::URL] = str_replace(':'.$k, $v, $rule[self::URL]);
}
}
return $this->config->site_url($rule[self::URL]);
}
}
UPDATE: v1.5 -- new Появилась возможность наследовать правила. Наследование производиться через указание в правиле имени другого правила роутинга в квадратных скобках. Например:
$route[] = array('main' => '(.+)/(\d\d)/(.+)',
'name' => 'base',
'url' => '[lang]/:param/:page/:word',
'route' => 'welcome/index/$1/$3/$2');
[lang] -- в данном случае, имя определенного ранее роутинга, который будет добавлен к строке адреса.
fixed исправлена работа с последним параметром не закрытым слешем.
P.S. Данная библиотека не претендует на новшества или гениальность решения, но вполне справляется с поставленной задачей хоть и покрывает лишь малую часть от возможностей Zend_Router. Но как для меня — этот функционал — уже не мало.