Динамические адреса на основе правил роутинга

Материал из 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. Но как для меня — этот функционал — уже не мало.

Личные инструменты