Another MVC approach
Join the DZone community and get the full member experience.
Join For FreeSo, after having read and heard much about the Model/View/Controller pattern, I decided to apply it to a new web application at the office. After much trial and error, redesign, refactoring, and more reading, I've come up with a simple architecture that actually works quite well. If you're not at all familiar with MVC, I suggest you read the wiki page for a brief introduction. Ready?
My application can be broken down into roughly 5 objects: the FrontOffice object, Controllers, Actions, ActionResults and Views.
FrontOffice
The
FrontOffice is where the request comes in. Since a request could come
in through any kind of protocol, I've defined FrontOffice as an
abstract class, extending on it for each protocol. In this case, we'll
use the FrontOffice_URL object, to extract the request from the URL.
The FrontOffice simply translates passes the request on to the
Controller that's responsible for handling the requested Action. This
is called a single point of entry.
Controller
I´ve
decided to cut the controller in three pieces to allow for more
abstraction. The Controller only takes care of the mapping from an
action alias to an action class, providing the model, and executing the
result.
Action
Action objects deal with authorizing an
action (business rules), and executing the action. This
is actually an implementation of the Command Pattern. Depending on the outcome of the Action::execute() method, the Action returns a ActionResult object. This determines the routing of the application. For example: if a record is successfully added to the DB, redirect the client to the overview page. Else, show the Add Record page again, and display the error.
ActionResults
ActionResult
objects are returned by an Action object to the Controller. They
implement a simple interface, that just says that every ActionResult
object must have an execute() method. Examples of ActionResult objects
are ActionResult_View (present HTML), ActionResult_Redirect (redirect client) and ActionResult_Download (present a file download).
Views
The Views are simple php files that define the html for a page and take some simple data to combine with it.
The diagram below shows how a request is processed by the system.
The Model
So, after seeing this, I can hear you say:
where's the model? Good one. THis may come as a surprise, but there's
no Model classes in my model. In my opinion, the model should handle
basic validation and sanitation rules of your data To achieve this, I
use a powerful DB engine, written by my friend Arnold Daniels. It's
called QDB and it's available for free on his website.
For each of my database tables I have a .ini configuration file (you can use yaml or
whatever you prefer) that defines datatype and validation rules
(required yes/no, maxlength, validate email etc). So when I need the
model in my controller, I just call the QDB library for a
record/table/recordset and it gives me the data. You don't have to
bother with mysql statements or whatever, it's all taken care of under
the hood. So basically, the Controller is ignorant of the type of
database you're running on. Check the code examples for the simplicity
of QDB.
Now, let's take a look at a simple example.
Example
We'll start with a simple example of a MVC
application based on this architecture. Let's say our application holds
a table with Employees, and we want to update a record (with id=3) in this table. I
use the mod_rewrite in Apache for clean urls. The actual URL for this
action would be http://mywebsite.com/index.php?__controller=Employee&__action=update&id=3 but this is rewritten to http://mywebsite.com/Employee/update?id=3.
# initialize the FrontOffice
$fo = FrontOffice::factory('URL');
# process the request
$fo->dispatch();
This could be all the PHP code in the webroot of your application. As a matter of fact, this is my index.php (safe some initialization code and user authentication, like include 'config.inc';). Looks like magic, doesn't it?
So what happens?
First,
we initialize the FrontOffice. I use a factory method to do it, and
this returns a FrontOffice_URL object. Here's the constructor of the FrontOffice_URL class:
class FrontOffice_URL extends FrontOffice {
public $output = "Html";
/**
* Initialize the FrontOffice - Read from URL the controller and the action
*
* @return FrontOffice object
*/
public function __construct() {
$this->_requestVars = new Data();
# capture get
if ($_GET['__controller']) $this->_controller = urldecode($_GET['__controller']);
if ($_GET['__action']) $this->_action = urldecode($_GET['__action']);
foreach ($_GET as $k=>$v) {
$k = urldecode($k);
$this->_requestVars->{$k} = urldecode($v);
}
# capture post
foreach ($_POST as $k=>$v) $this->_requestVars->{$k} = $v;
}
}
Quite simple, right? The controller and the action are distilled from the URL, and the rest of the request vars are stored in a generic Data object, which is just a void class (in PHP you can set properties on the fly). Note that the output format is set to "Html". In the future, you might want your application to respond in an XML format. Depending on the $output setting, you can include the proper views. At the end of the article this is shown in the directory structure.
So then we go to the second statement:
# load controller
$cclass = self::getControllerClass($this->_controller);
if (!class_exists($cclass)) throw new Controller_Unknown_Exception("Unknown Controller object `".$this->_controller."` requested");
$controller = new $cclass($this);
# controller must extend Controller class
if (!($controller instanceof Controller)) throw new Controller_Illegal_Exception("Controller `$cclass` is not a valid Controller object");
# execute the action
return $controller->execute($this->_action, $this->_requestVars);
Now, we've moved the request from the FrontOffice to a Controller object. Aren't you excited?! From here, we're actually gonna do something :)
The controller in this case is the Employee controller. We'll focus on the execute() method that's being called. As I said, it performs a simple mapping from action names to action classes. I prefer not to do this straight from the URL for several reasons. The execute() method looks up and loads the class for the requested Action, and then call the Action::execute() method. This method in turn returns an ActionResult object. The ActionResult::execute() does whatever the action requires (present a view, offer a file download, redirect the client etc).
class Controller_Employee extends Controller {
public function execute($action, $data)
{
$data->mode = $action;
switch (strtolower($action))
{
case "list":
$action = new Action_Overview('Employee', $data);
break;
case "update":
case "show":
case "add":
$action = new Action_Detail('Employee', $data);
break;
case "delete":
$action = new Action_Delete('Employee', $data);
break;
default :
$action = new Action_Overview('Employee', $data);
break;
}
$result = $action->execute();
if (!$result instanceof ActionResult) throw new ActionResult_Illegal_Exception('$result is not a valid ActionResult');
return $result->execute();
}
}
That's the code for the execute() method.
In the constructor of Action_Detail we have:
public function __construct($table, $data)
{
if (!$data->id && $data->mode !== 'add') throw new Action_Arguments_Exception('Param id required, not given');
$this->data = $data;
$this->record = QDB::i()->table($table)->load($data->id);
if (!$this->record) throw new Action_Exception('Record not found');
}
You might understand my appreciation for the QDB library after seeing this. It's totally void of MySQL statements and has a very intuitive API.
Back to the Controller_Employee::execute(). Two things happen inside of $result = $action->execute().
- The Action's auth() method is called, which would check stuff like: if the current user is not the employee's boss, the user can't change the employee record. In this example, I won't check for anything, so I can just use the generic Action_Detail object (it's auth() method always returns true). Otherwise you would create an object Action_Detail_Employee_Update extends Action_Detail and override the auth() method.
- If the Action::auth() returns true, we can go on executing the action.
Now that we're authorized to update this employee, we want to get to
the part where we create a form. For this I use a proprietary form library that
compares to PEAR's HTML_QuickForm (but is actually better :) ). So just
for argument's sake, don't bother with trying to understand the $form
object. The code for actually performing the action looks something
like this:
In Action_Detail::execute()
$form = HTML_Form::createFromData('form', $this->record);
# if the form is submitted and validated, redirect to overview
if ($form->isSubmitted() && $form->validate()) {
$this->record->save(); // record updaten
return new ActionResult_Redirect('/'.FrontOffice::currentController().'/list');
}
# create form
if ($this->data->mode === 'show')
return new ActionResult_View('View/Show.php', $this->record);
else
return new ActionResult_View('View/Update.php', $this->record);
The ActionResult is returned to the Controller, where we saw the return $result->execute(); code. In this case, we get the ActionResult_View object returned to us, with a view and some data to present in the view.
Almost there, just the ActionResult_View::execute() to wrap it up.
function execute()
{
# Make $data available as a global in the View
$data = $this->data;
# determine output format (HTML/XML etc)
$type = ucfirst(strtolower(FrontOffice::i()->output));
# locate View file
$view = preg_replace('/View/', 'View/'.$type, $this->view);
try {
$success = @include($view);
if (!$success) throw new View_Exception("View does not exist");
} catch (View_Exception $e) {
print $e->getMessage();
}
}
The View file that's included here could look something like this:
# initialize stuff like menu/layout etc
initLayout();
if ($data && $data instanceof QDB_Record)
{
$form = HTML_Form::createFromData('form', $data);
$form->validate();
$form->display();
}
And that's it. We include the PHP file and it outputs some HTML to the client.
To close, here's the directory structure I use:
Action
\Delete.php
\Detail.php
\Overview.php
\Static.php
ActionResult
\Download.php
\NotAllowed.php
\Redirect.php
\View.php
MyApplication
\Action
\Employee
\Update.php
\Controller
\Employee.php
\...
\View
\...
View
\Html
\Overview.php
\Start.php
\...
\Xml
\Overview.php
\Start.php
\...
Action.php
ActionResult.php
Controller.php
Data.php
FrontOffice.php
That's it. We're currently using this architecture in a big application and it's proving to be very flexible and easy to use. It may take a little time to get used to the structure, but the high reusability and small pieces of code really pay off in the long run.
Note
that I've simplified some stuff, and I haven't gone into detail about
some of the interfaces/abstract classes I use. I intended to get my
idea across, and look forward to any questions/suggestions and comments.
Opinions expressed by DZone contributors are their own.
Comments