IntroductionIf you’ve ever used an Internet application to purchase a book, buy a ticket, or check your e-mail, you’ve most likely encountered the ubiquitous user login page. Any sufficiently complex web application necessitates the creation of user accounts and the authentication of these accounts via some kind of user management module. Before a user can successfully log into an application for the first time, they must be presented with the opportunity to create an account. A user’s account data is typically saved in a database and used to validate against when they return to the application via the login screen. This is nothing new, and has been solved in many ways by thousands of applications. However, as I will describe later, this scenario is a good one for introducing complex application logic in WASP, and you will be able to see how simple and powerful it is to write code in such a framework.
The example in this article is written using the Open Source PHP 5 framework WASP. WASP is a powerful web application framework built on PHP 5. Frameworks have become popular in the web application community for their ease of use and ability to solve common problems quickly. WASP strives to allow web developers to make great applications with more fun and less code, but in the familiar playground of PHP.
To get the most of this article you should be familiar with PHP 5 and have a general understanding of the WASP framework. More information about WASP can be found by following the links at the end of this article.
Programming in Three TiersThe Model-View-Controller software architecture design pattern has long been considered an optimal choice for enterprise software developers. Using the MVC approach divides your software into three separate, but related, tiers—one for the data model, one for the user interface view, and one controller to tie in event handling and business processes. With this separation, you are able to isolate changes in one part of the application from the others. For example, if you were given a new requirement that your application needed to support a PocketPC interface, you can add another View to support it, leaving your business logic largely unaffected. By keeping your application compartmentalized in the MVC style, you minimize the number of places in your code that are affected by an unforeseen requirements change, or even an unexpected bug.
The WASP framework for PHP was built with this architectural approach in mind, and makes it easy to write your own PHP applications in the MVC style.
The Underlying ModelThe Model is the ‘M’ in MVC, and it is the abstract representation of the data the application interfaces. In most applications, the model is represented by an SQL-driven relational database and a set of business rule functions [or methods] defining how the data should be entered, retrieved, and deleted. The model layer in WASP adds a level of abstraction to the database that removes the need for common SQL queries, making it easier to save and retrieve data for your application.
For the sample application, I’ve created a database table called user that will hold the user account information for the application. The user table will hold the user’s name, e-mail address, physical address and phone number, and password. The SQL shown in Listing 1 will create the table for you.
Listing 1
user Table SQL
CREATE TABLE user (
user_id int(10) unsigned NOT NULL auto_increment,
first_name varchar(45) NOT NULL,
last_name varchar(45) NOT NULL,
password_hash varchar(45) NOT NULL,
summary text,
email varchar(45) NOT NULL,
phone varchar(45) default NULL,
address1 varchar(45) default NULL,
address2 varchar(45) default NULL,
city varchar(45) default NULL,
state varchar(45) default NULL,
postal varchar(45) default NULL,
PRIMARY KEY (user_id)
);
Now that you have your database table created, this is a good time to set up your WASP application environment. Instructions for doing this are in the documentation and tutorials on the home page. Ensure that you have an Application directory and your
build.properties file configured properly. After running the command
phing db you should have a db directory with a
UserWrapper.php class file in it. This is your auto-generated model for this example.
Edit the
UserWrapper.php class file that was just created and add the code shown in Listing 2 to finish your model’s business methods (below the
//##END_GENCODE line).
The
validateSave() method gets executed by the framework when the
save() method is called, and ensures all required fields are set. If not, an error will make its way up from the model, through the controller, to the view where the user will see it. You can see that the save method generates a new password for the user if the primary key has not been set (which can only happen if this is a new record) and sends it to them via e-mail.
The Simplicity of the ViewThe View is the layer that exposes the data in the model in a way that makes sense to a particular user using a particular interface. A sufficiently abstract view layer provides a means to separate the user interface from the business logic of the application, allowing for improvements and extensions of the interface without worrying about interfering with the underlying code. The view layer in WASP uses the PEAR Flexy template manager to render HTML pages.
With respect to our example, we will need to provide a view component to allow for account creation. Your application will need to present the user with a screen to collect their personal and credential information. The information collected should include the user’s name, e-mail address (which will be used as the login credential since it is guaranteed to be unique to a user), physical address and phone information. Other information can also be collected as needed by your application. Notably left out, however, is a password field. For this pattern, we have written code in our model to generate a password upon account creation and e-mail it to the user (via the
generatePassword() method in
UserWrapper.php). This ensures the user’s e-mail address is valid; otherwise, they will not be able to log in to the application because they will never receive their password.
In your WASP application, create a user module by running the command
phing module and naming it user. Edit the
user/templates/user.chunk so it contains the HTML shown in Listing 3.
Note that in WASP it is important that the names of the form fields in the template match the names of the columns in the
user table, since the controller will pass these values to the model when the user presses the
create button.
The Power of the ControllerThe controller in the MVC architecture is the event handling glue that makes the application flow together. The UIModule and Chunk classes that determine page flow for a particular state of the application encompass the controller in WASP. In this example, there will be two states the module will care about: REGISTER and LOGIN. Edit the UserModule.php class in your user directory and make the code changes as shown in Listing 4.Listing 2
Business Methods in UserWrapper.php
/**
*Called before save(), validates the object to make sure all required fields
*are set correctly.
*/
public function validateSave()
{
$arErrors = array();
if ($this->getFirstName() == null || $this->getLastName() == null)
$arErrors[] = ‘Please enter First and Last Name.’;
if ($this->getEmail() == null)
$arErrors[] = ‘Please enter a valid email address.’;
if ($this->getCity() == null)
$arErrors[] = ‘Please enter your city’;
if ($this->getState() == null)
$arErrors[] = ‘Please enter your state/province’;
if ($this->getId() == null && $this->getEmail() != null)
{
//Check for existing user
$oExisting = new UserWrapper();
$oExisting->setEmail($this->getEmail());
$oExisting->find();
if ($oExisting->next())
$arErrors[] = ‘Account with that email address already exists.’;
}
return $arErrors;
}
private function generateNewPassword()
{
$stPassword = Users::generatePassword(6);
$this->setPasswordHash(md5($stPassword));
}
public function save()
{
//Check for saving new users
$boNew = ( $this->getId() == null);
if ($boNew)
$this->generateNewPassword();
arErrors = parent::save();
if (! count($arErrors) && $boNew)
{
//This is a new user, so email their password
$this->emailPassword();
}
return $arErrors;
}
public function emailPassword($boForgot = false)
{
//Email unencrypted password if forgotten
if ($boForgot)
{
$this->generateNewPassword();
$this->save();
}
Emailer::send($this->getEmail(), “Welcome”,
“Here is your new account password” .
“\n\n password: ” . $this->_stPassword . “\n\n” .
“Visit ” . ROOT_URL . “Login to login.”);
The
UserModule.php class above will handle the display of the
UserIndexPage or the
LoginPage depending on whether the user has been through the
UserIndexPage’s account creation process or not. I’ll talk about the
LoginPage class in a bit; for now, let’s work on the
UserIndexPage.
I’ve already written the View for the
UserIndexPage using the HTML detailed earlier. Now let’s write a controller class that displays that particular view. Edit
UserIndexPage.php and give it the code shown in Listing 5.
The
UserIndexPage class saves the data entered on the user creation page by listening for the create button’s press in the request at the line if (
Request::getParameter(‘create’)). If it was pressed, the controller constructs a User model class and fills it using the form fields submitted by the user. The
save() method is then called, and any errors found by the
validateSave() method in the
UserWrapper class are captured. If there are no errors, the user is redirected with the
UserModule’s STATE parameter set to
STATE_LOGIN, which [you’ll remember from the
UserModule class code] causes it to draw the
LoginPage.
Listing 3
User Creation Page HTML for user.chunk
<html>
<body>
<form method=“post”><input type=“hidden” name=“user_
id”><h1>Create Account</h1>
<br />
<hr size=“1” noshade>
<table width=“660” border=“0” cellspacing=“0”
cellpadding=“6”>
<tr>
<td width=“141”><div align=“right”><strong>First Name:</
strong></div></td>
<td width=“503”><input name=“fi rst_name” type=“text”
value=“”></td>
</tr>
<tr>
<td width=“141”><div align=“right”><strong>Last Name:</
strong></div></td>
<td width=“503”><input name=“last_name” type=“text”
value=“”></td>
</tr>
<tr>
<td><div align=“right”><strong>Email:</strong></div></td>
<td><input name=“email” type=“text” value=“”></td>
</tr>
<tr>
<td><div align=“right”><strong>Password: </strong></
div></td>
<td><input name=“password” type=“password” value=“”></
td>
</tr>
<tr>
<td><div align=“right”><strong>Password Again: </
strong></div></td>
<td><input type=“password” name=“password_verify”></
td>
</tr>
<tr>
<td><div align=“right”><strong>Address:</strong></div></
td>
<td><input type=“text” name=“address1”></td>
</tr>
<tr>
<td><div align=“right”><strong>City:</strong></div></td><td><input type=“text” name=“city” onBlur=“validName(th
is);”></td></tr>
<tr>
<td><div align=“right”><strong>State / Province:</strong></
div></td>
<td><input type=“text” name=“state”></td>
</tr>
<tr>
<td><div align=“right”><strong>Country:</strong></div></
td>
<td><input type=“text” name=“contry”></td>
</tr>
<tr>
<td><div align=“right”><strong>Zip/Postal Code:</
strong></div></td>
<td><input type=“text” name=“postal”></td>
</tr>
<tr>
<td><div align=“right”><strong>Summary of You:</
strong></div></td>
<td>
<textarea name=“summary” cols=“35” rows=“3”
wrap=“VIRTUAL”></textarea> </td>
</tr>
<tr><td> </td>
<td>
<div align=“left”>
<input type=“submit” name=“create” value=“Create
Account”> </div>
</td>
</tr>
</table>
</form>
</body>
</html>
Listing 4
UserModule.php Controller Class
<?php
class UserModule extends UIModule
{
const STATE = ‘state’;
const STATE_REGISTER = ‘register’;
const STATE_LOGIN = ‘login’;
function __construct()
{
$this->setLoginRequired(false);
parent::__construct();
}
protected function init()
{
//First visit - no state parameter set
$this->_stPageState = self::STATE_LOGIN;
//check for state
if (Request::getParameter(self::STATE) == self::
STATE_REGISTER)
$this->_stPageState = self::STATE_REGISTER;
}
protected function handleEvents()
{
switch ($this->_stPageState)
{
case self::STATE_REGISTER:
$this->addContent(new UserIndexPage());
break;
case self::STATE_LOGIN:
$this->addContent(new LoginPage());
break;
}
}
}
?>
Listing 5
UserIndexPage Controller Class
<?php
class UserIndexPage extends Chunk
{
const TEMPLATE_NAME = ‘./user/templates/user.chunk’;
function __construct()
{
parent::__construct( array(self::TEMPLATE_NAME));
}
protected function init()
{
}
protected function handleEvents()
{
if (Request::getParameter(‘create’))
{
$oUser = new UserWrapper();
$oUser->fillFromRequest();
$this->_arErrors = $oUser->save();
$this->setFormElements($oUser->toArray());
if (! count($this->_arErrors))
$this->redirect($this->getSelfUrl(UserModule::STATE
. ‘=’ .
UserModule::STATE_LOGIN));
}
}
public function draw()
{
parent::draw();
}
}
?>
Listing 6
Login Page HTML
<html>
<head>
<script src=“prototype.js” type=“text/javascript”></script>
<script language=“Javascript”>
function performAJAXLogin()
{
var stPars = “?email=” + $F(‘email’) + “&” +
“password=” + $F(‘password’) +
“&login=true&ajax=true”;
var myAjax = new Ajax.Request(location.href,
{method: ‘get’, parameters: stPars,
onComplete: handleResponse});
}
function handleResponse(originalRequest)
{
if (originalRequest.responseText.indexOf(“http”) != -1)
window.location = originalRequest.responseText;
else
$(‘error’).innerHTML = originalRequest.responseText;
}
</script>
</head>
<body>
<div align=“center” style=“color:red” id=“error”></div>
<form name=“loginForm” method=“post”>
<center>
<fieldset style=“width:30%”>
<legend><span class=“sign”>sign in</span></legend>
<table>
<tr>
<td><strong>Email:</strong></td>
<td><input type=“text” id=“email” name=“email”
tabindex=“1”></td></tr>
<tr>
<td><strong>Password:</strong></td>
<td><input type=“password” id=“password”
name=“password” tabindex=“2”></td>
</tr>
<tr>
<td colspan=“2”>
<span style=“float:left”><a href=“?state=register”>Cre
ate an account</a></span><span style=“float:right”><input
type=“button” name=“login” value=“Login” onClick=“performAJ
AXLogin();”></span>
</tr>
</table>
</fieldset>
</center>
</form>
</body>
</html>
A Touch of AJAXNow that you’ve written the code to allow a user to register in your application and create an account, you’ll need to give the user the opportunity to login. As part of this example
Login pattern, I’m going to include an AJAX action to check for valid username and password. This isn’t strictly necessary for this pattern, but it illustrates how you would include AJAX calls in an MVC WASP application. To provide an easy way to perform AJAX calls, this example uses the
Prototype JavaScript library. Download the package and place the
prototype.js library file in your application’s user directory.
Now you’ll need to create the View for the Login page. Make a file in
user/templates called
login.chunk and add the lines of code shown in Listing 6.
The JavaScript function
performAJAXLogin() is executed when the user clicks the ‘Login’ button on the page, sending the login information from the form back to the server. The result of the AJAX request is then handled in the
handleResponse() function which checks for a redirect URL if the login was successful; otherwise, it displays an error message on top of the page.
You’re probably wondering how the server handles this request. It certainly is going to look different since it is not sending back an entire copy of the page. More specifically, when the AJAX call is made, the server instead only sends back an error message or a URL to send to a page redirect. Fortunately, the code to handle this is not as complicated as you might expect. In fact, it is surprisingly simple, thanks to the power of the WASP framework. Create a file in user called
LoginPage.php and add the code shown in Listing 7 to it.
Listing 7
LoginPage Controller Class
<?php
class LoginPage extends Chunk
{
const TEMPLATE_NAME = ‘./user/templates/login.chunk’;
function __construct()
{
parent::__construct(array(self::TEMPLATE_NAME));
}
protected function init()
{
}
protected function handleEvents()
{
if (Request::getParameter(‘login’))
{
$oUser = new UserWrapper();
$oUser->fillFromRequest();
$oUser->setPasswordHash(md5(Request::getParameter
(‘password’)));
$oUser->find();
if ($oUser->next())
{
SessionManager::setParameter(UIModule::USER,
$oUser->getId());
die(ROOT_URL . ‘Main’);
//Send back the AJAX redirect URL
} else {
die(‘Invalid Email or Password.’);
//Send back the AJAX error
}
}
}
public function draw()
{
parent::draw();
}
}
?>
You’ll notice that the
handleEvents() method operates as you would expect, except that in both the success and failure cases the code calls
die(). Lets take a closer look—when the login button is pressed, a UserWrapper model object is created and filled with the e-mail address and password submitted via the AJAX request. The
find() method is called to see if there is a user that matches the entered credentials. If so, the execution is halted, returning the URL of a module in the application to which the user will be redirected (in this case ‘Main’, but you can set it to any place you like). If no matching user is found the execution halts, returning only the error message in the response, which is displayed to the user without refreshing the page.
ConclusionThis example was provided as a representation of an interface design pattern for account creation and logins in MVC with WASP. You’ve constructed a user model class, complete with business rules specific to the creation of a user account. You then constructed a view to allow entry of user information, which is then saved to the model using the event handling of the controller. Finally you created an AJAX assisted login page, which allows the user to login or link to the account creation page if necessary.
By keeping the components of this pattern abstracted in an MVC way, it can be easily applied to your own web applications with minimal customization. The model can be incorporated into your own database schema, the view can be changed to match your particular styles, and the controller can be linked to the logic of the rest of the application.