If you're a serious PHP developer, you already know about (and have probably even used) PHP application development frameworks like Symfony, CakePHP, and the Zend Framework. These frameworks provide a well-rounded API that covers most application requirements and are a solid foundation for PHP application development. You can easily integrate them with third-party libraries and community-development components for additional functionality.
While the frameworks listed above are undoubtedly the most popular, they're not the only games in town; the number of available alternatives increases on a monthly basis. This series focuses on one such alternative: Agavi, a flexible and scalable framework that deserves serious consideration by any professional PHP developer.
In this article series, I'll guide you through the basics of MVC-based application development with Agavi, introduce you to basic framework concepts, and show you how to exploit Agavi's unique approach to quickly and efficiently build a full-fledged Web application from scratch. Through this process, you'll gain an appreciation for the subtleties of this framework, understand the design decisions that make it so secure and extensible, and gain a valuable addition to your PHP development toolkit. So let's get going!
I'll start with the answer to a very basic question: What is Agavi, and what makes it so special?
In the words of its official Web site, Agavi is "a powerful, scalable PHP5 application framework that follows the MVC paradigm." It provides a comprehensive toolkit to build and deploy PHP-based Web applications, and provides built-in support for security, data caching, internationalization, input validation, and database abstractions. Originally a fork of the Mojavi project, it is currently maintained by Bitextender GmbH, a software company based in Germany, and released to the community under the GNU Lesser General Public License 2.1.
Agavi is interesting for various reasons. Here are my top three:
- First, it focuses strongly on code reusability, allowing developers to easily create different interfaces to an application's functionality. This is particularly important in the context of Web applications, which often need to expose both HTML and SOAP interfaces to their internal workings. Suppose, for example, that you wish to build a SOAP interface to your application's existing search engine functionality. With Agavi, this is as simple as defining a new output type, and a renderer that delivers the output of the existing function in a new format. It's not necessary to tweak the existing functionality, and the entire process is transparent and easy to accomplish.
- Second, Agavi offers a sophisticated URL routing mechanism, which allows extensive configuration of how URL routes are mapped to application functions. This routing mechanism supports (among other things) optional and mandatory parameters, default values, nested routes, and callback functions. It's easily one of Agavi's most important features. The routing mechanism, like all other aspects of an Agavi application's configuration, is expressed entirely in XML, and Agavi's configuration subsystem allows run-time access to global application settings and variables.
- Third, Agavi provides extremely strict request filtering and input validation, out of the box. Filters can be used to pre- and post-process controller methods. Request parameters are validated on each request, and Agavi automatically strips out all unknown parameters, significantly reducing the risk of SQL injection and similar attacks. A number of built-in validators exist for common tasks, such as validation of strings, numbers, timestamps, e-mail addresses, and files. It's also possible to perform validation using regular expressions or define custom validators for situations where the built-in ones aren't sufficient. All these features conspire to make Agavi one of the most secure frameworks around for Web application development.
In addition to the above, Agavi also provides:
- A conditional caching engine.
- Support for most common database systems (including MySQL, PostgreSQL, and SQL Server) and ORMs (including Propel and Doctrine)
- A session management engine
- Customizable templating; and full compliance with OOP principles.
All in all, it's pretty cool...so dive right in and get started!
In this article series, I'll assume that you already have an Apache/PHP/MySQL development environment set up, that you have a working knowledge of PHP and XML, and that you are comfortable using PHP's simple and complex data types. You should also have a reasonable knowledge of general OOP and SOAP concepts, as well as some knowledge of XML tree generation with PHP's Document Object Model (DOM) extension. This series uses PHP V. 5.2.6 and Apache V. 2.2.11.
To begin, create the basic directory structure needed for an Agavi application, using the following steps:
Step 1: Create the application directory structure
Change to the Web server's document root directory (typically /usr/local/apache/htdocs on Linux® or C:\Program Files\Apache\htdocs on Windows®) and create a new subdirectory for the application. For reasons that I will shortly explain, name this directory wasp/.
shell> cd /usr/local/apache/htdocs shell> mkdir wasp |
You will reference the directory throughout this article as $WASP_ROOT.
Within this directory, create another sub-directory named lib/.
shell> cd wasp shell> mkdir lib |
Step 2: Define virtual host settings
For easier access to the application, define a new virtual host and set it to the application's Web root directory. While this step is optional, I recommend it especially when you work on a development machine which holds multiple in-progress applications, as it creates a closer replica of the target deployment environment.
To set up a named virtual host for the application, open the Apache configuration file (httpd.conf or httpd-vhosts.conf) and add the following lines to it:
NameVirtualHost 127.0.0.1
<VirtualHost 127.0.0.1>
DocumentRoot "/usr/local/apache/htdocs/wasp/pub"
ServerName wasp.localhost
</VirtualHost>
|
These lines define a new virtual host, http://wasp.localhost/, whose document root corresponds to the $WASP_ROOT/pub/ directory. Restart the Web server to activate these new settings. Note that you might need to update your network's local DNS server to let it know about the new host as well.
Step 3: Download and install Phing
Agavi makes use of the Phing build system to automatically generate code for actions, views, templates, and validators. Phing requires PHP 5.0 (or higher). The easiest way to install it is with the automated PEAR installer, which should be included by default with your PHP build.
To install Phing, simply issue the following commands at your shell prompt:
shell> pear channel-discover pear.phing.info shell> pear install phing/phing |
The PEAR installer will now connect to the new channel, download the package, and install it to the appropriate location on the system. This article uses Phing V. 2.3.3.
To install the package manually, visit its home page, download the source code archive, and manually uncompress the files to the desired location. For links to the package home page, see Resources in this article. Note that manual installation presupposes some knowledge of PEAR's package organization structure.
Step 4: Download and install Agavi
The next step is to download and add the Agavi framework to the application. At this point, it is also necessary to decide whether the Agavi libraries will be bundled with the application, or whether users will download and install it themselves. Each option has pros and cons:
- Requiring users to download the Agavi libraries themselves ensures that they always have access to the latest build (with the most recent bug fixes). However, the process can be intimidating for novice users, and if the newer libraries are not backward-compatible with the versions used when you develop the application, unusual and hard-to-track bugs might appear.
- Bundling the Agavi libraries with the application ensures that users can begin using the application out of the box, with no version incompatibilities. However, it also locks users in to a particular version of Agavi, possibly making it harder to upgrade to newer versions with additional features or necessary bug fixes.
For purposes of this article series, I'll assume that the Agavi libraries are bundled with the application. Therefore, download the latest version of the Agavi framework (this series uses Agavi V 1.0.1) from its official Web site and extract it to a temporary location. Then, copy the extracted src/ directory to $WASP_ROOT/libs/agavi:
shell> cp -R /tmp/1.0.1/src /usr/local/apache/htdocs/wasp/libs/agavi |
Also rename and copy the agavi-dist (Linux) or agavi.bat-dist (Windows) file from the extracted bin/ directory to $WASP_ROOT/agavi (Linux) or $WASP_ROOT/agavi.bat (Windows). This is the Agavi build script, which automates many common code creation tasks under the Agavi framework.
shell> cp /tmp/agavi-1.0.1/bin/agavi-dist /usr/local/apache/htdocs/wasp/agavi shell> chmod +x /usr/local/apache/htdocs/wasp/agavi |
Step 5: Configure and test the Agavi build script
The final step is to configure the Agavi build script and tell it the file locations for Agavi on the system. Open this script in a text editor, and update the $AGAVI_SOURCE_DIRECTORY variables with the path (absolute is preferred, but relative will also work) to $WASP_ROOT/libs/agavi. Here's an example of what this might look like on Linux:
SET AGAVI_SOURCE_DIRECTORY="./libs/agavi" |
On Windows, you should also set the PHP_EXECUTABLE variable, which defines the complete disk path to the PHP binary. Here's an example:
SET AGAVI_SOURCE_DIRECTORY="./libs/agavi" SET PHP_EXECUTABLE="C:\Program Files\PHP\php.exe" |
Save the file, and then test it by changing to $WASP_ROOT and running the following command:
shell> agavi status |
Figure 1. The output of the agavi status command
Figure 1 demonstrates an example of the output on Linux . The output lists versions and directory paths for PHP, Phing, Agavi, and existing projects. If you don't see similar output on your system, something went wrong and you'll need to troubleshoot the problem. The Resources in this article includes links to the appropriate sections of the Agavi manual to help you through this process. If, on the other hand, you do see similar output on your system, the foundations of your Agavi application are in place, and you're ready to start adding some code to it!
Before you begin, it's worthwhile to understand the example application you'll build. The application is the Web site of a fictional automobile dealership that specializes in the sale of used sports cars. The original plans for the dealership Web site was fairly tame: Some static pages with basic contact and service information, and an enquiry form for visitors to get in touch with sales executives directly. However, over a late lunch, the dealership team came up with an interesting twist: An online used-car classifieds system, which allows sellers to upload pictures and descriptions of their used Ferraris and Lamborghinis, and buyers to search these listings by budget, brand, and keyword. Site moderators have direct access to the uploaded listings, and manually approve suitable vehicles for display in search results. The database of car listings is also available through a SOAP interface, for easy integration into other applications.
They even thought of a cool name for it: The Web Automobiles Sales Platform, or WASP.
The WASP example application is conceived so it covers common requirements encountered in day-to-day application development: static pages, input forms, picture upload, login-protected administration panel, data paging and sorting, multiple output types, and keyword search. Implementing these features requires one to understand the finer details of form processing, input validation, session management, authentication and security, CRUD database operations, Web service APIs, and integration with third-party libraries. As such, it is a good starting point to understand application development with Agavi.
To begin building this application, initialize a new project with the Agavi build script. Change to $WASP_ROOT and run the following command:
shell> agavi project-wizard |
Set a name for the project:
Project name [New Agavi Project]: WASP Project prefix (used, for example, in the project base action) [Wasp]: WASP |
Accept default values for all the prompts that follow, except the one below:
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y |
The project wizard will now work automatically to create an empty application container and populate it with default settings. Figure 2 has a screen capture of it in action.
Figure 2. The Agavi project wizard in action
As part of the process, the project wizard will add some new directories to your application hierarchy:
$WASP_ROOT/app/ for application code; $WASP_ROOT/pub/ for Web-accessible content such as
images, stylesheets, and static HTML pages; and $WASP_ROOT/dev/ for development files.
Figure 3 indicates what the end result should look like:
Figure 3. The file layout for an Agavi application
Note that $WASP_ROOT/dev/ contains a copy of the Apache .htaccess file. It's intended specifically for use in projects using
a distributed version control system like SVN or CVS, as it allows you to maintain
your working copy without polluting the shared workspace. For your current purposes,
this directory can safely be deleted; see Resources for a link to the Agavi manual page explaining its utility in greater detail.
Once the project creation process is complete, you can open your Web browser and visit the virtual host you set up earlier. Enter the URL http://wasp.localhost/. You should see an Agavi welcome page, as in Figure 4.
Figure 4. The Welcome page of a new Agavi application
Congratulations! You now have a working (albeit extremely simple) Agavi application up and running. Next, take a closer look at its innards, and then begin modifying it to support the functions discussed earlier.
Application development in Agavi follows the traditional Model-View-Controller pattern: data models are separated from processing logic, which in turn is separated from the output seen by the user. Agavi implements this pattern through the use of Models, Actions, Views, and Routes.
- Models represent data and provide functions necessary to manage, manipulate, and perform calculations on this data, which is usually (though not always) stored in a database. Agavi includes adapters for most common databases, as well as drivers for both Propel and Doctrine ORMs.
- Views represent the output that the user sees. Views are closely coupled with page templates, which contain the layout code or markup necessary to correctly present the view to the user. Views can also assign values to template variables; these values are automatically interpolated into the page templates at run-time.
- Actions provide the link between Models and Views. They make changes to application data using Models, and then call Views to display the results to the user. An Action can have multiple Views, and can call a different view depending on the result that is to be shown at any given time.
- Routes provide the link between user requests and Actions. When a user makes a request for an application URL, the routing system intercepts that request and decides, based on the URL pattern, which Action should be invoked to fulfill the request. Routes make use of regular expressions for pattern matching, and are expressed using XML.
Still confused about how these four components fit together? The best explanation of the typical flow of a Web request through an Agavi application is probably given in the Agavi manual itself (see Resources for a link), which states: "When a Web request arrives, the routing mechanism selects the initial Action to be executed; the Action performs necessary changes in application state by calling the Models; it also selects one of its Views to be executed after it finishes. After that, the appointed View performs rendering of the application's output."
By default, all Actions live in the—what else?—Default module. However,
you might often wish to group Actions (and their related Models and Views) together
based on the different functional areas of your Web application. Modules provide a way
to accomplish this. For example, if your application includes subsystems for search,
user profile management, and news, you can create separate modules named Search, Profiles, and News, and place the corresponding Actions for each in these modules.
With all this background information at hand, go back to the code generated by the project wizard and take a closer look at how the default Agavi welcome page is generated. First, look inside the application's routing table, stored in $WASP_ROOT/app/config/routing.xml. In Listing 1,you'll find a route for the application index page:
Listing 1. The route for the application index page
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
<ae:configuration>
<routes>
<route pattern="" module="Welcome" action="Index" />
...
</routes>
</ae:configuration>
</ae:configurations>
|
A route entry typically contains pattern, module, and action attributes. The pattern attribute specifies the URL pattern that the route matches,
while the module and action
attributes indicate which module and Action to invoke to fulfill the request. By
default, Agavi will use the first match it finds; therefore, as a general rule,
arrange routes with the most specific ones first and the more general ones further down. To change this behaviour, add a stop=false attribute to the route; this tells Agavi to continue processing even when it finds a match.
The route in Listing 1 is an extremely general one. It ensures that for any URL request, Agavi will invoke the Index Action of the Welcome module. This Action is automatically created (as a PHP class) by the Agavi project wizard, and you'll find it at $WASP_ROOT/app/modules/Welcome/actions/IndexAction.class.php:
Listing 2. The Index Action
<?php
class Welcome_IndexAction extends WASPWelcomeBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
}
?>
|
Every Action class can have some standard methods, and Agavi will use these to
determine how to handle different types of requests. For example, an executeRead() method specifies what to do with GET requests, while an
executeWrite() method specifies how to handle POST
requests. In case neither of these methods are defined, every Action must have, at a
minimum, a getDefaultViewName() method, which specifies the
default View for the Action. From the code in Listing 2, it's clear that the default View for the IndexAction is named Success.
According to Agavi's file naming conventions, the Success View for an Index Action within the Welcome module should be stored at $WASP_ROOT/app/modules/Welcome/views/IndexSuccessView.class.php. Navigate to the file, and you'll see something like Listing 3:
Listing 3. The Success View
<?php
class Welcome_IndexSuccessView extends WASPWelcomeBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd, 'simple');
$this->setAttribute('agavi_release', AgaviConfig::get('agavi.release'));
}
}
?>
|
When rendering a View, Agavi automatically calls a method named executeXXX(), where XXX corresponds
to the output type required. To generate HTML output, the View must contain an executeHtml() method, which sets up necessary template variables and
prepares the template for rendering. As you'll see a little later in this article,
Views can also generate output in other formats—for example, to generate XML
output, define an executeXml() method, to generate YAML
output, define an executeYaml() method, and so on. You can
define template variables within the View through the setAttribute() method, and then access the variables within the template itself as keys of the $t array.
It's worth pointing out that the HTML markup to be generated by the View is not stored
in the View class file itself, but in a separate template file. As per Agavi
convention, this template file is named after the View; therefore, the template for an IndexSuccessView is located at $WASP_ROOT/app/modules/Welcome/templates/IndexSuccess.class.php. Open this file, and you'll see the HTML markup that generates the output shown in Figure 4.
Listing 4. The HTML markup
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<base href="<?php echo $ro->getBaseHref(); ?>" />
<title>Welcome to Agavi!</title>
</head>
<body>
<div>
<p>You are running <em><?php echo $t['agavi_release']; ?>
</em></p>
</div>
</body>
</html>
|
Notice from the code snippet in Listing 3 above how the template variable $t['agavi_release'] is defined within the View using the setAttribute() method, and then dynamically interpolated within the template file itself.
Finally, note that you should store any Web-accessible file assets used by the View,
such as image files, JavaScript libraries, or Flash animations, in the application's public area—in this case, $WASP_ROOT/pub/—and referenced appropriately from within the template.
So, here's a quick summary: An HTTP GET request for any application URL is automatically matched, through Agavi's routing subsystem, to the Welcome/IndexAction which, by virtue of the getDefaultViewName() method, knows to invoke the IndexSuccessView. The IndexSuccessView then calls its executeHtml() method to generate HTML output, interpolates a few template variables into the HTML markup, and sends the output back to the client for display.
In this explanation, a couple of things immediately pop out at you:
- First, so long as you name and place your Actions and Views correctly, there isn't really very much work for you to do. The framework will automatically locate and execute files for you, without requiring any manual intervention. And, if you find the file-naming convention a bit complex to understand, relax—as you'll see in Serving static content, a wizard takes care of all those details for you!
- Second, Routes play a critical role in matching URL requests and directing them to appropriate Actions. This also means that, unlike in some other frameworks, Actions can live anywhere in the hierarchy, and your application URLs need not directly reflect your internal classification of Actions and modules in different categories. Similarly, security and privileges are tied to the Action itself, and not to the module.
- Third, a single Action can have multiple Views, and each View can handle different output types. This magnifies the reusability of your code and makes it very easy to, say, add Views for XML or RSS output to the same Action at a later date, as the application evolves.
Setting the application index page and master template
Now that you understand a little more about how Agavi works, you can begin to build the application. While the default Agavi welcome page is very easy on the eyes, it's not necessarily the first thing you want your users to see. Therefore, as a first step, you'll create a new index page for the application.
First, change to $WASP_ROOT and remove the Welcome module that was auto-generated by the project wizard, as below:
shell> rm -rf app/modules/Welcome shell> rm -rf app/pub/welcome |
Also edit the routing table at $WASP_ROOT/app/config/routing.xml, and remove the various entries in it, so that you end up with the empty (for the moment) configuration in Listing 5:
Listing 5. The routing table
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
<ae:configuration>
<routes>
</routes>
</ae:configuration>
</ae:configurations>
|
The project wizard will already have created a Default module containing an IndexAction and an IndexSuccessView; this is a good choice for the application's index page. Open the file $WASP_ROOT/app/modules/Default/actions/IndexAction.class.php in your favourite text editor, and make sure that it contains the lines in Listing 6:
Listing 6. The Default/Index Action definition
<?php
class Default_IndexAction extends WASPDefaultBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
}
?>
|
Next, check that the View file $WASP_ROOT/app/modules/Default/views/IndexSuccessView.class.php exists, and contains the code in Listing 7:
Listing 7. The Default/IndexSuccess View definition
<?php
class Default_IndexSuccessView extends WASPDefaultBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
$this->setAttribute('_title', 'Index');
}
}
?>
|
Next, to create the template for the application index page, modify the template file $WASP_ROOT/app/modules/Default/templates/IndexSuccess.php to contain the code in Listing 8:
Listing 8. The Default/IndexSuccess template
Welcome to WASP, your one-stop shop for used sports cars online.
<p/>
We have a wide selection of used automobiles for your driving pleasure,
and all our models are backed with our unique 120-day money-back guarantee.
<p/>
Use the links above to navigate this web site.
|
Finally, add an entry to the routing table so that Agavi knows to direct all requests for the index page to Default/IndexAction (Listing 9):
Listing 9. The Default/Index route definition
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
<ae:configuration>
<routes>
<!-- default action for site root "/" -->
<route name="index" pattern="^/$" module="Default" action="Index" />
</routes>
</ae:configuration>
</ae:configurations>
|
Now, visit the application index page at http://wasp.localhost/ again in your Web browser, and you should see the revised index page in Figure 5.
Figure 5. The WASP index page
Yes, I know—it's not very appealing, is it? It definitely needs an image makeover!
When it comes to altering the overall layout of a page, you can go two ways. The first is, obviously, to add the layout markup to the selected page template. However, if you do this, you have to repeat it for each page template in your application, which is both time-consuming and not very update-friendly. A better solution is to change the application's master template, located at $WASP_ROOT/app/templates/Master.php, and have the new layout automatically reflected in all page templates.
Listing 10 shows the code for the revised master template:
Listing 10. The application master template
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<base href="<?php echo $ro->getBaseHref(); ?>" />
<link rel="stylesheet" type="text/css" href="/css/default.css" />
<title><?php if(isset($t['_title'])) echo htmlspecialchars($t['_title'])
. ' - '; echo AgaviConfig::get('core.app_name'); ?></title>
</head>
<body>
<!-- begin header -->
<div id="header">
<div id="logo">
<img src="/images/logo.jpg" />
</div>
<div id="menu">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">For Sale</a></li>
<li><a href="#">Other Services</a></li>
<li><a href="#">About Us</a></li>
<li><a href="#">Contact Us</a></li>
</ul>
</div>
</div>
<!-- end header -->
<!-- begin body -->
<div id="body">
<?php echo $inner; ?>
</div>
<!-- end body -->
<!-- begin footer -->
<div id="footer">
<p>Powered by <a href="http://www.agavi.org/">Agavi</a>.
Licensed under <a href="http://www.creativecommons.org/">Creative Commons</a>.</p>
</div>
<!-- end footer -->
</body>
</html>
|
You'll notice that the master template makes use of two additional assets—a CSS stylesheet and a logo image. Locate these files in the application's public area, so connecting clients can retrieve them over HTTP. Accordingly, create the css/ and images/ sub-directories within $WASP_ROOT/pub/ and copy the necessary assets to these locations (you'll find them in this article's download archive).
Listing 11 shows the stylesheet rules from $WASP_ROOT/pub/css/default.css, for illustrative purposes:
Listing 11. The application master stylesheet
body {
margin: 0;
padding: 0;
font-family: 'Verdana' sans-serif;
}
#header {
height: 80px;
background: #4062A8;
}
#logo {
float: left;
padding-left: 50px;
padding-top: 5px;
}
#menu {
float: right;
background: #4062A8;
margin-right: 20px;
}
#menu ul {
list-style: none;
}
#menu li {
margin-top: 35px;
float: left;
padding-right: 25px;
padding-left: 25px;
}
#menu a, #footer a {
color: white;
font-weight: bold;
}
#body {
padding-top: 20px;
padding-left: 50px;
min-height: 375px;
}
#footer {
font-size: x-small;
color: white;
float: right;
text-align: right;
background: #4062A8;
width: 400px;
clear: both;
margin-top: 20px;
}
|
Then, revisit the application's index page, and you should see something that looks like Figure 6:
Figure 6. The revised WASP index page
There you go! Isn't that much nicer to look at?
Thus far, you've only modified existing Actions within the application. However, the Agavi build script makes it very easy to add new Actions to your application. To illustrate, you'll now add a StaticPageAction and corresponding Views, that will be responsible for serving up static content such as the company's "About Us" and "Services" pages. The steps below reflect the standard process you follow to add any new functionality to an Agavi application.
Step 1: Create placeholder classes
Go back to your shell and invoke the Agavi build script as below:
shell> agavi action-wizard |
Set up a new Action named StaticContentAction and link it with two Views, StaticContentErrorView and StaticContentSuccessView, by supplying the following values when prompted:
Module name: Default Action name: StaticContent Space-separated list of views to create for StaticContent [Success]: Error Success |
Agavi will now go to work generating the necessary class files and placing them in the correct locations.
Once the process is complete, revisit the $WASP_ROOT/app/routing.xml file, and add a new base route for static content that references your newly-minted Action, as in Listing 12:
Listing 12. The Default/StaticContent route definition
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
<ae:configuration>
<routes>
...
<!-- action for static pages "/content/$page" -->
<route name="content" pattern="^/content/(page:[\w-]+)$" module="Default"
action="StaticContent" />
</routes>
</ae:configuration>
</ae:configurations>
|
This route is a little more complex than the ones you've seen earlier. First, it includes a "name" attribute; this provides a label that can be used when manually generating the route (you'll see this in action a little further along). Second, the regular expression will match URLs in any of the following formats:
/content/hello /content/HelloWorld /content/hello-world /content/Hello-World-123 |
It should be clear that the last segment in all of the above examples is a variable. It's necessary to tell Agavi's routing subsystem about this, by enclosing that segment of the route definition in parentheses. Using parentheses creates what the Agavi manual refers to as a capture group. Capture groups can be used to automatically convert certain segments of the URL into Agavi variables, which will eventually be validated, placed into an AgaviRequestDataHolder object, and accessed from within an Action or a View.
Step 3: Define validation rules
Defining a route with capture groups is only half the jigsaw, though. For the variables in the route to make it into an Action or View, they must pass Agavi's extremely stringent input validation filter. By default, this filter is set to the strictest level; this means that any GET or POST variables that it doesn't know about (or that fail the specific validation tests) will be automatically dropped and never make it into the application. This extremely strict input sanitization policy is part of what makes an Agavi application so secure.
Agavi comes with a wide range of built-in input validators, which can be configured
through a simple XML file. An input validator shares the name of the Action to which it belongs, and can be found in the module's validate/ directory. To see the StaticContentAction validator, edit the file at $WASP_ROOT/app/modules/Default/validate/StaticContent.xml and fill it with the XML in Listing 13:
Listing 13. The Default/StaticContent Action definition
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
parent="%core.module_dir%/Default/config/validators.xml"
>
<ae:configuration>
<validators method="read">
<validator class="regex">
<arguments>
<argument>page</argument>
</arguments>
<ae:parameters>
<ae:parameter name="pattern">#^[\w-]+$#</ae:parameter>
<ae:parameter name="match">true</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
|
Very simply, this XML block sets up an AgaviRegexValidator for the page variable, and checks that it matches the regular expression specified in validator definition.
As a result of steps 2 and 3, when passed the
URL /content/hello, Agavi's routing and validation subsystems automatically check the
URL, initialize a AgaviRequestDataHolder variable named page, and assign it the value hello.
Why do you want to capture this particular segment of the URL anyway? That's a good question, and the reason will become clear shortly. Until then, though, move forward and set up the StaticContentAction, which you should find at $WASP_ROOT/app/modules/Default/actions/StaticContentAction.class.php. Edit this file so it contains the code in Listing 14:
Listing 14. The Default/StaticContent View definition
<?php
class Default_StaticContentAction extends WASPDefaultBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
return 'Success';
}
}
?>
|
As mentioned earlier, Agavi requires you to explicitly specify the View used to handle
different types of requests. For example, an executeRead() method specifies how to handle GET requests, while an executeWrite() method specifies how to handle POST requests. In this case, since the StaticContentAction will only handle GET requests, it must contain an executeRead() method, and this method should reference the StaticContentSuccessView.
It's worth noting that the Success View is invoked only if the input validation tests mentioned earlier are successful. If input validation fails, Agavi default behaviour is to generate the Error View.
Let's now get to the meat of this section: Setting up the StaticContentSuccessView to serve static pages. Open the view file at $WASP_ROOT/app/modules/Default/views/StaticContentSuccessView.class.php, and fill it with the code in Listing 15:
Listing 15. The Default/StaticContentSuccess View definition
<?php
class Default_StaticContentSuccessView extends WASPDefaultBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
$words = explode('-', $rd->getParameter('page'));
array_walk($words, create_function('&$i', '$i = ucfirst(strtolower($i));'));
$tmpl = 'StaticContent' . implode($words);
if (file_exists(
dirname($this->getLayer('content')->getResourceStreamIdentifier())
. "/$tmpl.php")) {
$this->getLayer('content')->setTemplate($tmpl);
} else {
return $this->createForwardContainer(
AgaviConfig::get('actions.error404_module'),
AgaviConfig::get('actions.error404_action'));
}
}
}
?>
|
This might seem complicated, but it actually isn't. Fundamentally, this View tries to match the URL request to a static page template, and render it if available. First, the View's executeHtml() method retrieves the page variable that was set up by the routing subsystem a few steps ago, using the AgaviRequestDataHolder object's getParameter() method. It then reformats this value according to a pre-defined file naming convention and attempts to find and render a template file with the same name.
For example, if the URL requested by the client was /content/hello-world, the call to
$rd->getParameter('page') returns the value hello-world, and the View therefore looks for a template named $WASP_ROOT/StaticContentHelloWorld.php. If the template file exists, the View sends it on to the client; if not, it automatically forwards to Agavi's default Error404 view, which displays a "Page not found" error to the client.
It's also a good idea at this time to set up the StaticContentErrorView, at $WASP_ROOT/app/modules/Default/views/StaticContentErrorView.class.php,
to specify the behaviour when input validation fails (Listing 16).
Listing 16. The Default/StaticContentError View definition
<?php
class Default_StaticContentErrorView extends WASPDefaultBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
return $this->createForwardContainer(
AgaviConfig::get('actions.error404_module'),
AgaviConfig::get('actions.error404_action'));
}
}
?>
|
This is pretty simple—you just forward to the default Error404 action again.
Now, all that's left is to actually define the static pages. Look back at Figure 6 (or the template in Listing 10.You'll find two
items in the main menu that might reasonably be classified as static content—About Us and Other Services. Create two new template files at $WASP_ROOT/app/modules/Default/templates/StaticContentAboutUs.php and $WASP_ROOT/app/modules/Default/templates/StaticContentOtherServices.php and fill them with some static content, as in Listing 17.
Listing 17. The Default/StaticContentAboutUs template
We've been dealing with used sports cars since 1996, and have built up an
enviable reputation for honesty, transparency, and fairness in all our dealings.
Rest assured, when you buy a used car from us, you're not just buying an automobile...
you're buying a solid guarantee of service and quality!
<p/>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
<p/>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
|
Then, open your Web browser and visit either http://wasp.localhost/content/about-us or http://wasp.localhost/content/other-services, and you should see your static pages (Figure 7).
Figure 7. A static page
You have one minor item left: Linking the items in the main menu to the newly-created static content pages. Open the master template file, and use Agavi's route generator to automatically generate routes to these pages, as in Listing 18.
Listing 18. The updated application master template
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
...
<div id="menu">
<ul>
<li><a href="<?php echo $ro->gen('index'); ?>">
Home</a></li>
<li><a href="#">For Sale</a></li>
<li><a href="<?php echo $ro->gen('content',
array('page' => 'other-services')); ?>">Other Services</a>
</li>
<li><a href="<?php echo $ro->gen('content',
array('page' => 'about-us')); ?>">About Us</a></li>
<li><a href="#">Contact Us</a></li>
</ul>
</div>
...
</html>
|
Manually generating a route in Agavi is simplicity itself. Within your View or
template, use the AgaviRouting object, represented by $ro,
to generate the route by calling its gen() method with the
route name (remember that name attribute you set up when
defining the route?). Route parameters can be specified within an array, and passed to the gen() method as a second argument.
Save your changes to the master template, and revisit your application's index page. The main menu links for the static pages should now be active.
It's worth pointing out, for the purists among you, that there is actually an easier way to serve static pages in an Agavi application: You can simply place them in the application's pub/ directory as HTML files, and link to them manually. However, these pages cannot use Agavi's master template and need their own layout markup, which creates update problems at a later date; also, they don't benefit from Agavi's caching and security mechanisms.
Whew! That's about it for this first article. I introduced you to Agavi, explained its unique features, and guided you through the process of installing and configuring the framework on your development machine. I also explained the basics of how Agavi handles Web requests, and how to use the Agavi build script to quickly set up new projects and class file templates. Finally, I got you started with the WASP example application, which will serve as a testbed application throughout this series.
Your example application is still in its early stages: It knows how to serve up the index page and some static content, and it has a decent master template ready for further development. Implementing even this very basic functionality should give you some insight into the most important components of an Agavi application—routes, actions, views, validators, templates, and modules—and set up the foundation for the more complex functions you'll address in the second part of this article series.
You can download the code archive for the WASP application with the functions discussed to date in this article. I recommend you get it, start playing with it, and maybe try your hand at adding new things to it. I guarantee you won't break anything, and it will definitely add to your learning. Until next time...happy experimenting!
| Description | Name | Size | Download method |
|---|---|---|---|
| Archive of the WASP application | wasp-01.zip | 3075KB | HTTP |
Information about download methods
Learn
- Introduction to MVC Programming with Agavi, Part 2: Add forms and database support with Agavi and Doctrine (Vikram Vaswani, developerWorks, July 2009): Continue to explore the flexible, open-source Agavi framework as you create an input form, auto-generate data models with Doctrine, and integrate these models into the Agavi project (Part 2 of a five-part series).
- Introduction to MVC Programming with Agavi, Part 3: Add authentication and administrative functions with Agavi (Vikram Vaswani, developerWorks, August 2009): Build more function into the sample Web Automobile Sales Platform by adding the ability to add, delete, and update the automobile records. You will also see how to separate user functions from administrative functions with authentication. (Part 3 of a five-part series).
- Introduction to MVC programming with Agavi, Part 4: Create an Agavi search engine with multiple output types including XML, RSS, or SOAP (Vikram Vaswani, developerWorks, August 2009): Implement a simple search engine and add support for multiple output types such as XML, RSS, or SOAP for your sample Agavi program (Part 4 of a five-part series).
- Introduction to MVC programming with Agavi, Part 5: Add paging, file uploads, and custom input validators to your Agavi application (Vikram Vaswani, developerWorks, September 2009): Polish your sample Agavi application to support file uploads, store user data in sessions, integrate third-party libraries, and create custom input validators in this final article of the five-part series on Agavi. (Part 5 of a five-part series).
- Implement access control with Agavi (Vikram Vaswani, developerWorks, October 2009): Add sophisticated role-based access control to your Agavi app with the full-featured API for user authentication and role-based access control in Agavi.
- The official Agavi Web site and the Agavi Guide: Learn more about this scalable PHP5 application framework that follows the MVC paradigm.
- The Agavi API documentation:Take a closer look at the Agavi base classes.
- Using version control with Agavi: Find out a little more as you finish the setup in this tutorial.
- The Agavi blog: Read Agavi news.
- The Agavi mailing lists and IRC channels: Participate in the Agavi community, ask questions and get answers.
- Open Source area on
developerWorks: Learn about application development with CakePHP (Duane O'Brien, June 2009),
other PHP
frameworks (Duane O'Brien, October 2007), and building a PHP wiki (Duane O'Brien, February 2007).
- IBM XML certification: Find out how you can become an IBM-Certified Developer in XML and related technologies.
- XML technical library: See the developerWorks XML Zone for a wide range of technical articles and tips, tutorials, standards, and IBM Redbooks.
- developerWorks technical events and webcasts: Stay current with technology in these sessions.
- The technology bookstore: Browse for books on these and other technical topics.
- developerWorks
podcasts: Listen to interesting interviews and discussions for software developers.
Get products and technologies
- The Agavi framework: Download this PHP5-MVC pattern application framework for cleaner extensible code.
- The Phing build system: Download a project build system based on Apache Ant.
- IBM product evaluation versions: Download or explore the online trials in the IBM SOA Sandbox and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
- XML zone discussion forums: Participate in any of several XML-related discussions.
- developerWorks blogs: Check out these blogs and get involved in the developerWorks community.

Vikram Vaswani is the founder and CEO of Melonfire, a consulting services firm with special expertise in open-source tools and technologies. He is also the author of the books PHP Programming Solutions and PHP: A Beginners Guide.




