URL Processing
When Apache receives an HTTP request addressed to a Trax
application,
Apache mod_rewrite
is invoked and rewrites the request to invoke Trax file
dispatch.php
. At this time the URL which was
input to the rewrite rules is in
$_SERVER['REDIRECT_URL'].
dispatch.php
creates a new Dispatcher
object and calls its
dispatch() method. dispatch()
restores the state of the session identified by a cookie in the
request, or creates a session if none exists. Then it creates a
new ActionController object and calls its
process_route()
method.
The word "route" is used in Trax to describe a rule which
translates some URL into a particular controller object and method.
When process_route()
receives control, it calls
recognize_route() to
parse the URL into controller, action and id
components. recognize_route() calls
load_router() to load the
"routing table", which is a list of one or more
$router->connect()
calls, from
config/routes.php.
This list of calls define the rules for translating a URL into a
controller and action.
The translation rules work as follows: Starting with the first
rule in routes.php
, each rule is tested against
the URL to see
whether the rule matches. If a rule does not match, then the next
rule in the table is tested in turn, until a rule matches or the
table is exhausted. If no matching rule is found,
recognize_route() tests the last route in the table to see whether
it is the default route :controller/:action/:id
. If the last route is the default route, then
recognize_route()
returns it as a match, even if it does not in fact match. But if
there is no matching route and the last route in the table is not
the default route, then recognize_route() returns 'failure' which is
equivalent to HTTP code '404 Not found'.
Each entry in the route table contains two parts:
- A path, which is a character string to
test against the URL.
- Parameters, which are not tested against
the URL and aren't involved unless the
path part of the entry matches the URL.
Parameters are optional (and frequently
omitted).
A path is a series of substrings separated
by '/' (forward slash) characters. Each of these substrings can
contain any character except '/'. The path
does not begin or end with '/'. A substring may not be the null
(no characters) string, but it is legal for the entire
path to be the null string. Each substring is one of the following:
The following are legal path values:
:controller/:action/:id
This is the default path. It matches URLs
like word1/word2/word3
catalog/product/:action/:id
Remember that catalog
is a Perl regular
expression that matches catalog
, and
product
is a Perl regular expression that
matches product
, so this
path matches URLs like
catalog/product/word1/word2
''
matches '' (the empty string as a
path value matches the empty string as a
URL).
member/name=.*
matches URLs like
member/name=
or
member/name=Tom.Jones
or
member/name=Smith,J/since=1987/type=full
etc.
:controller
, :action
and
:id
may each appear at most once in a
path.
After the URL has been matched to a path,
the next step is to extract the name of the controller and action
to be invoked on this URL. These must be valid names in the PHP
language consisting only of lower-case alphameric characters and
'_' (underscore), because the controller name will translate
directly into a file name and a class name, and the action name
will be used as the name of a method in that class. The controller
and action names come from the route that matches the URL.
There are two places that a route can specify a controller or
action name: as part of the path, or in the
parameters. The
parameters are the optional second part of a
route. The value of parameters is an array
with key values that may be :controller
or
:action
. The following are legal
parameters values:
array(':controller' =>
'new_product')
array(':action' => 'enter')
array(':controller' => 'membership', ':action
=> 'new')
When a URL matches a route, the controller name is extracted as
follows: First, if the parameters array
exists and has an element whose key is
:controller
, then the value of that element is
used as the controller name. If no :controller
is specified by the parameters, then the
path is tested for a substring whose value
is :controller
. If found, then the part of the
URL which matched that substring is used as the controller value.
A controller value must be specified by either the
parameters or the
path. The action name is extracted by the
same process, substituting :action
for
:controller
. If the
path has a substring :id
,
then the part of the URL which matched that substring is forced to
lower case and the result assigned to
$_REQUEST['id']
.
If routes.php
contains the following:
router->connect('',array(':controller' => 'home'));
router->connect('product\?.*',
array(':controller' => 'catalog', ':action' => 'find'));
router->connect(':controller/:action/:id');
Then URLs will match routes as follows:
- URL
''
(no characters) will select
controller home
, action not specified.
- URL
product?item=4317
will select
controller catalog
, action
find
- URL
cart/add/4317
will select
controller cart
, action add
Action Call
When the names of the controller and action have been
successfully determined from the URL, the associated filesystem
paths are constructed and relevant files are loaded, and any
parameters and their values are stored in
ActionController::action_params.
First file app/controllers/application.php
is
loaded if it exists. This file contains the definition of the
ApplicationController class, which extends
ActionController
.
ApplicationController
contains properties and
methods used by all the controller classes, which should extend
ApplicationController
.
Then the controller name is used to find the file and class
containing the selected controller. By Trax naming conventions,
if the controller name is
then the controller file name is
_controller.php
and the controller class name is
. So for a
"catalog item" controller, the controller file name is
catalog_item_controller
and the controller class
name is CatalogItem
.
The controller file is loaded and a new object of the controller
class is created.
Next any needed helper files are loaded. Helper files contain PHP
code which helps prepare the output of an action method for
viewing. If file
application_helper.php
exists, it is loaded.
application_helper.php
contains
helpers that apply to every controller in the application.
Then the controller-specific helper file
_helper.php
is loaded if it exists. Finally any extra helper files, as
specified by calls to ActionController::add_helper(), are
loaded.
When controller and helper files have been loaded, the before
filters are executed (FIXME: We should check
return but don't). Next the controller object is tested for the
presence of a method with the name of the action as determined from
the URL. If such a method exists, it is called; if
no such method exists, then the controller object is tested
for the presence of a method named index()
.
If such a method exists it is called, otherwise the request fails
with 404 Unknown action. If an action method was found
and called, the after filters are executed.
Helper Loading
Helpers are classes that provide view logic. They exist to
hold view logic that would otherwise need to be added to a
template or controller. Helper services that are applicable to
the entire application go into
application_helper.php
, while
controller-specific helper functions go into a helper file named
after the controller, as
_helper.php
. Helper classes are written as subclasses of class Helpers,
which has a number of methods widely used by helper
subclasses. You can add a helper to an
ActionController
object by calling its
add_helper()
method, passing the name of the helper as an argument.
A number of predefined helper classes are distributed with Trax:
These classes are not automatically loaded,
you have to load them explicitly.
Filters
Filters enable controllers to run shared pre and post
processing code for its actions. These filters can be used to do
authentication, caching, or auditing before the intended action is
performed. Or to do localization or output compression after the
action has been performed.
Filters have access to the request, response, and all the
instance variables set by other filters in the chain or by the
action (in the case of after filters). Additionally, it's possible
for a pre-processing before_filter to halt the processing
before the intended action is processed by returning false or
performing a redirect or render. (FIXME: we don't implement this)
This is especially useful for
filters like authentication where you're not interested in
allowing the action to be performed if the proper credentials are
not in order.
Filter inheritance
Controller inheritance hierarchies share filters downwards, but
subclasses can also add new filters without affecting the
superclass. For example:
class BankController extends ActionController
{
$this->before_filter = audit();
private function audit() {
// record the action and parameters in an audit log
}
}
class VaultController extends BankController
{
$this->before_filter = verify_credentials();
private function verify_credentials() {
// make sure the user is allowed into the vault
}
}
Now any actions performed on the BankController will have the
audit method called before. On the VaultController, first the
audit method is called, then the verify_credentials method. If the
audit method returns false, then verify_credentials and the
intended action are never called. FIXME:
This is currently broken.
Filter types
A filter can take one of three forms: method reference
(symbol), external class, or inline method (proc). The first is the
most common and works by referencing a protected or private method
somewhere in the inheritance hierarchy of the controller by use of
a symbol. In the bank example above, both BankController and
VaultController use this form.
Using an external class makes for more easily reused generic
filters, such as output compression. External filter classes are
implemented by having a static +filter+ method on any class and
then passing this class to the filter method. Example:
class OutputCompressionFilter
{
static functionfilter(controller) {
controller.response.body = compress(controller.response.body)
}
}
class NewspaperController extends ActionController
{
$this->after_filter = OutputCompressionFilter;
}
The filter method is passed the controller instance and is
hence granted access to all aspects of the controller and can
manipulate them as it sees fit.
The inline method (using a proc) can be used to quickly do
something small that doesn't require a lot of explanation. Or
just as a quick test. It works like this:
class WeblogController extends ActionController
{
before_filter { |controller| false if controller.params["stop_action"] }
}
As you can see, the block expects to be passed the controller
after it has assigned the request to the internal variables. This
means that the block has access to both the request and response
objects complete with convenience methods for params, session,
template, and assigns. Note: The inline method doesn't strictly
have to be a block; any object that responds to call and returns 1
or -1 on arity will do (such as a Proc or an Method object).
Filter chain skipping
Some times its convenient to specify a filter chain in a
superclass that'll hold true for the majority of the subclasses,
but not necessarily all of them. The subclasses that behave in
exception can then specify which filters they would like to be
relieved of. Examples
class ApplicationController extends ActionController
{
$this->before_filter = authenticate();
}
class WeblogController extends ApplicationController
{
// will run the authenticate() filter
}
Filter conditions
Filters can be limited to run for only specific
actions. This can be expressed either by listing the actions to
exclude or the actions to include when executing the
filter. Available conditions are +:only+ or +:except+, both of
which accept an arbitrary number of method references. For
example:
class Journal extends ActionController
{
// only require authentication if the current action is edit or delete
before_filter :authorize, :only => [ :edit, :delete ]
private function authorize() {
// redirect to login unless authenticated
}
}
When setting conditions on inline method (proc) filters the
condition must come first and be placed in parentheses.
class UserPreferences extends ActionController
{
before_filter(:except => :new) { ? some proc ... }
...
}
Redirect Browser or Render Output
After the controller object's action method has returned to
ActionController::process_route()
and the after
filters have been executed, the controller object is examined for
a property named redirect_to
. If this
property exists and has a value, it means that the action method
has decided to redirect the user's browser to a different URL. The
value of the redirect_to
property is passed to
redirect_to()
which outputs a header redirecting the browser, then calls
exit .
If the action didn't redirect the browser, it should have provided
output to send to the browser. This is in the form of explicit
output produced by calls to
echo,
print or
printf ,
plus any properties of the controller object that are referenced in
the layout. ActionController::process_route()
collects all output produced by the controller's action method
in the output buffer, for presentation within a layout.
If the controller object has a property
render_text
which contains a string, then this
string is sent directly to the browser and all output and view
files are ignored.
If render_text
is undefined or empty, then the
saved output of the controller's action method is to be rendered.
A view file determined by the action is
found and included. The view file for an action is
app/views/
.phtml
.
This file contains HTML, which goes to the output buffer after the
action method's output. The output buffer is now assigned to
$content_for_layout. Finally the layout file is loaded. The
view file and layout file both contain HTML with
embedded PHP
expressions to present action method output to the user.