Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Another MVC approach

DZone's Guide to

Another MVC approach

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

So, 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.

Model View Controller

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().

  1. 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.
  2. 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.

 

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}