Rolling Your Own MVC: The View

Welcome to the third part of my series on MVC framework development. In the previous two articles, I discussed the general feature set that our framework will have and I also gave an overview of the page load scenario. In today’s episode I am going to switch gears and discuss a single component of the framework in detail: the view.

Using the view as a starting point may seem odd at first considering the view-related actions are some of the last steps in the page load scenario, but since our views don’t have any external dependencies, unit tests are very easy to write and so is the accompanying code.

Views in an MVC are responsible for everything presentational. This means that if it gets sent to the browser, it is the view’s job to handle it. Views must also also incredibly versatile. For example: if a client makes a request and asks for JSON instead of the default HTML, the framework should be able to accommodate this request. Likewise, if the client prefers to consume XML data, it should be able to handle that as well. Furthermore, views shouldn’t be confined to just a web-based output. There’s no reason why we couldn’t use the majority of our code to build a command-line application.

The code presented in this article will cover views that emit raw (serialized) PHP, JSON, XML, and HTML. We’ll start by looking at the functions which make up the core API to be shared by all view classes. Both an interface and an abstract class are provided for an example implementation. Following that, examples of concrete views will be demonstrated to show the flexibility of this method of handling views.

How Views Work

The easiest way to understand views is to think of them as classes whose sole purpose is to render data. A view’s core API consists of only a few simple methods which allow for data to be manipulated and also a method which renders it out to the client.

During the execution stage of the page load scenario, the controller will populate the view’s internal data store with data from the model. Having its own data independent from that of the model’s allows it to be restructured, modified, escaped, etc. without destroying the original data.

The View Interface

The code that follows defines an interface which is to be implemented by all view classes. Any components that interact with view classes will expect these methods.

interface Panda_View_Interface
{
    public function getVar($name);
    public function setVar($name, $value);
    public function unsetVar($name);
    public function getData();
    public function setData(array $data);
    public function setEchoOutput($bool);
    public function render();
}

The getVar, setVar, unsetVar, getData and setData methods all deal with modifying the view’s internal data. setEchoOutput should determine whether or not the view will be sent directly to the client, or just returned as a string. render, of course, should do the processing and rendering of the data.

The Base View Class

The following abstract class defines code for most of the methods of the interface. All concrete view classes that inherit this class will need to define the render method. Since only one method needs to be defined (generally speaking), view classes are generally short, simple, and very easy to extend or to build new ones with minimal effort.

abstract class Panda_View_Abstract
implements Panda_View_Interface
{
    protected $data = array();
    protected $echoOutput = true;

    public function getVar($name)
    {
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        else {
            return null;
        }
    }

    public function setVar($name, $value)
    {
        return $this->data[$name] = $value;
    }

    public function unsetVar($name)
    {
        if (array_key_exists($name, $this->data)) {
            unset($this->data[$name]);
        }
    }

    public function getData()
    {
        return $this->data;
    }

    public function setData(array $data)
    {
        $this->data = $data;
    }

    public function setEchoOutput($bool)
    {
        $this->echoOutput = (bool)$bool;
    }

    protected function output($output)
    {
        if ($this->echoOutput) {
            echo $output;
        }

        return $output;
    }
}

The code above should pretty much speak for itself. The only new method added to the API was the output method, which does the actual displaying/returning of data. While defining render methods, remember to return your data through this method in order to always insure it gets rendered properly.

The Serialized PHP View

Serialized PHP is very handy for PHP clients which consume PHP services. The emitted data can quickly be imported by the client much like JSON is to JavaScript (except without the need for eval()). Serialized PHP is also popular amongst larger PHP driven organizations such as Digg, Flickr and Yahoo, each of whom use it as an output type for their web service APIs.

This is how easy it is to implement into our view library:

class Panda_View_PHP
extends Panda_View_Abstract
{
    public function render()
    {
        return $this->output(serialize($this->data));
    }
}

The JSON View

JSON has recently become the preferred data-interchange format for the web. It is most commonly consumed by JavaScript applications, however libraries exist for nearly every modern language in wide use today.

As of PHP 5.2, the json extension has been enabled by default in all installations. Much like the View_PHP class above, no processing of the data needs to take place before the render — only a single function needs to be wrapped.

class Panda_View_JSON
extends Panda_View_Abstract
{
    public function render()
    {
        return $this->output(json_encode($this->data));
    }
}

I suppose that it is worth mentioning that this method could be considered harmful for the purists in the audience (that would be all of us, right?). According to the JSON specification, JSON is built on top of two data structures: name and value pairs and an ordered list of values.

In other words, JSON emitting functions should forcefully reject any input which is not an object or an array. PHP’s native set of functions will happily accept any type of data and send it out without warning.

The XML View

Generating a basic XML document from the view data is considerably more complex that the aforementioned methods. XML documents have the potential to be tremendously complex. The code that follows takes the easy way out and completely avoids the use of attribute nodes and makes the bold assumption that only nested nodes are required to display your data. Think of this more of a proof of concept than an ideal implementation. That said, for simple data structures this should work just fine.

When therender method is called a DOMDocument is created and a loop is begun over the view’s data via the recursive method getNodes. Whenever a compound value is detected, getNodes is called again as necessary.

The name of the root node defaults to “data” and the output is assumed to be formatted. These settings may be changed by the setRootNodeName method. Also, output may be formatted or minified by passing a boolean value to the setFormatOutput method.

class Panda_View_XML
extends Panda_View_Abstract
{
    protected $document;
    protected $rootNode = 'data';
    protected $formatOutput = true;

    public function setRootNodeName($name)
    {
        $name = (string) $name;

        if (!empty($name)) {
            $this->rootNode = $name;
        }
    }

    public function setFormatOutput($bool)
    {
        $this->formatOutput = (bool) $bool;
    }

    public function render()
    {
        $this->document = new DOMDocument;
        $this->document->formatOutput = $this->formatOutput;

        $root  = $this->document->createElement($this->rootNode);
        $this->getNodes($this->data, $root);

        $this->document->appendChild($root);
        return $this->output($this->document->saveXML());
    }

    protected function getNodes($struct, DOMNode $parentNode)
    {
        if ($this->isIteratable($struct)) {
            foreach ($struct as $key => $value) {
                /* Numbers don't make good node names, use the parent's */
                if (is_numeric($key)) {
                    $key = $parentNode->nodeName;
                }

                $childNode = $this->document->createElement($key);
                $this->getNodes($value, $childNode);
                $parentNode->appendChild($childNode);
            }
        }
        else {
            $nodeValue = $this->document->createTextNode((string)$struct);
            $parentNode->appendChild($nodeValue);
        }
    }

    protected function isIteratable($struct)
    {
        return is_object($struct) || is_array($struct) || $struct instanceof Iterator;
    }
}

The HTML View

The most common output format will be HTML. My first reaction while coding the HTML view was to extend View_XML, but I quickly trashed the idea. By their very nature, HTML documents implement heavy reuse of UI components (headers, navigations, footers, etc). Of course you don’t want to store that kind of data inside the view’s data so a template system needs to be implemented.

In the old days template systems consisted of header and footer includes which wrapped a bunch of code. Frankly, I always hated the idea of managing two documents when one would do just fine. The solution I came up with was to have two types of HTML documents: templates and partials.

Templates are documents which are designed to provide HTML for the areas of a web UI which don’t change very often. In other words, this is a single-document solution to the the header and footer includes mentioned before.

Partials are page-specific HTML documents which are injected into templates at render time. The view knows where to put views by a target argument which is just an XPath location.

class Panda_View_HTML
extends Panda_View_Abstract
{
    protected $document;
    protected $template;
    protected $partials;

    public function setTemplate($template)
    {
        if (is_file($template)) {
            $this->template = $template;
        }
    }

    public function setPartial($partial, $target)
    {
        if (is_file($partial) && !empty($target)) {
            $this->partials[$partial] = $target;
        }
    }

    public function unsetPartial($partial)
    {
        if (array_key_exists($partial, $this->partials)) {
            unset($this->partials[$partial]);
        }
    }

    protected function parse($file)
    {
        $out = '';

        if (is_file($file)) {
            ob_start();

            include $file;
            $out = ob_get_contents();

            ob_end_clean();
        }

        return $out;
    }

    protected function load($source, $target)
    {
        $xpath = new DOMXPath($this->document);
        $items = $xpath->query($target);

        if ($items->length > 0) {
            $source   = $this->parse($source);
            $fragment = $this->document->createDocumentFragment();

            $fragment->appendXML($source);
            $items->item(0)->appendChild($fragment);
        }
    }

    public function render()
    {
        if (empty($this->template) || !is_array($this->partials)) {
            throw new Exception('Unable to render: incomplete view configuration.');
        }

        $templateContents = $this->parse($this->template);

        if (!empty($templateContents)) {
            $this->document = new DOMDocument;
            $this->document->loadHTML($templateContents);

            foreach ($this->partials as $source => $target) {
                $this->load($source, $target);
            }

            return $this->output($this->document->saveHTML());
        }
    }
}

Thoughts for Improvement

The motivation behind this post was to demonstrate the potential of highly abstracted views. It’s important to note that this process can and will be improved upon in future iterations. Here are a few ideas that came to mind as I was writing this post:

  • More complex views (such as View_XML and View_HTML) need to be dynamically configured at runtime. Since each class has a proprietary set of properties and helper methods, it would be a good idea to have a simple configure method which accepts an array of configuration directives.
  • JSON output in View_JSON should be properly handled. Because it utilizes PHP’s native json_encode, it is inherently broken. JavaScript applications have the potential to be very brittle at times so data integrity should come first.
  • The View_XML class needs to be more precise. A SimpleXML-like structure would be much better suited for generating more complex XML documents.
  • I feel dirty every time I look at code that uses output buffering. I still don’t like the idea that View_HTML uses it to parse partials before they are injected into the document. A much more elegant solution would be to emit raw XML data and to have the client transform it into HTML via XSLT. The downside to this method would be that it assumes the client supports XSLT, which just doesn’t fly. You could transform the XML on the server-side, but that could be overkill for most implementations.

Conclusion

So there you have it. One interface, one abstract class and four distinct output formats coming in at just over 200 lines of code.

You can download the code above via SVN by checking out the following URI:
http://panda-php.googlecode.com/svn/trunk/Panda/View/

As always I welcome your comments on this article.

Other Posts in this Series


This entry was posted on Monday, April 28th, 2008 at 12:31 am and is filed under PHP. You can leave a response, or trackback from your own site.

2 Responses to Rolling Your Own MVC: The View

Michael Girouard’s Blog: Rolling Your Own MVC: The View | Development Blog With Code Updates : Developercast.com:

On April 28th, 2008 at 9:46 am #

[…] is back with part three of his series stepping you through the creation of your own MVC framework (Part 1 and Part 2) with […]

New Package: Views :: Panda PHP Components:

On April 30th, 2008 at 9:05 pm #

[…] I have committed many interesting view components into the repository. I’ve written an overview of the package on my personal blog which discusses each part […]

What do you have to say?

Site Stuff

Pages

Projects

Archives

Categories