Building a site on Drupal using MVC
Related articles: Extending MVC on Drupal and Drupal – nice ideas and bad coding
If you ought to build a new site and don’t have time to develop a CMS, you’ll likely take an open source solution. And likely it will be Joomla or Drupal. They seem as most popular. Actually they are similar in many ways. Both provide basic content management frameworks, extensible through plug-ins and customizable via themes. Both focused on block-designed sites which can be often built without any programming at all, just using downloaded theme and configurable set of extensions. So it’s personal what to choose. I prefer Drupal. Its component model seems clearer to me. The documentation is much more explicit then Joomla’s and gallery of ready-to-use modules is really impressive.
Now let’s see if Drupal allows to achieve all the goals we have so far. There are no doubts it will be good for generic stuff like pages of different layouts, blog/storyboards, image galleries. But what about customization of administrative UI? Custom administrative modules? Positive! And you’re going to enjoy their approach as soon as you’ve got the overall idea behind it.
So, here we are. First of all Drupal is not MVC, but it’s built by other paradigm known as PAC (Presentation Abstract Control). So, it took me for a while to get in. It remotely reminds of Java’s portlets. You build application out of separate blocks, which have own views, controllers and shared API. Another discouraging issue is Drupal source code designed without any OOP. Here is some diffuse explanation by the author, but the fact – you have to deal with plain procedural code, mostly PHP4-designed. Nonetheless it implements widely the hook technique , something like an instantiation of Observer design pattern. I, personally, find it amazing. Drupal has system functions and default page element templates. Every plugged-in module has own functions and may have template. Now check it out, module can override system or other modules’ functions and template gracefully. And further, system and module functions and templates can be overridden within theme.
As I’ve already said, Drupal is primarily adjusted for not site building, but theming. When you install a new module, you can find you front-end appearance changed automatically. Drupal provides set of view variables in template scope. But the problem is page element variables contain ready HTML, which you can only display somewhere in page layout. That’s the way PAC works, it finds and theming all the elements by itself. I’m a MVC guy and used to have just raw data within templates and helpers to decorate it. And it’s quite available under Drupal as well. Let’s start from beginning and advance step by step. You’ll see everything on examples.
Setting up theme
Create in /sites/default/themes a folder of the name of your theme. Let’s say, mysite. Now add some files there:
- template.php – theme controller
- page.tpl.php - default page template
- mysite.info - theme info
- screenshot.png –theme screenshot, which will be displayed in theme list http://mysite.com/admin/build/themes For mysite.info content will be:
name = MySite
description = ...
version = VERSION
core = 6.x
engine = phptemplate
Now we are to create the controller:
<?php
define("VIEWPATH", dirname(__FILE__));
/**
* @param array $vars
*/
function runRouters(%26$vars) {
if (module_exists('path')) {
$alias = drupal_get_path_alias(str_replace('/edit','',$_GET['q']));
if ($alias != $_GET['q']) {
// When it's site page, view template page.tpl.php or page-urlPart.tpl.php
// See http://drupal.org/node/104316
$template_filename = 'page';
foreach (explode('/', $alias) as $path_part) {
$template_filename = $template_filename . '-' . $path_part;
$vars['template_files'][] = $template_filename;
}
}
}
}
/**
* Override or insert PHPTemplate variables into the templates.
* @param array $vars
*/
function phptemplate_preprocess_page(%26$vars) {
runRouters($vars);
}
You see, right before page rendering we can modify any of view variables. Particularly we modify the list of suggested template for the request. Since we use “clean URIs” like http://mysite.com/page1/subpage1 instead of Drupal’s native URIs http://mysite.com/node/12, we have to extend template suggesting mechanism. Now it tries to find page1-subpage1-page.tpl.php for the request. When failed it seeks for subpage1-page.tpl.php or page.tpl.php. We can add here conditional logic, for instance, to match templates against content types.
Switching to MVC
Now let’s bring in some stuff of Zend Framework. I’m going to make it feel like home. First, we need auto-loader. Add into theme controller this code:
include realpath(dirname(__FILE__) . "/../../libraries/Autoloader.php");
new libraries_Autoloader();
And put autoloader at the path: /sites/default/libraries/Autoloader.php
<?php
define("LIB_PATH", realpath(dirname(__FILE__) . "/../"));
class libraries_Autoloader
{
/**
* Register self::autoload() with the spl provided autoload stack
*/
public function __construct()
{
spl_autoload_register(array(__CLASS__, 'autoload'));
}
/**
*
* @param string $class
*/
public static function autoload($class)
{
$fileName = str_replace('_', "/", $class) . '.php';
$file = LIB_PATH . '/' . $fileName;
if (file_exists($file)) {
require $file;
return;
}
}
}
Now we can just call classes by name, not bothering of including their files. So we can create following folder structure:
sites
default
Controller
Model
View
Helper
Just make sure, you follow Classes Naming Conventions of Zend Framework .
At the path /sites/default/View/View.php the following code located:
<?php
class View_View {
private $_vars;
/**
*
* @param array $vars
*/
public function __construct($vars)
{
$this->_vars = %26$vars;
}
/**
* Accesses a helper object from within a script.
*
* @param string $name The helper name.
* @param array $args The parameters for the helper.
* @return string The result of the helper output.
*/
public function __call($name, $args)
{
// Let's assume a view helper requested
$suggestion = 'View_Helper_' . ucfirst($name);
if (class_exists($suggestion)) {
return new $suggestion($this, $args);
}
return null;
}
/**
* Get alias
* @param string $name
*/
public function __get($name)
{
return $this->get($name);
}
/**
* View getter
*
* @param string $name
* @return mixed
*/
public function get($name)
{
if (isset($this->_vars[$name])) {
return $this->_vars[$name];
}
return null;
}
/**
* Set alias
* @param string $name
* @param mixed $value
*/
public function __set($name, $value)
{
return $this->set($name, $value);
}
/**
* View setter
*
* @param string $name
* @param mixed $value
*/
public function set($name, $value)
{
$this->_vars[$name] = $value;
}
}
That’s a simplified implementation of Zend_View abstract class. It imitates Zend_View within template and provides easy access to the view helpers.
Template html
<?
print $view->element ; // displays content of element
print $view->date('2010-10-10')->format("F d, Y"); // instantiates View_Helper_Date and perform format method
?>
Here is an example of view helper:
<?php
/**
* Implements date view helper
*/
class View_Helper_Date {
private $_date;
/**
*
* @param View_View|stdObject $ref
* @param array $args
*/
public function __construct(%26$ref, $args = array())
{
// @param string $args[0] - e.g. 2010-06-23T00:00:00
$this->_date = $args[0];
}
/**
* Emulate PHP date function for rual CCK date format
*
* @param string $format - see http://de2.php.net/manual/en/function.date.php
* @return string htmls
*/
public function format($format)
{
preg_match("/(\d{4})-(\d{2})-(\d{2})/", $this->_date, $matches);
$time = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
return date($format, $time);
}
}
For controllers I would propose following abstract class (/sites/default /Controller/Abstract.php):
<?php
class Controller_Abstract
{
public $view;
public function __construct(%26$vars, $action = "index")
{
$this->view = %26$vars["view"];
$this->$action();
}
}
Remember theme controller template.php? Now we ought to extend it with custom controllers routing. Let’s go the same way as templates routing.
<?php
define("VIEWPATH", dirname(__FILE__));
/**
* Tries to run controllers based on template suggestions
* Template name is converted into controller classname like that:
* page-home -> Controller_PageHome
*
* @param array $vars
*/
function runControllers(%26$vars) {
if ($vars['template_files']) {
foreach ($vars['template_files'] as $controller) {
$className = "Controller_";
$parts = explode("-", $controller);
foreach ($parts as $part){
$className .= ucfirst($part);
}
if (class_exists($className)) {
new $className($vars);
}
}
}
}
/**
* @param array $vars
*/
function runRouters(%26$vars) {
if (module_exists('path')) {
$alias = drupal_get_path_alias(str_replace('/edit','',$_GET['q']));
if ($alias != $_GET['q']) {
// When it's site page, view template page.tpl.php or page-urlPart.tpl.php
// See http://drupal.org/node/104316
$template_filename = 'page';
foreach (explode('/', $alias) as $path_part) {
$template_filename = $template_filename . '-' . $path_part;
$vars['template_files'][] = $template_filename;
}
}
}
}
/**
* Override or insert PHPTemplate variables into the templates.
* @param array $vars
*/
function phptemplate_preprocess_page(%26$vars) {
$vars["view"] = new View_View($vars);
runRouters($vars);
runControllers($vars);
}
It means when we have a request for a page like http://mysite.com/page/subpage, the controller /Controller/PageSubpage.php will be called if it’s found.
Here is an example of controller:
<?php
class Controller_PageHome extends Controller_Abstract
{
/**
* Default action
*/
public function index()
{
$model = new Model_News();
$this->view->stickyNode = $model->getSticky();
$this->view->promotedList = $model->getPromoted();
}
}
Now when Drupal’s PAC looks like customary MVC , we can come back to our page templates. For example, page.tpl.php (/sites/default/themes/mysite/page.tpl.php). We can access data of the elements added by system modules and view variables assigned by our custom controllers via $view namespace.
Template html
<?
print $view->node->title; // displays title of current node
foreach ($view->promotedList as $item) {
print $item->title;
}
?>
Pay your attention that administrative interfaces play under the same rules. You can arrange administrative theme using this MVC-like pattern.
Hooking
As I mentioned already Drupal allows customize core and installed modules behavior thought hooks. We examined phptemplate_preprocess_page hook in template.php file. Now let’s try some other. For example, you are customizing core optional module Book. By copying predefined templates (book-node-export-html.tpl.php, book-navigation.tpl.php, book-all-books-block.tpl.php) from the module folder in a folder within your theme and modifying them you will get a pretty good result. But if you want to customize markup for book menu items, you can do nothing via templates. It uses the decorator common for every menu item. Though you can change it, putting into template.php (theme folder) following:
function mysite_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) {
// Original code of theme_menu_item (includes/ menu.inc), customized by you
}
Another useful one is nodeapi hook. You put it rather not in theme controller, but in custom module:
function modulename_nodeapi(%26$node, $op, $a3 = NULL, $a4 = NULL)
{
// $op - operation insert, update, delete, validate, presave, load, …
}
Whatever happens to a node, the hook will be invoked
Modules
I assume modules are the very benefit everybody likes Drupal so much for. If you want to extend the system somehow just search for ready modules and likely someone has written it already. On the other hand, it will take you for while. Sometimes it seems easier to develop a feature by yourself then find a good implementation of it among thousands of modules. Though some of contributed modules are made really felicitous and got sort of standards ‘sui generis’. Let’s say, Content Construction Kit (CCK) . By default, for a node Drupal provides only title and body fields. CCK allows you enhance content types with fields of various types. You can find ready rich controls for CCK. For example:
- FileField - upload field for CCK
- ImageField - image upload field for CCK
- ImageAPI - image transformation API based on ImageMagic (thumbnals and so on)
- Date - date field for CCK
- Calandar - allows Date picker and event duration fields
Also I would like to mention jQuery Update module that fix the fact Drupal 6 is based on old stripped version of jQuery. A good one is PathAuto Module , which allows auto-generated node URLs (on default it’s node/xxx, but can be aliased automatically as news/xxx or /section/news/some-title-here).
You see Drupal is a very flexible framework for a CMS. It has its lacks though. For instance, default administrative content management UI is confusing and not useful. But let’s think positive, you can always to plug-in and customize it.
Please find continuation of the subject in Extending MVC on Drupal