Royall Spence's Blog

Ditch The Template System

All existing PHP template systems expect the user to break encapsulation, the most fundamental element of object oriented programming. Fortunately, there are other ways to build HTML documents. Let’s look, for example, at the popular templating library Twig. From the documentation:

For convenience's sake foo.bar does the following things on the PHP layer:
  • check if foo is an array and bar a valid element;
  • if not, and if foo is an object, check that bar is a valid property;
  • if not, and if foo is an object, check that bar is a valid method (even if bar is the constructor - use __construct() instead);
  • if not, and if foo is an object, check that getBar is a valid method;
  • if not, and if foo is an object, check that isBar is a valid method;

We notice immediately that it expects the programmer to include either public properties or properties exposed by get methods. Accessing data directly bypasses all of the behaviors we’ve programmed in our objects. We must make a distinction here. Procedural programming is about procedures operating on pieces of data. Object oriented programming, on the other hand, combines data and behavior into a single granular object. Why should our object spit out little pieces of itself for other sections of code to consume? It’s code cannibalism.

Object behaviors are public facts, but their internal details are kept secret. The get method approach breaks both of these facets of encapsulation. Not only do the members become publicly visible, but the operations performed on them occur above our object’s scope. Twig is not at all a bad library, but it does expect its users to employ procedural designs.

One object oriented alternative to this approach is to use printers instead of getters. Rather than passing their data to some other tool to build HTML elements, we can program our objects to represent themselves as HTML elements.

For example, consider a simple Person class:

class Person
{
    private $name;
    private $birthday;
    
    public function __construct(string $name, \DateTime $birthday)
    {
        $this->name = $name;
        $this->birthday = $birthday;
    }
    
    public function toHtml(PersonLayoutInterface $layout): string
    {
        return $layout
            ->withName($this->name)
            ->withBirthday($this->birthday)
            ->render()
        ;
    }
}

That’s a simple description, but it doesn’t do much on its own. Notice something about how this works. Our Person is able to show itself as a string of HTML without doing the work of building the HTML. We’ll need a PersonLayout to do the job.

interface PersonLayoutInterface
{
    public function render(): string;
    public function withName(string $name);
    public function withBirthday(\DateTime $birthday);
}

The interface is designed specifically for this type because we wouldn’t know what options were valid otherwise. In the real world we’d likely have multiple implementations like Form, LongDescription, or Json.

class TableRowPerson implements PersonLayoutInterface
{
    private $name;
    private $birthday;
    
    public function __construct(string $name = null, \DateTime $birthday = null)
    {
        $this->name = $name;
        $this->birthday = $birthday;
    }
    
    public function withName(string $name): TableRowPerson
    {
        return new TableRowPerson($name; $this->birthday);
    }
    
    public function withBirthday(string $birthday): TableRowPerson
    {
        return new TableRowPerson($name; $this->birthday);
    }
    
    public function render(): string
    {
        return (newTableRow())
            ->withChild((newTableCell())
                ->withChild(new Text($this->name))
            )
            ->withChild((newTableCell())
                ->withChild(new Text($this->birthday->format('Y-m-d'))
            )
        )->render();
    }
}

Our Layout shows only one particular display, but we can make as many as we need. Notice that we have passed our member variables from Person directly into another object.

The rule to remember is that we can always pass our members down into another object to get a result, but never return members directly to a higher scope. If we can write classes that follow this rule, we will find ourselves with tidy, consistently encapsulated objects.

This particular Layout returns a string in the form of a <tr> with my HtmlDocument library. There’s probably plenty of ways to accomplish the same thing, but I built the library with this approach in mind. It might be a little bit verbose, but it makes automated testing of discrete layout elements easy. You will rarely see a site with unit tested markup that does not follow such an approach.

We could develop a similar approach that involves passing in a Twig object that receives appropriate variables in the same way as our Layout objects. There wouldn’t be much point, though. The object composition hierarchy and PHP’s built-in text processing functions make Twig’s key features of template inheritance and text manipulation redundant. Hopefully this approach can help us to build smaller and more testable classes.

I think it’s wonderful. What do you think?