Reverse routing

Jeśli zadarzyło się Wam zmieniać adresy URL w aplikacji, to pewnie wiecie jaki to ból. Adresy trzeba ręcznie (albo półautomatycznie) zmieniać we wszystkich plikach. Pół biedy jeśli to malutka aplikacja – schody zaczynają się, kiedy mamy do czynienia z większym serwisem. Na szczęście jest na to sposób. Chciałbym Wam dzisiaj przedstawić rozwiązanie, które w znaczny sposób może ułatwić zarządzanie adresami URL. W wersji trzeciej, CodeIgniter ma udostępniać taką funkcjonalność „out of the box” (przynajmniej takie są plany), ale póki co, musimy się tym zająć samodzielnie.

Oczywiście nie zdziwicie się pewnie, że ktoś już poświęcił chwilę czasu i spłodził odpowiedni kawałek kodu. Autorem rozwiązania, z którego będziemy korzystać jest AJ Heller. Wymagane zmiany ograniczają się właściwie tylko do nadpisania funkcji site_url z helpera URL. Tak więc zaczynamy.

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * DrF Reverse Routing
 *
 * @author      AJ Heller <aj@drfloob.com>
 * @link        http://drfloob.com/
 */

// ------------------------------------------------------------------------

/**
 * (Reverse) Site_URL
 *
 * Returns a custom routed URL if one exists.  Returns a normal site_url otherwise.
 * It works by translating the passed-in URI into a custom route URI, if possible.
 * This function does not handle ANY regex used without capture-groups and back-references.
 * Visit http://drfloob.com/codeIgniter/reverse_redirect to learn why
 *
 * @access   public
 * @param    $uri    The standard CI URL, e.g. controller/function/param1
 * @param    $method
 * @param    $http_response_code
 */

    function site_url($uri = '')
    {
        $Router =& load_class('Router');
        
        // $uri is expected to be a string, in the form of controller/function/param1
        // trim leading and trailing slashes, just in case
        $uri = trim($uri,'/');
        
        $routes = $Router->routes;
        $reverseRoutes = array_flip( $routes );
        
        unset( $routes['default_controller'], $routes['scaffolding_trigger'] );
        
        // Loop through all routes to check for back-references, then see if the user-supplied URI matches one 
        foreach ($routes as $key => $val)
        {
            // bailing if route contains ungrouped regex, otherwise this fails badly
            if( preg_match( '/[^\(][.+?{\:]/', $key ) )
                continue;
                
            // Do we have a back-reference?
            if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
            {
                // Find all back-references in custom route and CI route 
                preg_match_all( '/\(.+?\)/', $key, $keyRefs );
                preg_match_all( '/\$.+?/', $val, $valRefs );
                
                $keyRefs = $keyRefs[0];
                
                // Create URI Regex, to test passed-in uri against a custom route's CI ( standard ) route
                $uriRegex = $val;
                
                // Extract positional parameters (backreferences), and order them such that
                // the keys of $goodValRefs dirrectly mirror the correct value in $keyRefs
                $goodValRefs = array();
                foreach ($valRefs[0] as $ref) {
                    $tempKey = substr($ref, 1);
                    if (is_numeric($tempKey)) {
                        --$tempKey;
                        $goodValRefs[$tempKey] = $ref;
                    }
                }
                
                // Replaces back-references in CI route with custom route's regex [ $1 replaced with (:num), for example ]
                foreach ($goodValRefs as $tempKey => $ref) {
                    if (isset($keyRefs[$tempKey])) {
                        $uriRegex = str_replace($ref, $keyRefs[$tempKey], $uriRegex);
                    }
                }
                
                // replace :any and :num with .+ and [0-9]+, respectively
                $uriRegex = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $uriRegex));
                
                // regex creation is finished.  Test it against uri
                if (preg_match('#^'.$uriRegex.'$#', $uri)){
                    // A match was found.  We can now build the custom URI
                    
                    // We need to create a custom route back-referenced regex, to plug user's uri params into the new routed uri.
                    // First, find all custom route strings between capture groups
                    $key = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $key));

                    $routeString = preg_split( '/\(.+?\)/', $key );
                    
                    // build regex using original CI route's back-references
                    $replacement = '';
                    $rsEnd = count( $routeString ) - 1;
                    
                    // merge route strings with original back-references, 1-for-1, like a zipper
                    for( $i = 0; $i < $rsEnd; $i++ ){
                        $replacement .= $routeString[$i] . $valRefs[0][$i];
                    }
                    $replacement .= $routeString[$rsEnd];
                    
                    /*
                             At this point,our variables are defined as:
                                $uriRegex:        regex to match against user-supplied URI
                                $replacement:    custom route regex, replacing capture-groups with back-references
                                
                            All that's left to do is create the custom URI, and return the site_url
                    */
                    $customURI = preg_replace( '#^'.$uriRegex.'$#', $replacement, $uri );
                    
                    return normal_site_url( $customURI );
                }

            }
            // If there is a literal match AND no back-references are setup, and we are done
            else if($val == $uri)

                return normal_site_url( $key );
        }
        
        return normal_site_url( $uri );
    }

    function normal_site_url($uri = '')
    {
        $CI =& get_instance();
        return $CI->config->site_url($uri);
    }

/* End of file MY_url_helper.php */
/* Location: ./application/helpers/MY_url_helper.php */

Powyższy kod zapisujemy w pliku application/helpers/MY_url_helper.php. To tyle – możemy przejść do używania tego rozwiązania. Co się zmienia? Od strony implementacji naszych adresów URL w aplikacji – nic. Dosłownie. Wszystko pozostaje tak, jak dotychczas. „Magia” zaczyna się w momencie przejścia do pliku application/config/routes.php. Możemy teraz określać jak będą wyglądać adresy, które zostaną wygenerowane przez helper URL. Dotychczas zasada była jasna – mogliśmy mieć jakiekolwiek adresy chcemy, ale trzeba było je zaimplementować w naszej aplikacji na sztywno. Teraz zmiana wszystkich adresów odbywa się w pliku routes.php. Oczywiście zakładając, że do generowania adresów URL w obrębie naszej aplikacji używamy helpera URL (co jest wskazane).

No dobrze załóżmy więc, że w domyślnym widoku CodeIgnitera (application/views/welcome_message.php), dodamy taki fragment kodu (nie zapomnij załadować helpera URL w kontrolerze albo dodać go do tablicy auto-ładowania):

<?php echo site_url('controller/method'); ?>

Otwórzmy teraz domyślną stronę naszej instalacji CodeIgnitera (kontroler Welcome). Z powyższego zapisu powinniśmy uzyskać adres podobny do tego:

http://localhost/adres_projektu/index.php/controller/method

Teraz dodamy linijkę kodu do wspomnianego wcześniej pliku routes.php:

$route['kontroler/metoda'] = 'controller/method';

Jeśli odświeżymy naszą domyślną stronę, to powinniśmy ujrzeć zmieniony adres, podobny do tego:

http://localhost/adres_projektu/index.php/kontroler/metoda

Prawda, że prosto i przyjemnie? Normalnie musielibyśmy zmienić adres w widoku, ale teraz nie ma takiej potrzeby, ponieważ robi to za nas helper URL. Oczywiście w regułach w pliku routes.php możemy również używać wildcardów :num i :any, tak jak dotychczas. Jedynym ograniczeniem tego rozwiązania, jest brak wsparcia dla wyrażeń regularnych przy definicji adresów URL, ale w większości przypadków bez problemu można się bez tego obejść.

No i na koniec, należy pamiętać, że „przepisywanie adresów” odbywa sie tylko dla funkcji site_url – na szczęście nic nie stoi na przeszkodzie, aby używać tej funkcji w połączeniu z innymi funkcjami, które odpowiadają za generowanie adresów URL, np. form_open z helpera Form ;)

Dodaj komentarz

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.