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>
Cookie-Based Auto-Login
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
Routing Examples
Adding an XMLRPC or Other Interface to Your Web Application walk people through the basic steps/ideas