The Agavi Cookbook
$Id: cookbook.xml 1823 2007-03-06 08:47:59Z david
$
Best Practices
Separating Business and Presentation Logic
Sharing and Passing Around Data Inside Your Application
Decorators and Slots
why decorate, setting up slots, passing around data between
slots
Popular Recipes
Removing the "index.php" Part From URLs
You need mod_rewrite for Apache or something similar for other web
servers, such as ISAPI_Rewrite for Microsoft IIS to get rid of the
"index.php" part in incoming and generated URLs.
Apache with mod_rewrite
Make sure mod_rewrite is enabled and the required AllowOverride
settings are active, and rename dist.htaccess to
.htaccess in your pub
directory which has the the following contents:
# rename this file to .htaccess to remove the necessity to have index.php in front of routes
# make sure mod_rewrite is on and AllowOverride settings are okay
# these two aren't really necessary
DirectoryIndex index.php
Options -MultiViews -Indexes
<IfDefine APACHE2>
AcceptPathInfo On
</IfDefine>
<IfModule mod_rewrite.c>
# enable rewrite engine
RewriteEngine Off
# ********** THIS IS THE ONLY SETTING YOU SHOULD HAVE TO CHANGE **********
RewriteBase /WEBSERVER/PATH/TO/pub/
# e.g. RewriteBase /~dzuelke/_projects/agavi/trunk/samples/pub/
# usually just "/" if your application's pub dir is the document root
# if requested url does not exist (i.e. it's likely an agavi route), pass it as path info to index.php
RewriteRule ^$ index.php?/ [QSA,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) index.php?/$1 [QSA,L]
</IfModule>
All you must do is enable rewriting:
RewriteEngine On
and change the rewrite base to point to your pub directory URL
path:
RewriteBase /~cbrown/projects/win-a-baseball-match/pub/
The important thing is that you use the
external path to the pub
directory as the RewriteBase, i.e. the path that you would have to
enter in your web browser location bar to reach the index page - in
our example, the project is located at
/~cbrown/projects/win-a-baseball-match/. If the
pub directory is the document root, then the
RewriteBase is just /.
The RewriteCond is a condition that executes the following
RewriteRule if the requested filename (that is the incoming URL
already mapped to a filesystem name by Apache) does
not exist. If the requested URL exists, for
instance, when an image on the webserver is requested, Agavi won't be
called. Otherwise, the framework is started and the given URL is
handed to Agavi as so-called Path Info which the WebRouting will use
to match a route.
Of course you can also use the rewrite rules in your virtual
host configuration. However, keep the following things in mind when
doing that:
You cannot use a RewriteBase, so you have to use the
full relative path in the rewrite rules and destinations,
including the leading slash.
The RewriteCond on %{REQUEST_FILENAME} only
work if the request filename has the full document root
prepended to it, i.e.
%{DOCUMENT_ROOT}%{REQUEST_FILENAME}
Assuming your application is in the webserver root,
i.e. called via "/" from the outside, that would mean:
RewriteRule ^/$ /index.php?/ [QSA,L]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
RewriteRule ^/(.*)$ /index.php?/$1 [QSA,L]
LightTPD
In your lighttpd.conf, add the following
lines (this example assumes that our project is in
/~cbrown/projects/win-a-baseball-match/):
url.rewrite-once = (
"^/~cbrown/projects/win-a-baseball-match/pub/([^?]*)(?:\?(.*))?$" => "/~cbrown/projects/win-a-baseball-match/pub/index.php?/$1&$2"
)
While the pattern looks a bit ugly-ish, it is strongly
recommended that you don't use your own in order for the routing to
work reliably. Don't worry, it's been engineered by a German, it won't
let you down.
Restart Lighty, and everything should work.
However, we're not there yet. The pattern works and does it's
job, but unfortunately, it's a bit overzealus and will rewrite any
URL. That's not exactly what we want, because our images, stylesheets
and other static files must remain accessible. So we have to add
another rewrite that catches URLs that shouldn't be rewritten, in our
case any that begins on "modpub", "img", "css" or "files":
url.rewrite-once = (
"^/~cbrown/projects/win-a-baseball-match/pub/(modpub|img|css|files).*" => "$0",
"^/~cbrown/projects/win-a-baseball-match/pub/([^?]*)(?:\?(.*))?$" => "/~cbrown/projects/win-a-baseball-match/pub/index.php?/$1&$2"
)
Don't forget to restart Lighty again for the changes to take
effect.
Microsoft IIS with ISAPI_Rewrite
This example assumes you're using the Lite version of
ISAPI_Rewrite, which does not allow per-directory
htaccess.ini files - all rewrite rules have to be
put into the httpd.ini in the directory where
ISAPI_Rewrite was installed. For now, we use these rules (assuming
your project is sitting in
/users/cbrown/projects/win-a-baseball-match/):
UriMatchPrefix /users/cbrown/projects/win-a-baseball-match/pub/
UriFormatPrefix /users/cbrown/projects/win-a-baseball-match/pub/
RewriteRule ([^?]*)(?:\?(.*))? index.php?/$1&$2 [L]
While the pattern looks a bit ugly-ish, it is strongly
recommended that you don't use your own in order for the routing to
work reliably. Don't worry, it's been engineered by a German, it won't
let you down.
Restart IIS, and everything should work.
However, we're not there yet. The pattern works and does it's
job, but unfortunately, it's a bit overzealus and will rewrite any
URL. That's not exactly what we want, because our images, stylesheets
and other static files must remain accessible. So we have to add a
rewrite condition that prevents rewriting for URLs that shouldn't be
rewritten, in our case any that begins on "modpub", "img", "css" or
"files":
UriMatchPrefix /users/cbrown/projects/win-a-baseball-match/pub/
UriFormatPrefix /users/cbrown/projects/win-a-baseball-match/pub/
RewriteCond URL (?!modpub|img|css|files).*
RewriteRule ([^?]*)(?:\?(.*))? index.php?/$1&$2 [L]
Don't forget to restart IIS again for the settings to take
effect (unless you'd like to wait until ISAPI_Rewrite figures out
something has changed, but that will take up to an hour).
Serving "application/xhtml+xml" to capable browsers
Serving XHTML as application/xhtml+xml might not be
a good idea under all circumstances. DOM changes the behavior
(innerHTML often does not work, and neither does
document.write; Element/Attribute names are not
normalized to upper-case; document.createElement does not
work, only createElementNS and so on), CSS is applied
slightly different, <script> and
<style> blocks must use CDATA sections
instead of HTML comments, Mozilla Gecko will not render documents
incrementally etc.
For more information on application/xhtml+xml vs
text/html, read the following documents:
http://www.mozilla.org/docs/web-developer/faq.html#xhtmldiff
http://webkit.org/blog/?p=68
http://www.hixie.ch/advocacy/xhtml
Propel Integration
Agavi was the first framework to feature autoloading support for
Propel. This does not only include your model classes, but also Propel
itself - Propel will be loaded and initialized on demand, and only if
you access your data model. This JIT loading mechanism guarantees
maximum performance because it eliminates any overhead. You can then
simply use any of your model classes in the code, without having to
require or init Propel first or anything.
This requires at least Propel 1.2.0!
To set up the advanced Propel support, you have to follow one
or two simple steps:
Register the Runtime Configuration File
Go to databases.xml and insert a new
database configuration for your Propel model:
<database name="propel" class="AgaviPropelDatabase">
<parameters>
<parameter name="config">%core.app_dir%/config/bookstore-conf.php</parameter>
</parameters>
</database>
bookstore-conf.php is the name of the
runtime configuration file Propel generated for you.
Add Your Object and Peer Classes to autoload.xml
This step is not necessary for Propel 1.3
For each Object and Peer, you now have to create an entry in
autoload.xml:
<autoload name="Book">bookstore/Book.php</autoload>
<autoload name="BookPeer">bookstore/BookPeer.php</autoload>You
do not have to add the om/*.php files or the
map/*.php files!
It is a good idea to add the Criteria class to the list of
autoloads, too:
<autoload name="Criteria">propel/util/Criteria.php</autoload>
Redirecting Back to the Originally Requested Page After
Login
If a unautheticated user tries to fire an action he is not allowed
to execute he is forwarded to the login action (configured in
settings.xml). To make a good user experience it's
nice to redirect the user back to the action he tried to execute after a
successful login. This is especially important if the user's session had
timed out and he was in the middle of something when he was logged
out.
Because Agavi forwards to the login action the URL is still the
one pointing to the original action. Agavi also stores the information
about the forward into the request object under
org.agavi.controller.forwards.login namespace. So if the
login action was actually triggered because of a denied access the first
thing we want to do is save the current URL for later use. The place to
do this is usually LoginInputView (remember, this
is all presentational application logic so the action itself shouldn't
do it).
if($this->getContext()->getRequest()->hasAttributeNamespace('org.agavi.controller.forwards.login')) {
// we were redirected to the login form by the controller because the requested action required security
// so store the input URL in the session for a redirect after login
$this->getContext()->getUser()->setAttribute('redirect', $this->getContext()->getRequest()->getUrl(), 'org.agavi.SampleApp.login');
}
else {
// clear the redirect URL just to be sure
$this->getContext()->getUser()->removeAttribute('redirect', 'org.agavi.SampleApp.login');
}
Now after a successful login we want to redirect the user back to
the action he requested. To do so we need this in the
LoginSuccessView:
if($usr->hasAttribute('redirect', 'org.agavi.SampleApp.login')) {
$this->getResponse()->setRedirect($usr->removeAttribute('redirect', 'org.agavi.SampleApp.login'));
return;
}
// else redirect to the welcome page or just proceed with the default behaviour of the view
And that's it. Enjoy the user experience!
Serving Output Variants of the Same Content
a quick example using a LatestProductsAction or whatever
Running Agavi Behind a Reverse Proxy
If you are developping an Agavi application that needs to run
behind a Reverse
Proxy , you need to be aware of a few things. The main issue is
that there is a difference between the public DNS for you applciation
and the internal, unregistered DNS. A reverse proxy intercepts all calls
for the public IP address and decides what to do with them. It will
decide what internal webserver to forward to (there can be several
webservers running the same application to help balance the load). As a
result your application receives a call from the Reverse Proxy and not
from the client.
This means that some of the $_SERVER[] variables just
contain the info for the reverse proxy and not for the client (e.g.
$_SERVER[REMOTE_ADDR] will be the IP address for the
reverse proxy). This is one of the reasons why it's a bad idea to rely
on the client's IP address for security. You'll also notice that
variables like $_SERVER['SERVER_NAME'] contain info about
the server within the network, but this address is unknown to the
outside world. E.g A client makes a request for
http://www.foo.com. A Reverse Proxy intercepts this and
forwards this request to an internal server
http://internal1.foo.com. In this case the
SERVER_NAME variable will be set to 'internal1.foo.com' and
not to 'www.foo.com'.
Now if you're using Agavi you should seldom have to deal with the
contents of $_SERVER[]. But sometimes you have to generate
an absolute url (e.g. as a link in a rss feed or a <base
href="..." /> tag in html). You can do this by calling the
gen() method on Agavi's Routing class with the
optional parameter 'relative' set to false
<link><?php echo $ro->gen( 'Newsitem' , array( 'id' => 5 ) , array ( 'relative' => false ) ); ?></link>
In a setup with a Reverse Proxy this would generate an url like
http://internal1.foo.com/news/1. This is not what we want.
Since internal1 is not known to a public dns server any user that
follows the link will receive a host not found error.
One way to deal with this is to let the Reverse Proxy rewrite all
html it sends back to the client. Information for this kind of setup can
be found in the manual for the Reverse Proxy. E.g. for Apache 2 see
http://www.apachetutor.org/admin/reverseproxies
. Another option is to use some of the alternate variables that are set
by Apache (I have no idea how other webservers handle this). You can
view these by looking at the output of the phpinfo()
function. With php 5.1.6 under Apache2 I have access to
HTTP_X_FORWARDED_FOR, HTTP_X_FORWARDED_HOST and
HTTP_X_FORWARDED_SERVER.
Armed with this information we can tell Agavi where to get the
name of the server. This information is needed by the
AgaviWebRequest object. To do this we need to
edit factories.xml.
<request class="AgaviWebRequest">
<parameters>
<parameter name="sources">
<parameter name="SERVER_NAME">HTTP_X_FORWARDED_SERVER</parameter>
</parameter>
</parameters>
</request>
This tells Agavi to use the value of HTTP_X_FORWARDED_SERVER
for SERVER_NAME, ensuring that absolute urls are
correct.
Role-Based Access Control and Rules for Unauthenticated
Users
Because of the way Agavi's SecurityFilter
works Agavi's Role-Based Access Control (RBAC) and the user credential
handling in general assume that the user has been authenticated. By
default if the user is not authenticated no credentials are checked.
This can become an issue if you want to build an application where the
same action can be public in one set-up but requires a certain
credential in another. For example a project management software's
calendar could be public in one firm but protected in another.
To enable role-based access control for unauthenticated users you
need to do two things - grant a role for unauthenticated users and put
some logic into your base action's isSecure()
method.
class MyProjectUser extends AgaviRbacSecurityUser
{
public function initialize(AgaviContext $context, array $parameters = array())
{
parent::initialize($context, $parameters);
if(!$this->authenticated) {
$this->grantRole('unauthenticated');
}
}
class MyProjectBaseAction extends AgaviAction
{
public function isSecure()
{
$cred = $this->getCredentials();
return $cred && !$this->getContext()->getUser()->hasCredentials($cred);
}
}
The first bit is pretty obvious.
AgaviRbacSecurityUser initializes unauthenticated
users to have no roles but we want to override this and grant them a
special role called unauthenticated. The
code for isSecure() then again might require
some reasoning. The action is marked as secure only if the user doesn't
have the required credential. What this means is that if the user is
authenticated but doesn't have the required credential
%actions.secure_action% is triggered. If the user is not
authenticated and unauthenticated role doesn't allow this
action %actions.login_action% is triggered.
In-Depth Tutorials
Populating and Validating Forms
explain the FormPopulationFilter
Caching
document the execution filter here, with examples, explanation of
how decorators are cached or not and slots can be included, variable
caching, groups, request methods, cache lifetime
Adding an XMLRPC or Other Interface to Your Web
Application
walk people through the basic steps/ideas