wiki | forum | trac | otrs | joomla | tech blog | mailman | bewelcome Branches: test.bw | alpha.bw | www.bw Participate: download | get involved

Request Routing in BW-Rox / Platform PT

See also

What happens when people request a page in bw-rox?

(A short summary - to be checked for wrong or out-of-date information)

Let's say the user types "https://www.bewelcome.org/searchmembers". What does our software do with that?

the apache webserver

  • routes every request to htdocs/index.php, which is the unique entry point for our framework.

the framework (RoxLauncher in /roxlauncher)

  • loads some basic settings (rox_default.ini, rox_local.ini, <servername>.ini)
  • scans the build folders (and some other folders) for autoload.ini and alias.ini, which are needed later to find out where specific classes are defined, and which controller to pick for a given request.
  • creates a RoxFrontRouter object, and calls its route() method.

the framework (RoxFrontRouter in /tools/routing)

  • has a look at the request (which can be "verifymembers" or "about/bod" etc)
  • chooses a controller classname. (see below)
  • creates a controller
  • runs the controller index() method, or sometimes other methods

the $controller object

  • gets the arguments from the request,
  • does some things with the model (such as finding a member in the database)
  • chooses a Page class (such as "PublicStartpage?")
  • creates a $page object
  • injects some info into the $page object (for instance, the ProfilePage gets a $member object)
  • returns the $page object to the framework.

the framework

  • injects some more information into the $page object
  • calls the $page->render() method.

the $page object

  • calls all the output methods. Beginning from $this->render(), it will call $this->head(), $this->body(), $this->leftSidebar(), etc.

How does the framework choose the controller classname?

  • first, it looks at the first part of the request. In your case, it is "verifymembers".
  • this string is turned all lowercase (strtolower), so "VeriFyMEMbers" would be turned into "verifymembers"
  • now it looks for the alias.ini definitions. Depending what it finds there, it might turn the keyword into something else. For instance, "people" will become "members", or "findmembers" will become "searchmembers". (see below for more about the alias.ini)
  • now it creates a classname based on the string, using ucfirst() and appending 'Controller' as a suffix. For instance, "verifymembers" becomes "VerifymembersController?".
  • now it checks if a class with that name exists, and if it has "!PAppController" as a direct or indirect parent class ("RoxControllerBase" is fine as a base class, because it inherits from "!PAppController").
  • if the class does not exist or does not have "!PAppController" as a parent, the framework will choose "RoxController" instead. For instance, a request to "onlinebanking" would go to "RoxController", because an "OnlinebankingController" does not exist.

About the alias.ini

The tricky thing about the alias.ini is that it works backwards, from right to left. For instance, if you have a definition in your alias.ini saying

; alias for verifymembers (goes to !VerifymembersController)
verifymembers = verify verifymember verification 

then any of the keywords on the right side will be transformed into "verifymembers". For instance, if the request is "verify/my/cat", then the keyword "verify" will be replaced by "verifymembers" and go to "VerifymembersController?".

How does a typical index() method look like?

...

Arguments for the controller's index() method

All the arguments are attributes of $args

  • $args->request has the pieces of the request. For instance, for "bw.org/help/faq", the request would be array('help', 'faq').
  • $args->get is the same as $_GET.
  • $args->post is the same as $_POST. (the idea is to get rid of global variables)

Why that many separate steps?

The routing happens in several stages (launcher, frontrouter, controller, page, and some more in between) for the following reasons.

  • classes become smaller, and thus easier to maintain.
  • scope of object attributes and object methods becomes smaller, so they are more local.
  • The process becomes more flexible: We can exchange one class with another.
  • Only the first stage (launcher) has to deal with filesystem, global variables etc. The other classes (frontrouter, controller, page) use local variables passed around as method arguments, or injected or stored as object attributes

Why don't we choose the controller method name by looking at the second part of the request?

In other frameworks (for instance, !CakePHP) the controller can have several methods, that will be called instead of the index() method, if the methodname is the same as the second part of the request ($args->request[1]). There are several reasons why we don't do this.

  1. It did not happen in the original PT / !MyTB applications. They all have only one index() method.
  2. It would spoil the namespace for controller methods. For instance, a request to "bw.org/messages/redirect" would call the RoxControllerBase::redirect(..) method, which is not meant to be called from outside.
  3. Our freestyle nested switch statements are not only completely sufficient, they are better than additional methods:
    • they are shorter to write.
    • they allow to easily specify alias names.
    • they allow to check for other segments of the request, not only the second one.
    • they allow to check for dynamic elements in $request[1], such as usernames or forum topic ids, and then use $request[2] for another switch statement. (for instance, "bw.org/members/henri/comments" or "bw.org/members/555/comments")
    • they allow to do the same check on different parts of the request. For instance, in "bw.org/about/faq", the switch statement will look at $request[1], while in "bw.org/faq", the switch statement will look at $request[0]. In both cases, we see the same page.
  4. Typical Zend or !CakePHP projects don't have polymorphic layout classes, like our pages and widgets. They only have controller + template (+model). We have a lot of application and display logic expressed in our page classes, which allows for much shorter controllers.

Special Routing for requests with $_POST (posthandler)

The application programmer can choose to use a "posthandler" for processing a form with method="post". The posthandler will run a callback method, and then redirect to a new url. This way, we avoid resending a form, so the same message is not sent twice.

The posthandler has some other niceties, which are:

  • It provides an easy way to remember things when doing a redirect
  • It counts how many times the form is sent again, and can remember things between resend events (using $_SESSION + serialize/unserialize).
  • It buffers the echo output when running the callback, and gives this text to the page that is rendered after the redirect. This way, we avoid errors like "headers already sent", and we can display the generate output on the redirected page (which can be nice for debugging).

... (to be continued)

How does a posthandler callback method look like?

A simple example is the calculator callback in the HellouniverseController?.

    public function calculatorCallback($args, $action, $mem_redirect, $mem_resend)
    {
        $post_args = $args->post;
        
        // give some information to the page that will show up after the redirect
        $mem_redirect->x = $x = $post_args['x'];
        $mem_redirect->y = $y = $post_args['y'];
        $mem_redirect->z = $x + $y;
    }

Special routing for .json format (XML HTTP Requests)

(to be checked for wrong information)

The framework has a built-in json support.

Some ajax applications want to communicate with the server in json format (javascript object notation). In these cases it is important that we don't break the json format of the served document, which can easily happen with echoes, error messages and the like. Moreover, we want to make it easier for application programmers to return json objects.

The framework has some mechanic to support json requests.

  • It will analyse the request string, and decide if it should serve json format.
  • In this case, it will pick a controller in the same way as above, but instead of calling the index() method, it will call the json() method of the controller.
  • The json() method gets a special argument, "$json_object", of type stdClass. The json() method can then stuff any attributes into this object. After running the method, the framework will print the $json_object in json notation.
  • All the echos and output during the json() method is buffered by the framework, and then goes into $json_object->text.
  • On the client side, the $json_object is turned into a javascript object.

How does a json request look like?

I am yet undecided how a json request should look like. Candidates are (assuming that ajaxchat is the application)

  • bw.org/json/ajaxchat/etc
  • bw.org/ajaxchat.json/etc
  • bw.org/ajaxchat/etc.json
  • bw.org/ajaxchat/json/etc
  • bw.org/.json/ajaxchat/etc

and so on.

How does a json() method look like?

A nice example is the ajaxchat.

    public function json($args, $json_object)
    {
        $model = new AjaxchatModel();
        $request = $args->request;
        
        if (!isset($_SESSION['IdMember'])) {
            echo 'not logged in!';
            $json_object->mustlogin = true;
        } else switch ($keyword = isset($request[1]) ? $request[1] : false) {
            case 'send':
                // TODO: implement
                $text = $args->post['chat_message_text'];
                $new_message = $model->createMessageInRoom(1, $_SESSION['IdMember'], $text);
                $json_object->messages = array($new_message);
                break;
            case 'update':
                $json_object->new_lookback_limit = $model->getNowTime('-0 0:0:1');
                $lookback_limit = isset($args->request[2]) ? $args->request[2].' .' : '000 .';
                $json_object->messages = $model->getMessagesInRoom(1, $lookback_limit);
                break;
            default:
                // ehm, not defined..
                // should not happen.
                echo __METHOD__;
        }
    }

Things to learn from this:

  • $json_object represents the output.
  • $args->request, $args->get, and $args->post have the typical routing information you need.

autoload.ini - where is my class defined?

In traditional PHP, in order to use a class (say, SearchmembersController?), we have to say require_once ROOT."build/searchmembers/searchmembers.ctrl.php"; To make this easier, modern PHP provides a global scope magic function called __autoload($classname) which is called each time a yet undefined class is requested (be it through a controller, a static method, an "extends" statement etc). By implementing this function, we can make sure that some files are only loaded when we need them.

The __autoload() function is implemented by PlatformPT, and can be configured using ini files called "autoload.ini". (previously it was "build.xml", but ini files are a lot easier to type). A typical ini file can look like this:

; comment
file1 = class1 class2 class3
file2 = class4
file3 = class5

It doesn't get much shorter..

Can we do that without any autoload.ini?

We could write an autoload mechanism which guesses the location of a class definition simply by translating the classname into a filename + path. This way, we would only write an autoload.ini if the classname does not follow the predefined naming scheme.

Unfortunately, the __autoload() mechanism is in the heart of Platform PT, and not easy to disable or customize.

Trac Customization: trac stylesheet
SourceForge.net Logo