Routes

Overview

In a simple PHP application you might have a stand-alone PHP file for each page such as /customer.php, /product.php, etc and each script is executed when it's requested by name in the URL. With a Phreeze application (like many modern web apps) the URLs of the web app do not necessarily relate to a specific PHP file with the same name. Instead the URLs are "virtual" and all requests go through one PHP file (usually index.php). This entry-point script analyzes the URL and then decides what functions to execute. In order to get all requests to go through one single file, you utilize the "rewrite" feature of your web server. For example, Apache can be configured using an .htaccess file.

For these virtual URLs to work, our index.php file has to know which function to call when a particular URL is requested. In other words, we need to map public URLs to PHP classes, methods or functions in our app. In a Phreeze application the variable GlobalConfig::ROUTE_MAP holds this information. By default this is configured in the _app_config.php file.

The route map is really just a specially formatted array of key/value pairs. The key is a URL pattern and the value is the 'route' to execute. Let's take a look at a simple route map:

GlobalConfig::$ROUTE_MAP = array(

	'GET:customers' => array('route' => 'Customer.View'),
	'POST:product' => array('route' => 'Product.Insert')
	
);

This route map has exactly two routes and they each are in the following format:

'[VERB]:[URL]' => array('route' => '[CONTROLLER].[METHOD]')

The first route in the above example 'GET:customers' => array('route' => 'Customer.View') might look like this in your browser: http://localhost/customers. When Phreeze encounters this URL it will instantiate the 'CustomerController' class and fire a method called View() on that class.

The second route in the example 'POST:product' => array('route' => 'Customer.Insert') is slightly different. Notice that it begins with 'POST' instead of 'GET'. This tells Phreeze that this route only matches POST requests from the browser. So, simply typing the URL http://localhost/product into your browser will not trigger the route. POST requests are usually the result of either submitting a form, or an AJAX request. In this example, a POST request to http://localhost/product would fire ProductController.Insert(). The common HTTP verbs used in web apps are GET, POST, PUT and DELETE and in your route map you can handle them all separately.

URL Parameters

The previous example allowed you to map a URL to a controller class so long as there was an exact match. However in a typical application you will have parameters as part of your URL, for example: http://localhost/api/sales/customer/25. Most likely this URL would have something to do with a customer record with an ID of 25. But, how would we get the route to respond to any ID number such as 25, 26 27, etc? This is done using wildcard patterns in the route map like so:

GlobalConfig::$ROUTE_MAP = array(

	'GET:api/sales/customer/(:num)' => array(
		'route' => 'Customer.View'
	)
	
);

Notice that (:num) is on the end of the URL. This tells Phreeze to map any URL that matches the pattern http://localhost/api/sales/customer/(:num) where (:num) is a numerical value.

This solves the problem of routing the URL to the appropriate controller, but we now have another issue. From within our controller code, we need to get the value of that parameter. In other words, we need to get the ID for the customer from the URL so that our controller method knows which Customer object is being requested. Let's add some code to the route map:

GlobalConfig::$ROUTE_MAP = array(

	'GET:api/sales/customer/(:num)' => array(
		'route' => 'Customer.View', 
		'params' => array('customerId' => 3)
	)
	
);

We've added a second key called 'params' to the route array. Before we look at that, let's take a moment to analyze the URL from the persepective of the Router. Using the forward slash / character as a delimiter, the URL http://localhost/api/sales/customer/25 would be split into 4 parts:

0 = api
1 = sales
2 = customer
3 = 25

Given the URL above, our controller would likely be interested in obtaining the value '25' without without manually parsing the URL. Lets take another look at the 'params' key: 'params' => array('customerId' => 3) What this tells Phreeze is that the item of the exploded URL at position 3 is going to be assigned a name of 'customerId'. Notice that this is a zero-based array, so the count starts at 0 instead of 1.

To make things more clear, let's take a look at how we access that from within the Controller:

class CustomerController extends Controller
{
	public function View()
	{
		// get the value of customerId from the router
		$id = $this->GetRouter()->GetUrlParam('customerId');
	}
}

The controller is able to get the value '25' only using the assigned name of 'customerId' so it doesn't need to know anything about the format of the URL.

Why use all of this abstraction and not just access the URL directly from within the controller? The reason is so that the Controller is not tightly coupled with the URL. This allows us flexibility to later change URLs without re-writing controller code. The Router is the only class that has to understand the URLs and route map and can provide information to the controller in a more abstract manner. This strategy also makes unit testing easier because we can test our controller methods from the command-line and use a mock router to provide information to the controller. The controller won't know or care whether it is running in a web environment or from the command line.

Wildcard Patterns

In the previous example we used the pattern (:num) as a placeholder for any valid number in the URL. What if the ID we want is not a numerical value? We can also use (:any) to match any character in the URL for example:

GlobalConfig::$ROUTE_MAP = array(

	'GET:/customer/(:any)' => array(
		'route' => 'Customer.View', 
		'params' => array('customerCode' => 1)
	)
	
);

All of the following URLs would match in the above example: http://localhost/customer/aaa, http://localhost/customer/123, http://localhost/customer/zzz

Order of Operations

One word of caution about using the (:any) pattern is that Phreeze will return the first match that it finds. In the example below, the 2nd route will never be hit because the pattern above it will be matched by the same URL. When two routes match the same URL, Phreeze will always use whichever one is first.

GlobalConfig::$ROUTE_MAP = array(

	'GET:/customer/(:any)' => array(
		'route' => 'Customer.View', 
		'params' => array('customerCode' => 1)
	),
	
	// THIS ROUTE WILL NEVER BE HIT!
	'GET:/customer/update' => array(
		'route' => 'Customer.Update'
	)
	
);

If you were to reverse the order of the two routes above, then the 'update' route would be hit and the (:any) route would be hit for any other match.

More Information

The route map array is process by a class in the Phreeze library names "GenericRouter" This class is an implementation of the IRouter interface. You can write your own implementation of IRouter to process your own specialized routes.

Classes that use the Router are the Controller and the Dispatcher.