(Request and routing)
request parameters and attributes, attribute namespaces, errors, cookies.
This chapter focuses on how to use user input in Agavi actions and views. We don't go into input validation as the validation in Agavi works in such a way that you can turn it on later without touching the code in actions or views. Just remember that in real life you should never use any un-validated user input.
All the input from the user like $_GET and
$_POST arrays in the web context is stored into
AgaviRequestDataholder (in the web context
AgaviWebRequestDataHolder but you don't have
to worry about the actual implementation) object that's given to the
action's execute method as an argument. Depending on your validation
settings this object has either all input or just the input that has
passed validation (see chapter Validation to learn more about
different validation set-ups). For added security PHP's superglobals
like $_GET and $_POST are emptied
entirely. This is to keep developers from accidentally using
insecure data. Agavi also removes so-called magic quotes from the
input should that feature be enabled in your PHP set-up.
class Products_EditAction extends WebShopBaseAction
{
// route pattern /product/(id:\d+)/edit
// eg. www.example.com/product/123/edit to edit prd no 123
public function executeWrite(AgaviRequestDataHolder $rd)
{
$prdId = $rd->getParameter('prdid');
$product = $this->getContext()->getModel('Product', 'Products', array('id'=>'$prdId));
$product->setName($rd->getParameter('prdname');
$product->save();
return 'Success';
}This simple example shows us two important type of input -
input coming directly from the user (submitted via an HTML form
perhaps) and implicit parameters from the route (URL). Both input
types are accessed with getParameter(name,
default) of the
AgaviRequestDataHolder.
In web context cookies, HTTP headers and uploaded files are
accessed just as easily with their respective access methods
getCookie,
getHeader and
getFile.
{Action::setAttribute, View::getAttribute, $template array / extracted vars}
To pass data from the action to the view or template, you
would use the action's setAttribute
-method, which takes two parameters: a name-parameter identifying
the data, and a mixed parameter of actual data. The data can be
anything you can use as a variable in PHP itself, for instance
strings, arrays or objects.
class Products_ViewAction extends WebShopBaseAction
{
// route pattern /prouct/(id:\d+)
// eg. www.example.com/product/123 to view the product with id 123
public function executeRead(AgaviRequestDataHolder $rd)
{
$prdId = $rd->getParameter('prdid');
$product = $this->getContext()->getModel('Product', 'Products', array('id' => $prdId));
$this->setAttribute('product', $product);
return 'Success';
}
}Now the attribute "product" is available in the view as an
attribute which you can access via the view's
getAttribute -method, and in the template
as a template variable. Attributes may also be set in separate
namespaces, for more information see @@attribute
namespaces@@.
All the request parameter that the action recieved are also
available via the AgaviRequestDataHolder -object in the view. This
is also true for the template, if the request data auto-assignment
is enabled in output_types.xml.
There are several variables automatically assigned in the
template if set up in output_types.xml. These
are the @@link:routing object@@, the request object (which is not
the same as the request data object), the
@@link:controller@@, the @@link:user@@ , the @@link:translation
manager@@ and the request data -object. The variable name of these
objects is naturally configurable to your own taste.
(routing functionality, getting info from the routing, generating urls)
Normally when you do HTTP GET requests, parameters are specified
using a format like index.php?param1=val1¶m2=val2. Routing can
help you get rid of this ugly format that search engines do not
particularly like and make the URI prettier for the users. Routing is
basically a method to map URIs to module/action -pairs and provide
information for them, such as the parameters. Think of it as replacing
mod_rewrite for Apache or ISAPI_Rewrite for Microsoft IIS but with a
lot more power and features. Routes are defined in the file
%core.app_dir%/config/routing.xml, and to enable
routing you must set the "use_routing" option to true in
settings.xml.
Let's look at an example route:
<route name="viewproduct" pattern="^/products/(id:\d+)$" module="Shop" action="ViewProduct" />
A request to the URL http://host.com/products/157
would run the "ViewProduct" action in module "Shop", with a request
parameter id set to "157".
Most of the "magic" is achieved using regular expressions (@linkto PCRE@). In the above example, the id parameter must consist of digits, otherwise, the pattern, and thus this whole route, would not match.
The name:expression syntax is really only a
convenient form for named subpatterns, which look something like
(?P<name>pattern)
The example pattern is anchored at the beginning and end. This is very important, because if the pattern above wasn't anchored at the end, a URL like /products/157/YOUR-SITE-SUCKS etc would match, too. So you always have to pay attention to anchor patterns properly. There are cases when you don't want that to happen,
Routes can run for either read- or write-requests. You tell a
route which requests to be applied to by using the method
-attribute. For read-methods, you would use
method="read", and for write-requests you would use
method="write". Routes are matched for both request
methods by default, and are always matched in the order they are
configured.
It is worth to note that if the route has the module and action attributes set, they will overwrite route parameters named "module" and "action". If the attributes aren't set, you can use module and action parameters, and they would do what you expect -- set the module and the action of the request.
If we take the previous Shop-example in section 6.2, we notice that we have to specify the id-parameter every time, otherwise the route will not match. So how do we add optional parts to the route?
Because the route pattern is a regular expression, this is done the same way optional parts are done in regular expressions: with the ?-quantifier.
<route name="viewproduct" pattern="^/products(/{id:\d+})?$" module="Shop" action="ViewProduct" />The
difference between this route and the previous one is that now the
slash and the id -parameters are optional. That is, the request
http://host.com/products would also run the
Shop.ViewProduct action, but the
id-parameter would be undefined. Your action can then check if the
id request parameter is null, and if so show a listing of, say, all
products.
The default-element specifies values that should be used if a parameter is not supplied. This also applies for generating the route even if the parameter is not optional. For instance, we have a route like this:
<route name="viewproduct" pattern="^/products(/{id:\d+})(/{lang:[a-z]{2})?$" module="Shop" action="ViewProduct">
<defaults>
<default for="lang">en</default>
</defaults>
</route>indicates that if we do not specify a value for the
lang-parameter (specified with the for="lang"
-attribute), the default will be set to "en". Likewise, if we would
remove the ?-quantifier from the end of the pattern and generate a
route to viewproduct but do not specify the lang-parameter, it will
use the default "en".
When using @@optional parts@@, you almost always need additional characters before and after the actual parameters. For that, you can use pre- and postfixes.
To use them, you enclose the name:expression part in curly braces, and outside of these, you define the pre- and postfixes that should not be included in a matched value:
^/uri(/pre-part-{varname:regexp}-post-part)$Let's look at a more complicated example from the Sample Application:
<route name="search_engine_spam" pattern="^/products(/buy-cheap-{name:[\S\s]+}-at-agavi-dot-org)?/(id:\d+)" module="Default" action="SearchEngineSpam">
<defaults>
<default for="name">/buy-cheap-{chainsaws}-at-agavi-dot-org</default>
</defaults>
</route>This would match the request
http://host.com/products/37 and
http://host.com/products/buy-cheap-whatever-at-agavi-dot-org/37.
The inner part is optional, and for Search Engine Optimization (or,
in this case, stupid spamming) only.
The <default> here indicates that if we
do not supply the "/buy-cheap-whatever-at-agavi-dot-org",
it will set it to
"/buy-cheap-chainsaws-at-agavi-dot-org", thus setting the
name-parameter to "chainsaws", and id to null. (see @@previous
section@@)
Pre- and Postfixes may also be regular expressions themselves. You must, however, define static @@default values@@ so these routes can be @@generated@@
The ignores-section specifies parameters that affect the routing or output types, but are not set as request parameters. From the sample application:
<route pattern="^/({locale:[a-z]{2}(_[A-Z]{2})?})" stop="false" imply="true" cut="true" locale="${locale}@currency=GBP" callback="AgaviSampleAppLanguageRoutingCallback">
<ignores>
<ignore>locale</ignore>
</ignores>
</route>If the locale-parameter is set here, we use the route to set the application locale to the value specified. The ignore-section means that the parameter specified will not be available as a request parameter later on.
Routing callbacks are user-defined classes that run when Agavi tests the route for matches. The class has to implement methods that Agavi call, and these methods would return true or false depending on if the route matching is to succeed or fail.
onMatched is called when the route matches. This
method can then return either true to indicate that it did indeed
match, or false to indicate that even though the pattern itself
matches, the route should fail.
onNotMatched is similarly called for routes that
do not match the pattern, and likewise can return true to indicate
that the pattern should match nevertheless, or false to indicate
that ithe route should fail.
onGenerate is called when the routing engine
generates an URI for the route, and can modify parameters and return
true or false indicating wether this fragment should be in the route
at all.
For in-depth information on how to write your custom routing callbacks, please see the @@cookbook@@
Imagine you have a big application with tens or hundreds of
actions. Imagine adding all those into
routing.xml one by one in the
<routes> -section. It would be very ineffective
to parse through all those routes in order to find the correct
route. Introducing... subroutes!
Subroutes are routes that are defined under other routes. Let us explain through an example:
<route name="blog" pattern="^/blog" module="Default" action="Blog">
<route name=".index" pattern="^/$" action=".Index" />
<route name=".article" pattern="^/(id:\d+)(-{title:\S+})?$" action=".ShowArticle" />
</route> This would not run the action
Blog in the module Default for the request
http://host.com/blog. Why not? Because the request cannot
be anchored at the end of the pattern, as the pattern has children.
That is, to run the Blog.Index -action, you would have to use the
request http://host.com/blog/ (note the slash), and to
run the Blog.ShowArticle with the id and title -parameters set, you
would have to use the request
http://host.com/blog/151-Agavi_rocks. This way you only
have to define "top level" routes for your main actions, and you can
use subroutes to define routes to actions that are conceptually
below the "top level" actions, making parsing of the route much
faster.
Routing can do more -- like match against other sources, or do things other than setting module and action. An example:
Because this is a MVC-framework, the data
produced in the model should be view-independent. That is, the same
data could be rendered as XHTML or a PDF or whatever you can
imagine. You can define the output type by setting the
output_type -attribute for the route:
<route name="rss" pattern="/rss$" output_type="rss" stop="false" cut="true" />
As you can see, the pattern is only anchored at the end. On a match, routing execution will not stop, and the matched portion ("/rss") will be cut from the route for the following tests against the remaining routes.
Now let's say your application also has fancy AJAX features, and you return the data in JSON format. You can use the exact same URLs for "normal" and AJAX requests, since there is a difference: most AJAX frameworks send special headers along with the request. We can use that to automatically set the output type for these to JSON:
<route pattern="text/javascript" source="_SERVER[HTTP_ACCEPT]" output_type="json" stop="false" />
Here, if the HTTP Accept header contains text/javascript
(which it does if you make an XMLHTTPRequest using Prototype or
Mootools or Dojo), then the output type will be set to "json". We
are operating on a different source, $_SERVER,
that allows to access it's members directly.
There are other match sources available as well. Please refer to the cookbook for more information on matching against other sources and information on how to create your own sources.
To generate routes in templates or actions, you use the
AgaviRouting-class which returns the URI or
route as a string. You must use the name -attribute for
the routes in order for the
AgaviRouting-class to know which route to
generate, unless you are generating a route to the same route the
user currently is at. The way to generate a route is
$ro->gen(routename[, array parameters[, array options]])
So if we take our blog-route from section 6.2.6 and want to generate a route to /blog/151/Agavi_rocks, we would use
$ro->gen("blog.article", array('id' => 151, 'title' => 'Agavi_rocks'));This would generate a relative URI, thus won't include the protocol (http/https/whatever) nor the host name. To generate a complete URI to the document with the same protocol that the user used, you set the "relative" option to false:
$ro->gen("blog.article", array('id' => 151, 'title' => 'Agavi_rocks'), array('relative' => false));To generate a route to the same page the user is currently at, use null as the route name.
If the user has accessed the page with the HTTP-protocol and you need to generate a route to a super-secure location using HTTPS, you set the "protocol" option to "HTTPS". To generate a URI to the same route that the user is on but add an anchor (#foobar), use
$ro->gen(null, array(), array('fragment' => 'foobar'));