SitePoint Sponsor

User Tag List

Results 1 to 13 of 13
  1. #1
    SitePoint Wizard
    Join Date
    Mar 2002
    Location
    Bristol, UK
    Posts
    2,240
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    An idea for router implementation, feedback wanted

    I'm trying to get my head around a decent MVC router implementation. At present my router uses the rather restrictive /controller/action/params/ style which works perfectly well for half of my URLs, and requires horrible ugly switch statement hacking for the other half, so I'm pretty eager to come up with a new method for interpreting requests.

    Realistically, a static file containing routing patterns and rules, however flexible it would make the application, is out of the question, because new modules should be able to be simply slotted in with zero configuration.

    I'm using two URLs here as examples (both of which are incompatible with my current setup described a moment ago).

    /content/articles/create/
    /pages/123/settings/

    I was thinking that the routing could be done on a kind of trial and error basis. For each URL component, the router can ask the same questions, in the same order, stopping and moving onto the next component as soon as a 'yes' answer is returned:

    1. is the component the name of a class that I can instantiate?
    2. is the component a method that is defined in the previous class?

    If the answer to both of these questions is 'no', it's probably safe to assume the component refers to a specific resource (like an ID in the database). Maybe it would be sensible to enfore a rule that resource IDs should be numeric, and check this when parsing the URL.

    So if we look back at the first example URL I gave a minute ago, content and articles are both classes, and create is a method (in the previous class). In the second example, pages is a class, 123 is a resource ID, and settings is another class which instantiates with that ID, or something.

    Here's a VERY BASIC draft of how it could possibly be implemented. Obviously it would need to be expanded to allow for multiple classes (maybe even multiple methods...?) to be called, but you should get the idea of what I'm trying to do.

    PHP Code:
    $components explode('/'$_SERVER['REQUEST_URI']);

    foreach(
    $components as $component) {

        if(
    class_exists($component)) {
            
    $class $component;
        } elseif(
    method_exists($class$component)) {
            
    $method $component;
        } elseif(
    is_int($component)) {
            
    $resource $component;
        }


    I haven't worked out 100% of the logistics yet but I'm guessing that some sort of validation process will need to take place, to stop someone from daisychaining two completely unrelated classnames together in the URL, for example.

    What do people think? Is this a half-decent way of parsing URLs or is this likely to cause problems further down the line?

    Looking forward to hearing people's input and, most importantly, criticism (please be nice though)

    Sam

  2. #2
    SitePoint Guru dagfinn's Avatar
    Join Date
    Jan 2004
    Location
    Oslo, Norway
    Posts
    894
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    This looks interesting and reasonable to me, except that you need to make sure it's secure. I suppose you've already thought of that.
    Dagfinn Reiersøl
    PHP in Action / Blog / Twitter
    "Making the impossible possible, the possible easy,
    and the easy elegant"
    -- Moshe Feldenkrais

  3. #3
    SitePoint Addict Mastodont's Avatar
    Join Date
    Mar 2007
    Location
    Czech Republic
    Posts
    375
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Explode and sub-parts comparing is a perfect way to go, IMHO better than regexps. But implementation using class_exists is too limited, allows only one route pattern and excludes URLs in other languages (yes, you probably do not need this).

    You should further take into account at least categories, implicit parts of URL ( e.g. /login -> controller user, action login) and static routes ( /hotnews -> controller X, action Y).

  4. #4
    Twitter: @AnthonySterling silver trophy AnthonySterling's Avatar
    Join Date
    Apr 2008
    Location
    North-East, UK.
    Posts
    6,111
    Mentioned
    3 Post(s)
    Tagged
    0 Thread(s)
    Wouldn't this bust it?
    PHP Code:
    <?php
    class View
    {
        public function 
    Page();
    }

    class 
    Page
    {
        public function 
    View();
    }

    # /view/page/view this would view a page about the view from my window
    ?>
    In the foreach loop, View is an object and Page is method within that object, great all stuff satisfied.

    But unless you break out of the loop, it would move onto segment 2, Page, which is also an object and it contains a method called View.

    A little ambiguous, no?

    Is it your intention to have the latter end of the URL override previous segments?
    @AnthonySterling: I'm a PHP developer, a consultant for oopnorth.com and the organiser of @phpne, a PHP User Group covering the North-East of England.

  5. #5
    Spirit Coder allspiritseve's Avatar
    Join Date
    Dec 2002
    Location
    Ann Arbor, MI (USA)
    Posts
    648
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I would suggest having an array of keys and values, so you are not so locked down on class names:

    PHP Code:
    $routes = array (
    'page' => 'Pages_Controllers_Standard',
    'admin' => 'Admin_Controllers_Home'
    );
    // etc... 
    That might not be 'zero configuration' as you said, but you can then reuse controllers and can have them located anywhere in your system (rather than throwing them all in a system-wide folder of controllers)

  6. #6
    SitePoint Wizard
    Join Date
    Mar 2002
    Location
    Bristol, UK
    Posts
    2,240
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks for taking the time to read and reply, everyone!

    Quote Originally Posted by Mastodont View Post
    Explode and sub-parts comparing is a perfect way to go, IMHO better than regexps. But implementation using class_exists is too limited, allows only one route pattern and excludes URLs in other languages (yes, you probably do not need this).
    Not something I'd thought of, but thanks for mentioning it. If I were to introduce multiple language support (which is unlikely, but I guess you should never say never), I'd probably specify that in a querystring instead: ?lang=en-gb for example.

    You should further take into account at least categories, implicit parts of URL ( e.g. /login -> controller user, action login) and static routes ( /hotnews -> controller X, action Y).
    Good point, maybe a static routing file in XML format could also be included, which would take precedence over the other rules.

    Quote Originally Posted by SilverBulletUK View Post
    Is it your intention to have the latter end of the URL override previous segments?
    Another good point I'll make sure there are some hard and fast rules for determining priority for situations like this.

    Quote Originally Posted by allspiritseve View Post
    I would suggest having an array of keys and values, so you are not so locked down on class names:

    PHP Code:
    $routes = array (
    'page' => 'Pages_Controllers_Standard',
    'admin' => 'Admin_Controllers_Home'
    );
    // etc... 
    That might not be 'zero configuration' as you said, but you can then reuse controllers and can have them located anywhere in your system (rather than throwing them all in a system-wide folder of controllers)
    Well, there probably will be a system-wide folder of controllers, but like I've said in response to Mastodont's post, maybe it's a good idea to allow class names to be overridden as and when necessary.

    Thanks once again guys, your input is much appreciated

  7. #7
    SitePoint Zealot
    Join Date
    May 2008
    Location
    Montreal
    Posts
    155
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I have found that when searching for the appropriate route one needs to be somewhat more pessimistic. Consider the following routes which are valid for most blogs:

    Code:
    /2007/08/10/this-is-a-blog-post
    /2007/08/10
    /2007/08
    /2007
    Each of the above routes are valid in terms of my blog and also in terms of how many blogs work. Suppose that you predicate that will tell us if a everything up to the current part of a route is a valid route. Clearly, as shown above, /2007 is a valid route; however, if I wanted to go and look at a specific blog post and not the blog archives for the year 2007 then by your current approach I would not be able to, as your approach is optimistic: you accept the first working route. Thus, you will need to work a bit harder to exhaust all of your options so that you accept the longest route that you can.

    I lay it out in psuedo code.

    Code:
    valid_route(String) := a predicate that tells us if a route is valid
    
    Let R:String := "/2007/10/08/moo",
         Q:Queue := new Queue(split R '/'),
         T:String := "", # our temporary route, we will add parts into this incrementally,
         L:String := "", # our longest route so far found.
    
    while not empty Q:
        T += pop Q # append a route part onto our temporary route
        if not valid_route T:
            break
        ;
        L = T
    ;
    
    if L is "":
        goto index page
    ;
    # L now holds the route with which to dispatch
    I've made the assumption that you have some means of remapping routes. It should be noted, however, that remapping routes is, for the most part, only useful if you are not sticking to strict RESTful principles. In the above example, a RESTful URL would likely be more along the lines of (someone correct me if I am wrong):

    Code:
    /article/this-is-a-blog-article
    /archive/?year=2007&month=08&year=07
    /archive/?year=2007&month=08
    /archive/?year=2007
    Otherwise, good luck with your router, I hope it works out well!

  8. #8
    SitePoint Wizard bronze trophy
    Join Date
    Jul 2006
    Location
    Augusta, Georgia, United States
    Posts
    4,147
    Mentioned
    16 Post(s)
    Tagged
    3 Thread(s)
    Quote Originally Posted by allspiritseve
    I would suggest having an array of keys and values, so you are not so locked down on class names:
    I would also recommend using names that map to specific controllers.

    You could do this with a array as allspiritseve mentioned or the database itself.

    I prefer to keep it simple stupid:

    /blog/1/5

    Blog would become the controller name because it is the first item. That name would be mapped to the associated controller. if a controller didn't exists for the name then a error would occur.

    Everything that follows the 1st argument(option) is mapped to the appropriate name based on the magical options for the controller.

    If the name of any one of the magical arguments is called method then that method will override the default for the controller if it exists.

    route: /thread/1/3

    break down
    1. thread (abstract name)
    2. 1
    3. 3


    Controller Options (thread)
    1. entry
    2. page


    option mapping
    1. entry => 1
    2. page => 3


    controller abstract name: thread

    thread then maps to some class that is an instance of AppController.

    A singleton called pathway is responsible for mapping the controller options to the appropriate name. This replaces the $_GET array.

    PHP Code:
    $this->pathway->hasOption('entry');

    $entry $this->pathway->getOption('entry'); //1 
    That code would go into the controller and basically replaces the $_GET array.

    This also makes it possible to use the same route but replace certain options. That is the function of the makeUrl method of the Pathway class.

    PHP Code:
    echo $pathway->makeUrl(array('entry'=>6)) =>  /thread/6/
    or to create a link to the next page perhaps:

    PHP Code:
    echo $pathway->makeUrl(array('page'=>4)) => /thread/1/
    or remove certain options. maybe for a link back the first page.

    PHP Code:
    echo $pathway->makeUrl(array(),array('page')) => /thread/
    This also makes the routes near impossible to hack. The makeUrl method will only generate a url based on valid options for the controller and its name. So a route like:

    route: /thread/1/3/4/908/hjuy/89765

    That would still work but everything that doesn't map to a a option would be dropped.

    Given that route the makeUrl method would still only use and generate a url based on the valid options.

    PHP Code:
    echo $pathway->makeUrl(array(),array('page')) => /thread/1  // notice everything else has been dropped 
    Last edited by oddz; May 6, 2009 at 22:11.

  9. #9
    SitePoint Addict Mastodont's Avatar
    Join Date
    Mar 2007
    Location
    Czech Republic
    Posts
    375
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    For routes with huge number of params you can implement named parameters, too:
    http://book.cakephp.org/view/46/Routes-Configuration
    Last edited by Mastodont; May 7, 2009 at 00:18.

  10. #10
    SitePoint Guru dagfinn's Avatar
    Join Date
    Jan 2004
    Location
    Oslo, Norway
    Posts
    894
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thinking abstractly about this problem, it's a multi-strategy, multi-rule kind of approach where a general clean solution might involve implementing different strategies in different classes and having an overall strategy to coordinate them. A perhaps extreme solution would be the Blackboard pattern.
    Dagfinn Reiersøl
    PHP in Action / Blog / Twitter
    "Making the impossible possible, the possible easy,
    and the easy elegant"
    -- Moshe Feldenkrais

  11. #11
    SitePoint Wizard silver trophybronze trophy Stormrider's Avatar
    Join Date
    Sep 2006
    Location
    Nottingham, UK
    Posts
    3,133
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    This is an interesting read - thanks. I'm trying to find a decent way to route my requests as well.

  12. #12
    SitePoint Zealot
    Join Date
    May 2008
    Location
    Montreal
    Posts
    155
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    You can see one implementation that is able to parse a large number of routes here: http://ioreader.com/code/php/route-parser.phps

    The default method of operation for that parsing is a direct mapping between the controller directory structure and file structure. The other mode of operation is to re-map a route to a controller.

    The following code is an example of how it is used. The route parser doesn't assume anything about how controllers are stored. For example, in one project my controllers are classes, in another project my controllers are simply files and actions are functions within, and finally, in another project controllers are represented by a single function in a file. I have made parts of this code vague enough for it to represent any of the above possibilities.
    PHP Code:
    $router = new RouteParser('/path/to/base/controller/dir/');
    $route get_route(); // this is not provided, but the route is all or part of the URI
    $request_method get_request_method(); // GET, POST, PUT, DELETE

    // add the route remappings into the router
    include_route_remappings($router"/path/to/route/remappings/file.php");

    $path_info $router->parse($route);
    if(!
    $path_info)
        
    error_404();

    // get the controller, method, and arguments form the route parser
    list($dir$pdir$controller_name$action_name$arguments) = $path_info;

    // include the proper file, we expect the class/function name to be the same as
    // or a transformation of the file name (e.g. file-name.php => FileNameController)
    // by virtue of getting down here we *know* that this file exists, we
    // simply haven't included it yet.
    load_file("{$dir}/{$pdir}/{$controller_name}.php");

    $controller get_controller($controller_name);

    if(!
    $controller)
        
    error_404(); // controller doesn't exist

    // get the request-method-specific action for this controller
    $action get_action($controller$request_method);

    if(!
    $action)
        
    error_404(); // action doesn't exist

    // call the controller's action
    dispatch_to($action$arguments); 
    To get route-remappings into the router I usually have a single file for them that is included. By having the router stored in a local variable '$routes' (presumably within the function 'include_route_remappings') and then by including the routes file, I can take advantage of the RouteParser's ArrayAccess offsetSet to define route-remappings in a simple way. The following are the route-remappings I use for my blog.

    Code:
    $routes['/about'] = '/index/about';
    $routes['/profile/(:alphanum)'] = '/index/profile/$1';
    $routes['/find-comment/(:alphanum)'] = '/index/find-comment/$1';
    $routes['/tags'] = '/index/tags';
    $routes['/tags/(:any)'] = '/index/tags/$1';
    $routes['/(:year)'] = '/archive/index/$1';
    $routes['/(:year)/(:month)'] = '/archive/index/$1/$2';
    $routes['/(:year)/(:month)/(:day)'] = '/archive/index/$1/$2/$3';
    $routes['/(:year)/(:month)/(:day)/([a-zA-Z0-9-]*)'] = '/view/post/$4';
    $routes['/captcha'] = '/index/captcha';
    $routes['/books'] = '/index/books';
    A final note is that route arguments are best defined in route-remappings.

  13. #13
    SitePoint Wizard Ren's Avatar
    Join Date
    Aug 2003
    Location
    UK
    Posts
    1,060
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    PHP Code:
            $parts explode('/'trim($path'/'));
            
    $name $this->root;

            
    $i 0;
            while (isset(
    $parts[$i], $this->routing[$name][strtolower($parts[$i])]))
                
    $name $this->routing[$name][strtolower($parts[$i++])];

            
    $controller $this->getController($name);
            
    $controller->execute($locatorarray_slice($parts$i)); 
    Is what I came up with. Basically eats items at the beginning of the path, and sends the left overs to the controller to handle.

    PHP Code:
        $routing = array(
                
    'ObjectController' => array(
                    
    'data' => 'DataController',
                    
    'meta' => 'MetaController',
                    
    'history' => 'HistoryController'
                
    )
            ); 
    So /foobar would go to ObjectController, /data/foobar to DataController, and so on..


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •