Copyright © 2002 Loren Siebert
Table of Contents
Ambivalence is a Model-View-Controller framework for PHP web development. Based on the Java-based Maverick project, Ambivalence also offers clean MVC separation and an XML sitemap. Ambivalence provides an optional service to authenticate and enforce access controls upon users, based on the JBoss implementation of the J2EE Java Authorization and Authentication Service (JAAS).
Core features include:
Configuration using an XML sitemap.
Easily encapsulation of variable content within a common layout, look-and-feel, etc.
Clean separation of application security logic from functionality via an XML security map.
Object-oriented data access infrastructure based on PEAR.
Table of Contents
An excellent quick introduction of MVC aka "Model 2" concepts can be found here.
This is a short explanation of how Ambivalence (and your PHP web application) processes requests.
Ambivalence processes commands that are mapped to a dispatcher through extension mapping. For example, all URIs which end with *.m are mapped to Ambivalence. The URI minus the extension is the command. For example, if a request comes in for http://your.site.com/runQuery.m, the command is "runQuery". However, if the request comes in for http://your.site.com/foo/runQuery.m, the command is "foo/runQuery".
What Ambivalence does with a command is determined by the Ambivalence configuration file (ambivalence.xml). Here is a complete (albeit simple) example:
<ambivalence version="1.0"> <command name="results"> <controller file="controller/Results.class" /> <view name="success" layout="layout/lookAndFeel.inc"> <param name="content" value="content/results.inc"/> <param name="title" value="Triathlon Results- Results"/> </view> <view name="error" layout="layout/lookAndFeel.inc"> <param name="content" value="content/errors/resultsError.inc"/> <param name="title" value="Triathlon Results- Results"/> </view> </command> </ambivalence>
This file defines a single command, results, which has two possible outcomes (views): "success" and "error".
When the results command is requested, the following steps occur:
An instance of the controller class Results.class in the directory controller is created.
The perform() method on the controller is called. This results in properties of the controller being populated with the HTTP request parameters prior to executing application logic.
During execution, the perform() method defines the object that will be used as the model.
The return value from perform() specifies the name of the view to render. This example will assume "success".
The model is placed in the global PHP context as the variable $model.
The PHP page lookAndFeel.inc in the layout directory is evaluated along with the appropriate view content (results.inc in the content directory), and these files use the data in $model to generate the view (in HTML, XML, etc).
The result is sent back to the client.
One possibly confusing aspect of Ambivalence is how the actual URL file is used, and what goes into it. For a command named "foo" mapped from the URI /foo.m, there must be an actual file named foo.m in the document root for that virtual host. Where things get slightly confusing is that the file must not have anything in it except for comments, because by the time the PHP parser actually gets down to the foo.m file, all processing has already occurred and the results have already been sent to the client. This is because Ambivalence does all of its processing in the prepend phase, so the actual file itself has nothing left to do. The only reason for the actual file to be there is to act as a placeholder for Apache, because if it is missing, Apache will give a 404 error. Here is an example balances.m file for a page whose MVC logic might display account balances:
<?php // placeholder for balances.m: This page fetches account balances for a user ?>
Once your application is in place, you will find that you spend more time modifying the model, views, and controllers of existing pages than you do setting up placeholders for brand new ones, so fiddling with the placeholder files is not something that consumes much time.
Ambivalence contains an authentication and authorization service (called PAAS) that enforces access control to resources based on principals and roles. PAAS is loosely based on a Java specification of this service in the J2EE space known as JAAS, and on a concrete implementation of this specification known as JBossSX (part of the JBoss J2EE application server suite).
The purpose of PAAS is to separate the security constraints surrounding resources from the actual resources themselves. Doing this yields several benefits. First, the application doesn't need to know how to secure itself-- this is done for it by PAAS. So changes to the secutity restrictions to a resource do not require any changes to the actual application code. Second, separating out the security piece makes it easy to have different constraints depending on where the application is deployed. For instance, my application deployed in an intranet setting might allow people with the User role to see a directory listing, but in an extranet setting, I might require a Manager role. With PAAS, this is easily done by making a simple change to the XML configuration file.
Currently, PAAS allows for FORM-based login using some principal (e.g. username) and credential (e.g., password).
Controllers are responsible for processing commands and building up the data model for the view to use when rendering results to the client. Ambivalence has a base controller class that populates the derived controller class with any HTTP GET/POST variables before calling the controller's perform() method. Your BaseController subclasses should have setter methods for any parameters you are expecting to get via HTTP GET/POST. For example, if you have an HTML form that contains a text input called "salary" then your controller must have a method called setSalary() that takes the variable as an input and sets a member variable for use later in the perform() method.
Note how the perform() method first calls the perform() method of its parent.
Below is an example of a controller class for a home page that inherits from the ControllerBase class. Note that the ControllerBase.class file doesn't need to be included here because it is already required by Ambivalence.
<? require_once("model/AdvertDAO.class"); require_once("model/LoggedRequestDAO.class"); class Home extends ControllerBase { var $rsv; var $prv; var $ad; var $salary; function getPopularRace() { return $this->rsv; } function getAdvert() { return $this->ad; } function getPopularResult() { return $this->prv; } function getSalary() { return $this->salary; } function setSalary($sal) { $this->salary = $sal; } function perform() { parent::perform(); $advertDAO = new AdvertDAO; $loggedRequestDAO = new LoggedRequestDAO; $this->ad = $advertDAO->getAdvert("generic"); $this->rsv = $loggedRequestDAO->getPopularRaceView(); $this->prv = $loggedRequestDAO->getPopularResultView(); return "success"; } } ?>
PAAS makes use of an object oriented data access layer built on top of the PEAR::DB library. The code is based on the Data Access Object design pattern, as outlined in this J2EE Blueprint. By using DAO's in your application and inheriting from the BaseDAO class, you get several benefits:
Here is an example DAO that a controller might use to log requests to a database:
<?php require_once("BaseDAO.class"); class LoggedRequestDAO extends BaseDAO { function logRaceRequest($raceId, $remoteAddr) { $insertStatement = "insert into request_log (race_id, ip_address) " . "values (".$raceId.",'".$remoteAddr."')"; $result = $this->db->query($insertStatement); if (DB::isError($result)) { $this->halt($result->getMessage(),$insertStatement); } } } ?>
To enable Ambivalence's data access layer, you need to specify a default datasource to use. If you don't specify the name of your datasource name (DSN) file in the DAO constructor, Ambvivalence will search for the DefaultDSN.inc file in your include path. Here is an example of a DSN file:
<?php $user = 'mysql'; $pass = 'mysql'; $host = 'localhost'; $db_name = 'dev_db'; ?>
Note that you can specify multiple DSN's in your application. In fact, each DAO you create can connect to a different database--- all you have to do is specify which datasource file to use. In addition, you can have the same DSN file defined differently in different environments. For example, the DefaultDSN.inc file may point to a development database, a staging database, or a production database depending on where the code is being deployed.
Layouts allow an easy way for a common look and feel to be applied to many different views. The difference in Ambivalence is that the view doesn't know what layout it is in--- it doesn't reference it anywhere. The look and feel, or layout, for a view is set up in the ambivalence.xml file.
Layouts contain all the content that doesn't change depending on which view gets rendered for a command. Views contain the rest. Here is an example layout file where the title and content are set dynamically (in the ambivalence.xml file) but the rest of the page is static.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <title><?=$title?></title> </head> <body bgcolor="#eeeeee"> <? require($content) ?> </body> </html>
Table of Contents
Ambivalence makes use of PEAR's database functionality for its authentication and authorization service, and also provides access to data via a Data Access Object pattern that you are encouraged to inherit from in your application. You are free to use other database packages out there (PHPlib) or roll your own, but if you leave things as they are, you will just need to make sure you have the PEAR libraries properly installed on your system. Newer versions of PHP come pre-bundled with PEAR, so you may not have to do anything, but if you see errors complaining that "DB.php" cannot be found, you should refer to the PEAR site for help.
Ambivalence also makes use of PHP4 native sessions. The very first thing Ambivalnce does on each invocation is call PHP's session_start() function. If you get any session-related errors, you should check to make sure your version of PHP properly supports sessions. The PHP main site has excellent references to help with this. As with PEAR, if you are using the latest version of PHP, everything should just work.
The first step in building an Ambivalence application is to configure Apache/ PHP so that the Ambivalence dispatcher is invoked for all commands with the ".m" extension.
<?php // The only three lines you need. require_once("ambivalence_init.inc"); // This one is crucial require_once("paas.inc"); // optional, used for authentication/authorization require_once("ambivalence.inc"); // This one is crucial ?>
The heart of a Ambivalence application is the ambivalence.xml file. This file contains all the information about what commands are available, what code (controllers) are associated with specific commands, and what views can result from a command.
This file can reside anywhere in your PHP include path, but for simplicity you may want to just put it in your application-specific include directory (e.g., /path_to/myapp/php-include).
By way of convention, the following constructs are usually considered equivalent and can be used interchangably:
<element attr="blah"/>
<element><attr value="blah"/></element>
See the appendix for an explanation of the options available in the Ambivalence configuration file.
The paas.xml file is currently used by the authentication and authorization system to specify application-wide login modules along with any security constraints imposed on the application. If your application is not using this (i.e., your auto-prepend file does not include paas.inc), then you do not need this file. Otherwise, Ambivalence will look for this file in your PHP include path.
See the appendix for an explanation of the options available in the paas.xml file.
For small applications, it's easy to stick model, view, and controller code all in one place, but when small applications grow large, it pays to have some structure in place already. Here is a sample directory tree for an Ambivalence application.
| +->htdocs (The web server document root containing the placeholder *.m files, along with any images, plain HTML files, or even PHP files that aren't processed by Ambivalence) | +->php-include (contains all XML config files and DefaultDSN.inc, and prepend file if not set up elsewhere) | +->model (The Data Access Objects and data structure files) | +->controller (Command processing classes here) | +->layout ("Look-and-feel" wrappers) | +->content (Specific views to be wrapped by layouts) | +->errors (Error-related views)
Table of Contents
Using PHP as the templating language with Ambivalence is as straightforward as can be, in that you can just access the properties of the $model object directly in your view code, along with any other variables you may have set up in the ambivalence.xml file.
The biggest risk in using PHP as the templating language is that view developers must be vigilant in using only the aspects of PHP required to display data from the model. That is, it is very easy for someone to break the MVC separation by putting business logic or even a database call in the view code. Following releases of Ambivalence will allow for proper restricted templating (perhaps even via a port of the excellent Velocity language from the Jakarta project) so the onus is not on the view developers to know what not to use when authoring view files.
But if you simply limit yourself to a few directives, PHP can effectively function as a templating language for your Ambivalence project, without any of the extra overhead that yet another layer would introduce. If you use the shorthand for opening PHP code segments, the view code stays much easier to read. Here are the directives you should use along with some examples:
<? $foo = "bar"; ?> <? $bar = 7.7; ?> <? $wholename = $firstName." ".$lastName; ?> <? $salary = $model->getSalary(); ?> <? $nameArray = $model->getNames(); ?>
<? if ($salary > 1000000) : ?> <strong>You are rich!</strong> <? endif; ?>
<? foreach ($model->notes as $note) : ?> One note per line... <?= $note ?> <? endforeach; ?>
<? include ("content/fragments/advertisingBlock.inc"); ?>
Ambivalence is hosted by SourceForge. For updated information, visit the website or the project page.
Downloads are available here.
For help using Ambivalence and discussion of the future direction of Ambivalence, subscribe to the amb-user mailing list.
A comprehensive sample application built with Ambivalence is in the works. One real world application built with Ambivalence is the Triathlon Results Application.
Ambivalence is an Open Source project distributed with an Apache-style license. It was created by Loren Siebert based on original Java work done by Jeff Schnitzer and Scott Hernandez, along with ideas from the J2EE world. All the good ideas come directly from these projects, and all the errors are my own. The first public release was 21 July 2002.
This product includes software developed by the Apache Software Foundation (http://www.apache.org/).
Table of Contents
ambivalence — The root element of Ambivalence configuration
ambivalence ::= (command+)
Name | Type | Default |
---|---|---|
version | CDATA | Required. Must be "1.0" |
ambivalence is the root element of Ambivalence configuration. It contains everything that deals with model/view/controller specification.
version. The version of the schema. Must be 1.0.
<ambivalence version="1.0" > <command> ... </command> <command> ... </command> <command> ... </command> </ambivalence>
command — Defines the behavior of one command
A command is the basic unit of work in Ambivalence. When an HTTP request is processed by Ambivalence, the URI is examined to determine which command to execute. If no command matches the URI, Ambivalence returns an error. Note that there are two types of "not found" cases, and they differ. One results from the actual placeholder file associated with the URI being missing. Apache handles this with a 404 error. The other occurs when the placeholder exists but the actual command isn't defined in the ambivalence.xml file, in which case Ambivalence returns an error explaining what happened.
The contents of a command element determine the runtime behavior. You can define a controller class and some number of views, one of which will be rendered for every request.
Note that the controller child element is optional, but if it is left out, there can be only one view element since a controller is needed to select from the available views.
name. The URI to associate with this command, minus the context root, extension, or query parameters. For example, "welcome" and "protected/reportDetail".
<ambivalence version="1.0" > <command name="portfolio"> <view layout="layout/lookAndFeel.inc"> <param name="content" value="content/portfolio.inc"/> <param name="title" value="My Portfolio"/> </view> </command> </ambivalence>
controller — Associates a controller with a particular command
A controller is a user class which "controls" how the request is handled. Here is where user code is executed, the model is prepared, and which view to render is chosen.
file. The full name of the class file which will act as the controller. This class must inherit from ControllerBase.class.
<ambivalence version="1.0"> <command name="results"> <controller file="controller/Results.class" /> ... </command> </ambivalence>
Note: In this example, Ambivalence will instantiate a Results object out of the Results.class class code found in the controller directory, which of course must be in the PHP include path.
view — Defines a view as part of a command
view ::= (param*,[Special])
Name | Type | Default |
---|---|---|
name | CDATA | |
layout | CDATA | Required |
A view renders the model to the response. Zero or more view elements can appear under a command element. Here they are named (with the name attribute) and designate the options available in the command for rendering the model. The layout attribute allows command-specific presentation to be separated from a common look-and-feel or template applied throughout a site. The layout can be defined in one file, and any changes to this file will automtically be reflected throughout all the views that use that layout.
Note that if a command defines only a single view, it does not need to be named. Regardless of what the controller returns, the one view will be rendered.
param child elements populate the global context variables available to the view code. In the example below, the parameter "title" is set to "Triathlon Results", so the variable $title is populated with this value in the global scope. The meaning of a param is specific to the particular View type.
name. This is the string that the Controller will return to identify which view to render. Can be omitted if there is only one view for a command.
layout. The "look and feel" or parent view to be wrapped around the current view.
<ambivalence version="1.0"> <command name="results"> <controller file="controller/Results.class" /> <view name="success" layout="layout/lookAndFeel.inc"> <param name="content" value="content/results.inc"/> <param name="title" value="Triathlon Results"/> </view> <view name="error" layout="layout/lookAndFeel.inc"> <param name="content" value="content/errors/resultsError.inc"/> <param name="title" value="Triathlon Results- Error"/> </view> </command> </ambivalence>
Table of Contents
paas — The root element of the PAAS configuration
paas ::= (datalogin, security-constraint+, login-config)
Name | Type | Default |
---|---|---|
version | CDATA | Required. Must be "1.0" |
paas is the root element of the configuration for the PHP Authentication and Authorization Service (PAAS). It contains everything that deals with authentication and authorizization or users for web application resources.
version. The version of the schema. Must be 1.0.
<paas version="1.0" > <datalogin> ... </datalogin> <security-constraint> ... </security-constraint> <security-constraint> ... </security-constraint> <login-config> ... </login-config> </paas>
datalogin — Defines the datasource name and the queries to use for both authentication and authorization.
datalogin ::= (dsnFileName, principalsQuery, rolesQuery)
Name | Type | Default |
---|---|---|
dsnFileName | CDATA | Required |
principalsQuery | CDATA | Required |
rolesQuery | CDATA | Required |
The datalogin element tells PAAS which datasource to use when validating users and their credentials, as well as the queries to use. The principalsQuery and rolesQuery attributes specify queries to first authenticate and then authorize the user. The "?" in the queries is replaced at runtime by PAAS with the principal passed in to it (usually a username). It is up to you to write these queries in a way that is legal for your database.
dsnFileName. The dsnFileName attribute points to the file containing the connection information for the datasource associated with the security information. It can be the same one used by the application by default (DefaultDSN.inc), or it can be a separate one as shown in the example below.
principalsQuery. The principalsQuery should return a single value.
rolesQuery. The rolesQuery should return a column of possible roles for that principal. Of course, the column may be emtpy or have only one element in it.
<paas version="1.0"> <datalogin> <dsnFileName>SecurityDSN.inc</dsnFileName> <principalsQuery>select password from Users where username = '?'</principalsQuery> <rolesQuery>select role from Roles where username = '?'</rolesQuery> </datalogin> </paas>
security-constraint — The security-constraint element is used to associate security constraints with one or more web resource collections.
security-constraint ::= (web-resource-collection+, auth-constraint)
none
A security-constraint specifies which roles are sanctioned to access certain resources deemed to be protected.
none
<paas version="1.0"> <security-constraint> <web-resource-collection> <web-resource-name>Restricted</web-resource-name> <description>Authorized users only</description> <url-pattern>/au/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>AuthenticatedUser</role-name> </auth-constraint> </security-constraint> </paas>
login-config — The login-config element is used to configure the authentication method that should be used (currently only FORM is supported), and the attributes that are needed by the form login mechanism.
login-config ::= (auth-method, form-login-config)
Name | Type | Default |
---|---|---|
auth-method | CDATA | FORM |
The login-config element tells PAAS which pages to show for logging in a user and for displaying an error during the login process.
auth-method. Currently only "FORM" based authentication is available.
<paas version="1.0"> <login-config> <auth-method>FORM</auth-method> <form-login-config> <form-login-page>/login.m</form-login-page> <form-error-page>/loginError.m</form-error-page> </form-login-config> </login-config> </paas>
web-resource-collection — The web-resource-collection element is used to identify a subset of the resources within a web application to which a security constraint applies.
web-resource-collection ::= (web-resource-name, description, url-pattern)
Name | Type | Default |
---|---|---|
web-resource-name | CDATA | Required |
description | CDATA | Required |
url-pattern | CDATA | Required |
The web-resource-collection element tells PAAS which url-pattern to restrict, and gives this restriction a name and a description.
web-resource-name. The web-resource-name attribute is just a human-readable name for this restriction.
description. The description is just a human-readable description for this restriction.
url-pattern. The url-pattern is a regular expression denoting which URLs should be restricted.
auth-constraint — The auth-constraint element indicates the user roles that should be permitted access to this resource collection.
The auth-constraint element tells PAAS which role is allowed to access this resource.
role-name. The role-name attribute is a text string that corresponds to a role in the table that matches users to roles, specified in the rolesQuery attribute of the datalogin element.
form-login-config — The form-login-config element specifies the login and error pages that should be used in form based login.
form-login-config ::= (form-login-page, form-error-page)
Name | Type | Default |
---|---|---|
form-login-page | CDATA | Required |
form-error-page | CDATA | Required |
The form-login-config element tells PAAS which page to load when the security interceptor detects an invalid access attempt to a restricted resource, and which page to show if the login attempt fails.
form-login-page. The form-login-page attribute becomes the target of a redirect when a login is required by the security interceptor.
form-error-page. The form-error-page attribute becomes the target of a redirect when a login attempt fails.
B.1. |
Why did you choose the name Ambivalence? |
Because Maverick was already taken. |
|
B.2. |
Huh? |
grep -i m.*v.*c /usr/share/dict/words |
|
B.3. |
What public websites are using Ambivalence? |
|