webdevRefinery Forum: Hydrogen Router: An In-depth guide - webdevRefinery Forum

Jump to content

Hydrogen what, now?

Hydrogen is an open-source PHP toolkit that focuses on speed, with the goal of making your webapps faster if they're built on Hydrogen then they would be if written from scratch. It was created to be portable: applications written on Hydrogen should be able to be installed on many different types of servers with different database types or cache engines without changing any code or doing any extra programming work.

This forum contains the official tutorials for Hydrogen, until Hydrogen reaches version 1.0 and they move to HydrogenPHP.com. Please feel free to comment on them if you need help understanding them, or contribute your own tutorials that might later be published on the official site!
  • 2 Pages +
  • 1
  • 2
  • You cannot start a new topic
  • You cannot reply to this topic

User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 30 October 2011 - 10:18 AM (#1)

Hydrogen Router: An In-depth guide


This guide applies to Hydrogen versions 0.4.0 and higher.

Router? But I already got one from Verizon!
Router is the preferred method of matching URL requests to Controller objects, making aggressive use of caching to stay very quick, regardless of the number or complexity of rules defined. Many rules can be added to a Router instance without incurring any extra processing or lookup time.

Router runs by looking at the PATH_INFO
Not familiar? PATH_INFO is super easy to understand. Let's say you have a PHP script in the root of your domain called index.php. You might load this in your web browser:
http://example.com/index.php

But what's cool is that you can actually continue the path past the .php, and everything you type there will be available to the Hydrogen Router. For example:
http://example.com/index.php/archive/2011/01/24

If you request the above URL, your PATH_INFO in PHP will be
/archive/2011/01/24
, and the Router can look at that and determine which controller should handle the request.

Basic Router rules
Each "rule" in Router is simply a test to see if the request's URL (specifically, the PATH_INFO) matches a specific pattern, and if so, calling the appropriate Controller class and function to handle that request. Router rules can match all request types, or can be set to respond only to DELETE, POST, PUT, or GET HTTP requests.

The main goal of any routing rule is to end up with a variable named 'controller' that defines which class should be loaded, and a variable named 'function' that defines which function to call within the Controller class. This is a simple rule to direct requests to the root of the app ('/') to the welcome Controller's greeting() function:

$router = new Router();
$router->request('/', array(
    'controller' => 'welcome',
    'function' => 'greeting'
));
$router->start();


The supplied array of variables are the "defaults". These can easily be overridden by specifying that these variables should come from the URL:

$router->request('/:controller/:function');


The :variable format intercepts that specific piece of the request URL and assigns it to the name of the variable. In the example above, the Router rule will match any URL with two path "segments" (like /welcome/greeting) but ONLY if the specified controller and function actually exist. If not, Router will move on to the next rule.

Parts of a URL can be made optional by putting them in parentheses. The following rule will match a direct request to the root, or allow the URL to override the default controller and function:

$router->request('/(:controller(/:function))', array(
    'controller' => 'welcome',
    'function' => 'greeting'
));


With the above rule, a request to /login would look for a Controller class named 'login' and call its 'greeting' function.

Consider, though, the likelihood that the controller classes are namespaced and probably start with a capital first letter. To achieve this, overrides can be used. Just as variables in URLs take precedence over the 'defaults' array, overrides are applied before any controller function is called.

$router->request('/(:controller(/:function))', array(
    'controller' => 'welcome',
    'function' => 'greeting'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller'
));


Using a second array of overrides, it's easy to transform a value of 'welcome' for the controller into '\myapp\controllers\WelcomeController'. In the array of overrides, existing variables can be referenced with %variable or %{variable}. Additionally, it can be appended with a list of pipe-separated filters to affect a change on the variable contents. Legal filters:

- "capfirst" makes the first letter uppercase
- "upper" makes the entire string uppercase
- "lower" will lowercase the entire string.

Filters are executed in the order they are listed. The following override array will turn wELcOMe into Welcome:

array('controller' => '%{controller|lower|capfirst});


Additional arguments aside from 'controller' and 'function' can also be collected, and sent into the Controller's function as parameters. The following rule allows an optional "id" variable to be collected:

$router->request('/blog/:function(/:id)', array(
    'controller' => '\myapp\controllers\BlogController',
    'id' => 1
));


With the above rule, if /blog/page/4 were requested, Router would call

\myapp\controllers\BlogController::page('4');


--assuming, of course, that BlogController contains a function called 'page'. Otherwise, Router would try any following rules.


Collecting a variable number of arguments with variable groups
Groups of variables can also be captured by using the :*variable format. Consider the following rule:

$router->request('/(:controller(/:function(/:*args)))', array(
    'controller' => 'home',
    'function' => 'index'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
));


In the above case, if /blog/archive/2011/sept/10 is requested, Router would make the following call:

\myapp\controllers\BlogController::archive("2011/sept/10");


It's unlikely that the argument would be useful without being separated into each of its pieces, though, so Router provides two special overrides for this case. Overriding 'args' with
Router::EXPAND_ARRAY
like this...

$router->request('/(:controller(/:function(/:*args)))', array(
    'controller' => 'home',
    'function' => 'index'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
    'args' => Router::EXPAND_ARRAY
));


would cause this call to be made:

\myapp\controllers\BlogController::archive(array('2011', 'sept', '10'));


Or, override args with
Router::EXPAND_PARAMS
for this call:

\myapp\controllers\BlogController::archive('2011', 'sept', '10');



Variable restrictions
Sometimes it's useful for a rule to only match if a certain collected variable is restricted in some way. For example, consider the following rule:

$router->post('/:section(/:controller(/:function))', array(
    'controller' => 'home',
    'function' => 'index'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
));


If we only want :section to match either 'blog' or 'wiki', we can add an array that restricts :section to those items:

$router->post('/:section(/:controller(/:function))', array(
    'controller' => 'home',
    'function' => 'index'
), array(
    'controller' => '\myapp\controllers\%section\%{controller|capfirst}Controller',
), array(
    'section' => array('blog', 'wiki')
));


Now, if the first element of the path isn't 'blog' or 'wiki', the entire rule fails to match and Router moves on. Restrictions aren't limited to whole words, though. Advanced users familiar with regular expressions can, instead, pass a regular expression string:

$router->post('/:section(/:controller(/:function))', array(
    'controller' => 'home',
    'function' => 'index'
), array(
    'controller' => '\myapp\controllers\%section\%{controller|capfirst}Controller',
), array(
    'section' => '[^0-9/]'
));


Now, :section cannot contain a digit or a slash. It's important to prevent your variables from matching slashes, because they might swallow later variables in your rule!


Controlling argument order
Router's purpose is to manipulate requests in a way that any Controller function can be called. You shouldn't have to change your controller functions to make them work with your router! So the next argument that each rule types allows for is an array that determines in which order arguments should be passed to functions. Consider this rule:

$router->get('/:controller/:function(/:forumID(/:topicID))', array(
    'forumID' => '1',
    'topicID' => '1'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
), array(
    'forumID' => '[0-9]+',
    'topicID' => '[0-9]+'
));


That will call the specified function with the arguments 'forumID' and 'topicID', whatever they happen to match in the URL. But what if the function requires the topic ID first, and then the forum's? The next array argument will let you specify the order:

$router->get('/:controller/:function(/:forumID(/:topicID))', array(
    'forumID' => '1',
    'topicID' => '1'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
), array(
    'forumID' => '[0-9]+',
    'topicID' => '[0-9]+'
), array('topicID', 'forumID'));


Now the topicID is first. Note that NO additional arguments will be sent to the function that aren't specified in that array. For example, only putting 'topicID' in that array without 'forumID' would cause the function to be called with only one argument. This can be useful if you capture any variables to be used in the Controller namespace, but don't want them passed on to the function.


Arguments: Served up in the format you want them. Fresh and Juicy.
Router supports sending each variable to the matched function as separate parameters (the default behavior), or as an associative array. Referring to the example above, if you'd prefer this type of call to be made:

\myapp\controllers\HomeController::index(array(
    'topicID' => 6,
    'forumID' => 2
));


Then simply pass in 'true' after the argument order array:

$router->get('/:controller/:function(/:forumID(/:topicID))', array(
    'forumID' => '1',
    'topicID' => '1'
), array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
), array(
    'forumID' => '[0-9]+',
    'topicID' => '[0-9]+'
), array('topicID', 'forumID'), true);


Done! Now your function will be called with an array as its only argument.


The CatchAll rule: because you gotta show something!
If none of the rules that you've set so far match the requested URL, you'll often want to show an error page, send the 404 header, or maybe even just redirect to your homepage. This is where the catchAll rules comes in:

$router->catchAll(array(
    'controller': 'error',
    'function': 'error404'
));


That's it! Now, if none of the previously-set rules match, the user will be directed to the error page. Note that
catchAll()
takes the exact same arguments as every other rule type, except for the URL. So if you'd like to specify some arguments to be passed along, change their order, send them as an associative array, or whatever you like, you can follow the same format as the other rules.


One more tiny feature: Global Overrides
Often, when setting up your rules, your Overrides array will contain a lot of the same things for each rule. So save yourself some time and set those overrides before you set any rules:

$router->setGlobalOverrides(array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
    'allArgs' => Router::EXPAND_PARAMS
));


Now, you can leave the overrides array out of your future rules, and these will still apply. If you do decide to add in per-rule overrides, they'll be merged with what you put here -- so you can always count on your 'controller' override being there, even if you don't redefine it.


A Final Example!
This is an example ruleset that could run a real application. If you picked up Hydrogen in its early stages, you might be familiar with the old Dispatcher class (which Router replaces), and complex rules like the HomeMatch rule and PathInfoAutoMap rule. Those looking to upgrade to the Router class will probably use a ruleset exactly like this one:

$router = new Router();
$router->setGlobalOverrides(array(
    'controller' => '\myapp\controllers\%{controller|capfirst}Controller',
    'args' => Router::EXPAND_PARAMS
));
$router->request('/(:controller(/:function(/:*args)))', array(
    'controller' => 'home',
    'function' => 'index'
));
$router->catchAll(array(
    'controller' => 'error',
    'function' => 'error404'
));
$router->start();


That's it! Beginning to end, that will route all requests to their appropriate destination.


One more thing!
Note that Router will expect ALL controllers to be autoloaded! The Router is not responsible for including your php files for you. Thankfully, Hydrogen's Autoloader class makes this very easy. Check out its documentation, or hang out for the upcoming Autoloader guide. Or, just look at the Hydrogen MVC Starter for a simple example.


Now you know everything!
Congratulations :)
5


User is online soulcyon 

  • 兄貴 シャンク
  • Group: Members
  • Posts: 1598
  • Joined: 14-April 10
  • LocationNew Brunswick, NJ
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,Graphics,MongoDB,CouchDB

Posted 30 October 2011 - 03:57 PM (#2)

just perfection.
Posted Image
0


User is offline AwesomezGuy 

  • Certified Asshole™
  • Group: Members
  • Posts: 1251
  • Joined: 08-March 10
  • LocationIreland
  • Expertise:HTML,CSS,PHP,Javascript,SQL

Posted 30 October 2011 - 04:10 PM (#3)

You should probably update Hydrogen Overview so people don't try to use the old Router.
0


User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 30 October 2011 - 05:46 PM (#4)

View PostAwesomezGuy, on 30 October 2011 - 04:10 PM, said:

You should probably update Hydrogen Overview so people don't try to use the old Router.

I'm actually planning on breaking the overview out into individual pieces (hence the entire forum for these) as I start to consider each piece officially complete for 1.0 release. The Overview will be updated before I release 0.4.0 :)

View Postsoulcyon, on 30 October 2011 - 03:57 PM, said:

just perfection.

Glad you like it!
0


User is offline gibbonweb 

  • 兄ヨハネス
  • Group: Members
  • Posts: 2063
  • Joined: 23-June 10
  • LocationMunich(DE)
  • Expertise:HTML,CSS,PHP,Javascript,Python,SQL,Graphics

Posted 30 October 2011 - 06:41 PM (#5)

You could just fork Doxygen to properly parse namespaces and generate ONE REAL MAGNIFICENT documentation? pleeeze?
0


User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 30 October 2011 - 07:01 PM (#6)

View Postgibbonweb, on 30 October 2011 - 06:41 PM, said:

You could just fork Doxygen to properly parse namespaces and generate ONE REAL MAGNIFICENT documentation? pleeeze?

This has been in the back of my head forever xD. Not even phpDocumentator handles this yet, and it's getting HUGELY frustrating. Granted, I would want tutorial-style docs in addition to class docs, but I still need a good freaking set of class docs.

As-is, almost all of this guide comes from the class documentation in Router.php.
0


User is offline gibbonweb 

  • 兄ヨハネス
  • Group: Members
  • Posts: 2063
  • Joined: 23-June 10
  • LocationMunich(DE)
  • Expertise:HTML,CSS,PHP,Javascript,Python,SQL,Graphics

Posted 30 October 2011 - 07:26 PM (#7)

View PostKyek, on 30 October 2011 - 07:01 PM, said:

This has been in the back of my head forever xD. Not even phpDocumentator handles this yet, and it's getting HUGELY frustrating. Granted, I would want tutorial-style docs in addition to class docs, but I still need a good freaking set of class docs.

As-is, almost all of this guide comes from the class documentation in Router.php.

I know, that's why it would be so convenient: your inline documentation is getting *really* good and complete.
0


User is offline NoizeMe 

  • Group: Members
  • Posts: 591
  • Joined: 06-May 10
  • LocationGermany
  • Expertise:HTML,CSS,PHP,Java,Javascript,Python,Node.js,SQL,MongoDB,CouchDB,Cassandra

Posted 30 October 2011 - 09:47 PM (#8)

Just a short question about the new router:
Is there a way for getting the name of the controller which was matched as string?
Posted Image
0


User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 31 October 2011 - 06:03 AM (#9)

View PostNoizeMe, on 30 October 2011 - 09:47 PM, said:

Just a short question about the new router:
Is there a way for getting the name of the controller which was matched as string?

Yep! The only difference between the :controller variable and any other variable in the URL is that :controller and :function are both omitted from the arguments list by default. If you supply an argument order array, you can put either "controller" or "function" into it to pass those to the function as well.
0


User is offline NoizeMe 

  • Group: Members
  • Posts: 591
  • Joined: 06-May 10
  • LocationGermany
  • Expertise:HTML,CSS,PHP,Java,Javascript,Python,Node.js,SQL,MongoDB,CouchDB,Cassandra

Posted 31 October 2011 - 06:57 AM (#10)

View PostKyek, on 31 October 2011 - 06:03 AM, said:

Yep! The only difference between the :controller variable and any other variable in the URL is that :controller and :function are both omitted from the arguments list by default. If you supply an argument order array, you can put either "controller" or "function" into it to pass those to the function as well.

I actually want to that before executing the router.
I've a view variable which matches up with the current controller name and I want to set it right before executing the router, so I don't have to set it in every controller manually.
Posted Image
0


User is offline gibbonweb 

  • 兄ヨハネス
  • Group: Members
  • Posts: 2063
  • Joined: 23-June 10
  • LocationMunich(DE)
  • Expertise:HTML,CSS,PHP,Javascript,Python,SQL,Graphics

Posted 31 October 2011 - 08:19 AM (#11)

View PostNoizeMe, on 31 October 2011 - 06:57 AM, said:

I actually want to that before executing the router.
I've a view variable which matches up with the current controller name and I want to set it right before executing the router, so I don't have to set it in every controller manually.

Isn't the controller supposed to decide which view it will use?
0


User is offline NoizeMe 

  • Group: Members
  • Posts: 591
  • Joined: 06-May 10
  • LocationGermany
  • Expertise:HTML,CSS,PHP,Java,Javascript,Python,Node.js,SQL,MongoDB,CouchDB,Cassandra

Posted 31 October 2011 - 08:39 AM (#12)

View Postgibbonweb, on 31 October 2011 - 08:19 AM, said:

Isn't the controller supposed to decide which view it will use?

The variable is used inside the base-view.
Posted Image
0


User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 31 October 2011 - 09:49 AM (#13)

That's impossible, I'm afraid. There are cases where the URL in the rule will match, the controller class will exist, and the function will exist, but a match still won't be made because the required number of parameters to call the controller's function aren't there. The only way to do that quickly (read: without reflection) is to just execute the function and intercept any errors. So the Router doesn't actually know if the rule is a match until after the controller has already been called. I'm afraid that value would have to be set in each controller.
0


User is offline Koen 

  • Leroy Jenkins
  • Group: Members
  • Posts: 2503
  • Joined: 10-March 10
  • Locationthe Netherlands
  • Expertise:HTML,CSS,Javascript,Graphics

Posted 31 October 2011 - 11:10 AM (#14)

Now I'm thinking about it, I honestly miss working with Hydrogen! I need to find a project to get working on it again :D
Please click the + if I helped you!
Twitter: @KoenKlaren

<callumacrae> YOU DID A ROMNEY
0


User is offline Cyril 

  • Group: Members
  • Posts: 2545
  • Joined: 03-August 10
  • Expertise:HTML,CSS,PHP,Javascript,Graphics

Posted 31 October 2011 - 11:23 AM (#15)

View PostKoen, on 31 October 2011 - 11:10 AM, said:

Now I'm thinking about it, I honestly miss working with Hydrogen! I need to find a project to get working on it again :D



True that. I'm going to base my whole server on Hydrogen. I'm going to make a "web module" function - basically I can just plug in a new website into the code, and have it running :)

website :: github :: twitter :: dribbble :: forrst
html, css, php, javascript, graphics
0


User is offline NoizeMe 

  • Group: Members
  • Posts: 591
  • Joined: 06-May 10
  • LocationGermany
  • Expertise:HTML,CSS,PHP,Java,Javascript,Python,Node.js,SQL,MongoDB,CouchDB,Cassandra

Posted 01 November 2011 - 09:26 AM (#16)

Another trivial question:
I've the normal default rules.
$router = new Router();
$router->setGlobalOverrides(array(
	'controller' => '\noizeme\controllers\%{controller|ucfirst}Controller',
	'args' => Router::EXPAND_PARAMS
));

$router->request('/(:controller(/:function(/:*args)))', array(
	'controller' => 'index',
	'function' => 'index'
));

$router->catchAll(array(
	'controller' => 'error',
	'function' => 'error404'
));


With a request to / everything works fine.
But with a request to /blog the router matches just the ErrorController.
Why is that?

Edit: Oops, for some strange reason $_SERVER['PATH_INFO'] is not set...
Edit2: So even when $_SERVER['PATH_INFO'] is set the problem remains...
Edit3: What the f...?
Now the PATH_INFO is set to /home/noize/Arbeitsfläche/noize.me/blog.
I'm using the following rewrite-rules:
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1

Posted Image
0


User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 01 November 2011 - 10:59 AM (#17)

Kill off that RewriteBase line, maybe? My Apache-fu is not strong D:
0


User is offline NoizeMe 

  • Group: Members
  • Posts: 591
  • Joined: 06-May 10
  • LocationGermany
  • Expertise:HTML,CSS,PHP,Java,Javascript,Python,Node.js,SQL,MongoDB,CouchDB,Cassandra

Posted 01 November 2011 - 11:23 AM (#18)

View PostKyek, on 01 November 2011 - 10:59 AM, said:

Kill off that RewriteBase line, maybe? My Apache-fu is not strong D:

Yeah, I fixed that issue, but I still he doesn't match /blog :(.
Posted Image
0


User is offline Kyek 

  • Founder of wdR
  • Group: Administrators
  • Posts: 5081
  • Joined: 20-February 10
  • LocationPhiladelphia, PA, USA
  • Expertise:HTML,CSS,PHP,Java,Javascript,Node.js,SQL

Posted 01 November 2011 - 11:30 AM (#19)

Innteresting. Your PATH_INFO is set to exactly /blog, and the rules above don't match?

Do you have an autoloader running? Try to run this manually without include()-ing anything:

$controller = \noizeme\controllers\BlogController::getInstance();
$controller->index();

0


User is offline NoizeMe 

  • Group: Members
  • Posts: 591
  • Joined: 06-May 10
  • LocationGermany
  • Expertise:HTML,CSS,PHP,Java,Javascript,Python,Node.js,SQL,MongoDB,CouchDB,Cassandra

Posted 01 November 2011 - 11:44 AM (#20)

It seems that an exception occurred inside BlogController->index().
I fixed the error and now everything works as exprected.

But to be honest I don't thinks it's good that hydrogen eats up that error message :(.
Posted Image
0


Share this topic:


  • 2 Pages +
  • 1
  • 2
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users


Enter your sign in name and password


Sign in options
  Or sign in with these services