Controller Routing

Hi guys.

Are there any good examples of controller routing/mapping I could take a look at?

I’m trying to write my own, but at the moment its a bit too strict as in you have to define the entire url and then explicitly set the routing parameters.

What I want to be able to do is have wildcards rather than having to define every possible solution/outcome in the router config.

Cheers.

You’re absolutely right - I should not have used filesize as a measure of “lightweight”. The true measure is indeed the actual resources used - memory usage, how many files have to be loaded, how many actual lines of code have to be executed, etc. to get the same output as whatever it is being compared to.

Also - if you’re interested - Two tools that are very useful in gauging code “size”, quality, and complexity are phploc and [URL=“http://github.com/sebastianbergmann/phpcpd”]phpcpd. I use them to scan all my projects regularly.

And I quote, “CodeIgniter is written to be compatible with PHP 4”

Emphatically not interested. I’ve seen the nighmarish work arounds PHP 4 often requires to get simple things to work under the hood.

I put up with language constraints enough with Javascript on the client side. I’ll be damned if I put up with the same headache of catering to antiquated language versions server side.

Examples can always be concocted to break the system, and there are always exceptions to the rule. If you truly have URLs that cannot be matched by any pattern, then I’d argue you have bigger problems than URL routing.

If there is a good reason for it, like pages in a CMS that can have any URL and are designed to look like a browsable folder structure, etc. then you can still solve it with a wildcard match route. Just make your very last route a single wildcard parameter, and then send it off to your “pages” controller to handle (to lookup the URL alias in a pages database table to find the page, etc.). You can even use the wildcard “catchall” route to run it through a final/legacy URL parser that uses explode, etc. if the patterns are for some reason not good enough.

That said, if you don’t like the pattern matching routes, just stick with exploding the URL by slash or whatever you currently do. I have found the pattern matching to be by far the most flexible and elegant solution out of everything I have tried so far - and believe me, I have been very far down the “explode the URL by slash” path, and it gets very ugly. Routing tables with pattern matching is the best way to de-couple the routes from the controllers. Your router’s job is to parse the URL into segments or named parameters - not to load or instantiate controllers - that’s the job of the dispatcher.

Examples can always be concocted to break the system, and there are always exceptions to the rule. If you truly have URLs that cannot be matched by any pattern, then I’d argue you have bigger problems than URL routing

It’s hardly a concocted example… any decent online shopping cart will need to handle products in heirachical categories, the URIs will inevitably look the same unless you actively work around it somehow (e.g. messily appending an _ to the end of products).

I just don’t think a pattern based route table is as flexible as you’re suggesting it is. Whether you use pattern matching or exploding on / there are cases where it breaks down. The difference is, using the explode method only these edge cases need to specifically coded for. If you have a routing table, every route needs to be coded and then you still need to cater for edge cases.

Route matches typically go from specific to generic as you go down, because they run on a first-match rule - The first route that matches the input URL gets to handle it. So in the typical setup, there are fallback routes that do match the generic :controller/:action/:id and :controller/:action, etc. that will get used if nothing else matches before it. So yes - the idea is not to make you specify every single route to every single controller, but to give you the flexibility to do so if you want, or let it fall back to the generic defaults if you don’t want to add specific routes for whatever you’re handling. In most cases, you’ll end up with a half a dozen or so custom routes and let everything else fall back to the generic controller/action/id convention.

But the URL could end with either a category or a product. You have no way to know which it is.

Using an ending / as a notifier is bad. You wouldn’t want:

/accessories/bags/alpinestars

to initiate the product controller and have

/accessories/bags/alpinestars/

initiate the category controller as they’re the same page (and the first would cause a “product not found” error). There’s also usability issues presented by forcing a trailing slash as a required element. One should forward to the other for SEO but having /foo/bar different to /foo/bar/ is bad.

The trouble with that approach is that it’s terribly inflexible. You’re essentially hard-wiring the URL to a pre-determined set structure. URLs are meant to represent a browse-able hierarchy of resources that cascade down, i.e:

http://autoridge.com/2008

This type of structure would be impossible with a hard-wired “explode” approach, or else would require splitting up functionality into specifically named modules/controllers for it to work - not even counting the fact that PHP won’t let you start variable, function, or class names with numbers, so the “2008” portion might very well be impossible, depending on the framework’s naming conventions.

As for the ‘admin’ prefix, it could be either included in the route as an optional parameter, or detected before being parsed by the router, and added as a special flag to the dispatcher. There are many possible ways to achieve this functionality.

That’s right - that is exactly what I do for vehicle display pages, and there are indeed 3 different routes to satisfy those paths. Ideally, I can get to the point where I can only use a single route with two optional parameters - like rails does - but sadly I am not quite there yet (currently can’t nest optional params).

:controller(/:action(/:id))

Or for our vehicle example:

:year(/:make(/:model))

It’s not routing actual static pages, it’s making static routes. There is a big difference. A static route would be mapping something like “/login”, “/logout”, “/register”, “/about”, etc. to a specific controller and action to handle it. This comes in very handy if you want better-looking URLs for common things mapped to a specific controller/action that already exists or doesn’t match the typical controller/action/id route style.

Example of a login route:


$router->route('login', '/login')
	->defaults(array(
		'module' => 'User_Session',
		'action' => 'new',
		'format' => 'html'
	));

It is not for general escaping. It is a broken implementation for escaping strings for SQL to be sent to MySQL. And it is broken for that as well. What you “belive” is wrong. That is all I’m going to say further on this as it is off topic.

if this helps :


$do = $_GET['do'];
$do = mysql_escape_string($do);

if (file_exists($do.'/index.php'){
include($do.'/run.php');
}

something like that … you should b having " modules " folder for say, and each categorie should b in seperatd folder contained inside wiz index and run .php

Off Topic:

@Egyptechno: Why would you use mysql_escape_string() for something you’re not sending to the database?
And how about checking paths such that I can’t provide $_GET[‘do’] as ‘…/…/some/path/i-m/not/supposed/to/access’ ?

My boss has a bad habit of mismanaging available programming time and hence his deadlines are never met because they are not realistic - then he gets mad and yells at the team. We are in the middle of a major rollout so, if I’ve been extra testy of late I apologize.

A few quick points as a response:

These issues aside, I do know where you’re coming from, and I too feel that many frameworks are too heavy, try to do too much, or hide too many things from you. That’s exactly why I made Alloy. We’re in the same boat here, believe it or not. The key is to find a balance and always focus on real-world usage. Framework projects that don’t do that are obvious to spot, so just avoid those particular ones or just don’t use a framework at all.

Well, I know nothing about Alloy of course. Gazelle (Formerly PAMWF) is just now getting its error and exception routines finished so isn’t ready for primetime. I’m trying to stay beneath 10 file loads not including the templates for normal execution, and beneath 2MB for file memory use which is what Joomla and vbulletin suck up roughly. Right now it’s hovering around 703 KB. It’s being designed as a programmer’s CMS, idea for building something onto, not idea for shop and plug solutions like Joomla and Drupal offer. I find the hook systems those products have in place to allow for that ease of use by the end user to create nightmarish difficulty for the programmer that has to work with the code.

I’m also aiming the framework to be an idea step 2 for beginning programmers - step 1 being general syntax and control flow mastery. I think I can accomplish these goals but we’ll see. If the framework starts getting to difficult for beginners I’ll make a simplified version because, currently, there is no good beginner’s framework which is sad.

Here’s my router described earlier.


public static function parseRequest() {

		$path = array_filter(explode('/', $_SERVER['REDIRECT_URL']));
		$pages = Core::$pages;
		
		if ( pathinfo($_SERVER['REDIRECT_URL'], PATHINFO_EXTENSION )) {
			$event = pathinfo($_SERVER['REDIRECT_URL'], PATHINFO_FILENAME );
			array_pop($path);
		} else {
			$event = 'index';
		}
		
		$path = array_reverse($path);
		$path[] = '';
		$found = array();

		do  {
			$p = array_pop($path);

			foreach ($pages as $page) {
				if ($page['attributes']['name'] == $p) {
					$found = $page;
					$found['pathAfter'] = $path;
					break;
				}
			}
			
			if (isset($found['children'])) {
				$pages = $found['children'];
			}
		
		} while (count($path) > 0);
		
		if (empty($found)) {
			die('No Homepage Defined.');
		}
		
		$class = $found['attributes']['class'];
		
		Core::$page = new $class( $found );
		Core::$page->parseEvent( $event );
	}

Eventually the die statement will become a proper exception object, but I’m in the middle of a rewrite and I want things to die hard when they go wrong. The array_filter call at the top addresses the issue of url crap like [noparse]http://www.site.com///i//don’t//really/pay/much///attention//to/slash/placement////[/noparse]

All the empty elements are pulled by the filter. After reversing the array I do put one back, but that for is the home page controller which is the only page in the system with no name.

Core::$pages is pulled from the database in development, or from the master cache file in production. The function above is part of the Core class.

It’s simple and works well enough. Any additional fidgeting with the page can be done by the real controller, the Core method’s only job is to accurately find that file.

The problem is that the URIs may not be pattern matchable. The examples I supplied would both match the same patterns yet need to go to different controllers. It’s more than likely the pages are added dynamically, which means you’ll end up with more than a half dozen, and you’ll need to add a route for each product/category in the system.

Thanks for the responses guys.

I have my router pretty much done, the only issue at the moment is only regular expression that’s causing the headaches.

Thanks guys

I think that’s a terrible solution. You are again forcing a specifically named controller to handle a specific request when it should just be handled by it’s own component.

You would handle this with wildcard parameters or your own custom regex rule:


// <*param> = Wildcard capture (.*) - matches anything, including directory separators
$router->route('product_page', '/<*category_path>/<:product>')
    ->defaults(array('module' => 'products', 'action' => 'view'));

You would then get two params to pass onto your product controller’s “view” action - ‘category_path’, and ‘product’. The product controller could then explode the category_path or do whatever else it needs to do with it, and lookup the product by alias. This same logic applies to your search filter example. Wildcard parameters are very useful for these kinds of edge cases.

Hello Michael,

basically you can choose regexps or progressive parsing.

Regexps are fine but their scalabity is IMHO worse. Having a route

/:component/:categories/:actions

matched e.g. by URLs

/articles/USA/latest
/articles/astronomy/latest
/events/music/show

means that you probably have to compose more regexps, because each component could have different categories and actions.

Progressive scanning and matching (part by part) is probably better, but I did not do any test on this subject.

I think routing tables like that are messy. I prefer a “best match” approach similar to what Mastodont was implying

e.g.

/admin/users/edit/12

explode on /

does module “admin” exist?
no - call $defaultModule / $defaultController / $defaultAction with parameters “admin”, “users”, “edit”, “12”
yes - set admin module then:

does controller “admin/users” exist?
no - call admin module / $defaultController / $defaultAction with parameters “users”, “edit”, “12”
yes - initiate admin/users controller then:

does method “edit” exist on the users controller?
no - call admin module / users controller / $defaultAction with parameters “edit”, “12”
yes - call admin module / users controller / “edit” action with parameters “12”

Add and replace levels as necessary by the application but this approach is better because once the router is set up it requires zero configuration yet you can specify defaults where required or create a very minimal routing table if you need to do something drastically different.

The URL Router in [URL=“http://alloyframework.org/”]Alloy Framework (currently a work-in-progress) might be of some use to you. You can use Alloy\Router and Alloy\Router\Route independent of the framework itself, because there is no coupling or outside dependencies.

Basically, It uses a routing syntax like this:


// View vehicle record
$router->route('vehicle', '/<#year>/<:make>/<:model>')
	->defaults(array(
		'module' => 'Vehicle',
		'action' => 'view',
		'format' => 'html'
	));

And does a string match on a URL to produce an array of key/value pairs like this:


// URL like this
$url = $_SERVER['REQUEST_URI']; // '2008/ferrari/f430'

// Match URL to provided routes
$params = $router->match('GET', $url); // <HTTP Method, URL>

/*
// Resulting params would look like this:
$params = array(
	'year' => '2008',
	'make' => 'ferrari',
	'model' => 'f430',
	'module' => 'Vehicle',
	'action' => 'view',
	'format' => 'html'
);
*/

It also does reverse matching to produce URLs for links:


// To produce '2008/ferrari/f430' again
$linkUrl = $router->url('vehicle', array(
	'year' => '2008',
	'make' => 'ferrari',
	'model' => 'f430'
));

It’s the only PHP URL router I know of that is this easy and flexible, is REST-based, and has no other framework inter-dependencies. It was modeled after the URL router in Merb and Rails.