PHP
Article
By Zack Wallace

Transphporm – a Different Kind of Template Engine

By Zack Wallace

If there is one thing the world needs, it’s definitely another PHP template engine! But wait, this one is different!

Many PHP template engines (Smarty, Blade, Twig…) are little more than abstracted PHP code, making it easier to write loops, if/then/else blocks, and display variables with less verbosity than vanilla PHP. These traditional template engines require the designer to have an understanding of special syntax. The template engine must also be in place and operational in order to parse data and thus completely render the final design. Nothing wrong with this.

Magazine, flyer, brochure, cover layout design print template

Transphporm follows a different approach that is generally known as “template animation”. What this means is that the web designer creates the entire HTML/CSS/JS page including placeholder content such as “Lorem Ipsum”, then the template engine comes along and replaces parts of the DOM with new data before the final render.

The benefit of template animation is that the designer does not have to know the backend code, template language, or any particular special syntax. They can use whatever tools they want to create 100% functional HTML pages.

The server-side software does not even need to function while the designer works. This means the designer doesn’t have to wait for the backend to deliver data for them to work with.

Basically, the designer can work with this:

<h1>A Good Title Here</h1>
<p>A subtitle</p>

<p>Some awesome text for clients to see.</p>

Instead of this:

<h1>{$futuretitle|upper}</h1>
<p>{$futuresubtitle|capitalize:true|default:'Default Text'}</p>

<p>{$futureawesometext}</p>

The placeholder content in the first example will be replaced by Transphporm. In the second example, the template engine must be functional and parsing the templates in order to see the final design as the designer envisions it.

Notice how the separation of concerns reaches nearly 100%. There is no special syntax or code that the designer needs to know. On the server-side, Transphporm does not need to have any HTML tags hard-coded. You don’t have logic and code on the front end, and you don’t have presentation and nodes being hard-coded in the backend.

Note: The backend may still produce reasonable HTML tags such as what is created through a WYSIWYG editor. i.e img, a, p, strong, em, etc., but never block level when done right.

Let’s see how this works. If you’d like to follow along, please prepare a new environment.

Installing Transphporm

In the public folder of our project, we’ll install the package via Composer.

composer require level-2/transphporm:dev-master

Then, we create a new index.php file with the content:

<?php

require 'vendor/autoload.php';

Note: Frequently run composer update as Transphporm is an active project. It updated a few times during the writing of this article.

Create Your Pages

Transphporm requires no front-end syntax or special code at the design level. We will use a CSS-like selection language to find and replace content before rendering output to the browser.

By the nature of template animation I can theoretically grab any ready-to-go template off the web so long as it is XHTML valid. This means <meta charset='utf-8' /> and not <meta charset='utf-8'> as is with current HTML5. Modern practice is to not use self-closing tags, but to be XML-valid, we must. Note that your final render can be HTML5, but the templates you create for Transphporm must be XHTML. This is just the nature of using PHP’s XML methods.

A Transphporm object requires two values. One is valid XHTML markup which represents the designer’s template. It could be an entire page, or just snippets of XHTML to be used in the greater theme. The other is TSS (Transphporm Style Sheet) directives which contain the replacement logic.

Reference the github page for full documentation as I can be only so verbose within the length of this article.

Add this to the index.php file:

$page = 'home.xml';

$tss = 'home.tss';

$template = new \Transphporm\Builder($page, $tss);

echo $template->output()->body;

Now create the home.xml file with this basic XHTML template:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        
        <title>HTML5 TEMPLATE</title>

        <style>
            body { padding: 20px; font-size:1em; }
            header nav { background-color:#ACEAD0; padding:15px 10px; }
            header nav ul { padding:0; }
            header nav li { display:inline; list-style:none; margin-right:15px; }
            header nav a { text-decoration:none; }
            article header h1 { margin-bottom:4px; color:#34769E; font-size:2em; }
            article header h2 { margin:0; font-size: .8em; color:#555; }
            article p { font-family:Arial, sans; color:#444; line-height:1.4em; }
            footer { background-color: #444; padding:10px; }
            footer p { color:#E2E2E2; }
        </style>

    </head>
  
    <body>
        <header>
            <nav>
                <ul>
                    <li><a href="#">Home</a></li>
                    <li><a href="#">The Goods</a></li>
                    <li><a href="#">Favorites</a></li>
                    <li><a href="#">Media</a></li>
                    <li><a href="#">Contact</a></li>
                </ul>
            </nav>
        </header>
        
        <main>
            <article>
                <header>
                    <h1>Page Title</h1>
                    <h2>By Jehoshaphat on Jan 1, 2015</h2>
                </header>
                
                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut voluptas deserunt in quam itaque consequatur at recusandae, veritatis placeat porro cum magni eos iure vero, obcaecati est molestiae quos enim.</p>

                <p data-p="yes">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Commodi sed asperiores cum explicabo praesentium, mollitia possimus nam. Aperiam autem doloribus hic. Ex quae quia fugiat, fugit eaque quam adipisci nemo.</p>
                
                <ul>
                  <li>A list item 1</li>
                </ul>
            </article>
        </main>
            
        <footer>
            <p>footer stuff</p>
        </footer>

    </body>
</html>

Now create a home.tss file with this:

article h1 {content: "My New Page Title"}

The TSS directive is “selecting” the h1 using CSS-like syntax. “Find an h1 that is a child of an article tag”. Note that specificity matters. If you had many article tags or many h1 tags, they would all be selected and changed!

This is especially true for repeating nodes like the <li>. If there were more than one in our template, each individual list item would be replaced by the entire repeating dataset!

If your index.php, home.xml, and home.tss files have been created correctly, you should be able to open the website and see the template. The template has <h1>Page Title</h1> but you should actually see “My New Page Title” when rendered in the browser.

Transphporm supports common CSS syntax including:

  • .classname
  • tagname.classname
  • direct > descendant
  • #id
  • element[attribute="value"] OR simply element[attribute]

Methods not supported include ~ and +.

Beyond these selectors, Transphporm supports pseudo elements including:

  • element:before
  • element:after
  • element:nth-child(n)
  • element:even
  • element:odd

You can chain selectors together just like in CSS.

article header > p {content: "First paragraph after the article header section"}

This is the easy stuff and is hopefully straightforward.

At this point, you’re surely thinking “but I don’t want to hard code text in the TSS directive!”

Well, that’s where the fun begins.

Handling Data

Let’s pretend this project is a bit more complicated. You are not likely to have the new <h1> content hard-coded in a TSS file! And you’ll likely have more than one piece of content to update.

I’m going to create an object called $data and assign some values to it.

Update index.php to this:

require 'vendor/autoload.php';

// Some fancy routing to setup the page.
$page = 'home.xml';
$tss  = 'home.tss';

// Some fancy controller/model action to get data.
$data = new stdClass;

$data->pagetitle = "Awesome Page";
$data->title     = "Article Title";
$data->subtitle  = "By Zack";

$template = new \Transphporm\Builder($page, $tss);

echo $template->output($data)->body;

Now we have $data as an object. We also added $data to the output() method.

The home.tss file can now be updated to this:

title {content: data(pagetitle)}
article h1 {content: data(title)}
article h1 > h2 {content: data(subtitle)}

The attribute we use called content accepts a value called data(). This is similar to how in CSS you might supply rgba() as a value for color.

At this point we can replace any content by utilizing the CSS syntax. Notice how we even replaced content within the <head> section.

--ADVERTISEMENT--

DOM Concerns

At this point, you may be thinking that the designer and programmer don’t have to talk to each other; but on some level they do. The designer must use proper XHTML or the engine will crash (error handling may be improved over time).

You might be wondering what happens if the designer changes the DOM and thus breaks a TSS selection. This can happen, especially if you rely more on the DOM tree than on well-thought-out IDs and classes. I would rather use id="posttitle" than rely on main article header > h1 which may not always be set in stone.

Multiple Partials

We can also utilize more than one XML file to assemble the final template.

Here is an example TSS file:

@import 'another.tss';

aside {content: template("sidebar.xml")}

We can use @import to include other TSS files. We can also use template() to assign partial xml to the selected node as seen above. Any TSS directives that come after this line can affect the included xml file.

This way, you can carefully craft the importing of xml partials and TSS directives to assemble the final page. Just like CSS, everything compiles top-down.

Not only this, but you can replace content with partial content of the included template. In the above example, I’m including the entire sidebar.xml file. But what if I only want the <aside> within that file? Just do this:

aside {content: template("sidebar.xml", "#extra aside")}

This will only pull the <aside> out from under the element with ID “extra”. The benefit of this is that the designer can still work with complete templates if they want to, even though you may only extract certain elements. It also means that, just as you can combine images together into a single file and use CSS to display them as needed, you can combine template elements in a single file and just extract the parts you need.

Advanced Directives

One concern you might have is that if the designer breaks up the design into many partials, doesn’t that defeat the purpose of template animation because now you need Transphporm to put it back together? Kind of.

This is because the “master” template still contains the designer’s complete mockup. Nothing changes there. The “real” final markup will be within the partials. This makes the designer’s job only slightly more complex as they will have to edit the main design visually, then copy and change things within the partials for production. If they update the primary page, they may have to update partials too.

If you refer back to our basic HTML template, partials could easily be used for the header, the menu, the primary content area, the footer, etc. But placing these in partials doesn’t affect the designer being able to leave the primary base template fully intact and visually complete. The partials will simply replace the placeholder markup later.

And as we just saw, the partials themselves can be part of a larger complex or complete page.

Repeating Data

Transphporm can handle repeat data through the use of a repeat() attribute.

Our template has an unordered list. We can fill it by using $data to pass an array of objects. Let’s add an array of fruit objects to index.php.

$fruits = []; // Master array of all fruits

$fruit = new stdClass;
$fruit->name = 'Apple';

$fruits[] = $fruit;

$fruit = new stdClass;
$fruit->name = 'Pear';

$fruits[] = $fruit;

$data->fruits = $fruits;

Now in the TSS file:

article ul li {repeat: data(fruits); content: iteration(name)}

If you’ve added the data to index.php and updated the TSS file, the unordered list should now have two <li> nodes listing “Apple” and “Pear”.

Before:

<ul>
    <li>A list item 1</li>
</ul>

After:

<ul>
    <li>Apple</li>
    <li>Pear</li>
</ul>

Notice how we used two attributes “repeat” and “content” separated by a semicolon. Just like setting height, width, color and border in CSS on a single node, you can apply multiple transforms using a single selection with Transphporm.

We passed data(fruits) to the repeat function. This will repeat the selected node (the <li>) including its closing tag. Then, we use iteration() to tell it what data within each fruit object to display.

Note: beside data() and iteration(), there are other transforms available including format which accepts the values “uppercase”, “lowercase”, “titlecase”, “date”, and can deal with numbers as well.

Attributes

Transphporm can do some fun stuff with attributes.

Let’s say we wanted to hide the <li> if it equals “Apple”. To do this, we can inspect the current iteration and select based on that.

article ul li:iteration[name='Apple'] {display: none;}

Notice how it works like a psuedo element by using a colon after the <li> and in square brackets matching a name=value pair. Notice also that Transphporm can just as easily hide elements with the familiar CSS directive display:none (though with Transphporm, the elements are fully removed from the DOM, not just hidden).

In our template, the second paragraph has data-p="yes". Let’s hide it based on that value.

article p[data-p="yes"] {display:none}

Notice how there is no colon after p here like before when using iteration, which is a Transphporm function. We are selecting with normal CSS attribute syntax which doesn’t use a colon.

You can also set the content of the attributes themselves. The example from the docs uses a “mailto” link like so:

a:attr(href) {content: "mailto:", iteration(email)}

Again, attr is a Transphporm function so the colon is used. Also notice “(href)” is a direct selection of the attribute itself, no value is used.

Something cool happens here – content is set to “mailto:” but then we use a comma and the iteration’s value. Transphporm will concatenate multiple values when using a comma, e.g.:

p {content: "This ", "is ", "really ", "long"}

So with the previous “mailto” example, a proper attribute href="mailto:someemail@example.com" will be assembled.

Headers

Transphporm does not send headers. You will have to use normal PHP functions for that.

The output() method returns an object with two attributes. One is body which contains the rendered HTML, the other is headers which is an array of headers.

The way to set a header is to select the <html> element and use the built-in pseudo element :header to set it.

html:header[location] {content: "/another-url"}

To make use of headers set this way, you’ll have to read the array returned by Transphporm, take those values and set them with PHP’s header() function. Instructions for how to do this are in the docs.

Conditional Changes

It is possible to change elements based on logic in other parts of the application. Let’s say your program has a function like this one:

class Security {
  public function LoggedIn() {
    // Some logic
    return false;
  }
}

$data = new Security; // Be sure to pass $data to the output() method as usual

Now you can read this value and determine the appropriate action:

#welcome:data[LoggedIn=""] {display:none}

This could be quite handy!

It should be noted that, with template animation, we usually start with a complete template with all features, then strip away parts that don’t belong. We would build the template with “logged in” features, then remove them from the DOM if not needed.

This is described as a “top down” approach, as opposed to “bottom up” where only minimal markup is present, and we add stuff as needed.

Both approaches work with Transphporm.

Benefits of Decoupled Display and Logic

An advantage of separating display logic from markup is that they become decoupled entirely. This means that any given XML doesn’t have to belong to any given TSS. In fact, some XML could be used with different TSS and some TSS could be used with any other XML.

One example of this is using TSS to repopulate forms. A single TSS could be used to repopulate any given form template. It would look like this:

form textarea {content: data(attr(name))}
form input[type="text"]:attr(value) {content: data(attr(name))}

The function attr(name) reads the “name” attribute of the selected element. If the “name” attribute of the textarea element were “comments”, then it compiles to:

form textarea {content: data(comments)}

data(comments) here is referencing the passed-in $data object we’ve already used before, but it would work just as well if $_POST itself were passed instead.

The second line in the above TSS is selecting all form input elements with a type of “text”. It is then specifically selecting the value attribute itself which is where you would put content when populating a form. The data is, again, coming from data(xyz) where “xyz” is the name attribute of the selected element. In turn, the name attribute will match the data coming in from $_POST or a $data object.

The end result is that anywhere you need to repopulate a form, you could simply include one TSS file like so:

import 'form-populate.tss';

As an example of passing $_POST you might do this in your PHP:

$data->title = "Article Title";
$data->formData = $_POST;

Once $_POST has been passed in through $data, a binding feature can specifically link formData to all the TSS used under that form element.

form {bind: data(formData)}
/* Now that formData is bound to form, it can be accessed directly */
form input:attr(value) {content: data(attr(name))}

Binding just means I can call data(key) rather than data(formData[key]) because the context of data() was changed to reference formData directly.

Language Independence

It should be noted that decoupling means any language can be used to process the TSS files without any change to the templates. The author has built a sister compiler in Javascript that is 100% compatible as far as TSS and XML templates go. Find it on Github at Transjsform.

Challenges

Using template animation takes care of both the designer and the coder. The designer can create their templates and partials without any real concern for the backend or template engine. Only valid XHTML is needed. The programmer, on the other hand, only needs the ability to easily find the DOM elements which need their content replaced.

The programmer and designer will, however, need a planned-out file structure for where to place major and minor templates, partials and so forth.

The person who is not quite taken care of yet is the editor/writer.

It’s fine to have XHTML templates. It’s fine to have TSS logic somewhere. The data can come from a controller and be assembled into a single $data object. But where do the writers write?

Obviously, some form of editorial interface is going to be required to connect a writer to a particular piece of content/TSS to update it. There is nothing contained in the template itself to inform the backend which nodes require content editing and which do not.

Some form of meta-data will be required to say “this paragraph needs custom data, but this one does not”. The programmer also needs to know which sections will be replaced by a partial. There may even be multiple versions of a partial depending on some other application logic.

Because of this, we can’t really claim that there is a 100% separation between designers and coders, as they still need some level of congruity regarding the overall structure of things.

Make the Editor Learn TSS?

One of the reasons for template animation is to prevent a designer from having to learn a template language or syntax. This is a pointless goal if you are just making a writer learn TSS syntax instead! It would likely be much easier for a designer to learn a template syntax than to teach a writer CSS syntax.

If the designer is also the content editor, then they have to learn TSS anyway, so why not just learn an existing mature template language? If the editor doesn’t know CSS syntax, nothing will be intuitive for them.

Parts of TSS are for assembling the page, other parts for assigning copy. Editors further only need access to some of these transforms. Some transforms may be structural in nature while others content oriented.

A true separation of concerns here must include the editors as well as the programmer and designer. No existing CMS is going to be able to adopt Transphporm as-is. A complicated template could potentially contain hundreds of transformations with lots of various logic paths. The content editors need a WYSIWYG.

All together, this presents an interesting challenge for making Transphporm usable for everyone.

It should be noted that the author is currently working on a parser where an editor could submit Markdown as the content and Transphporm would parse it into HTML for the output. This feature should be added very soon and takes us a step closer to an editing interface.

Caching

Basic caching is being implemented as written about here. Without a cache, PHP currently has to traverse the XHTML and parse TSS for every page load. In its current iteration, this could create a performance issue when using Transphporm.

Conclusion

Using CSS-like syntax to select DOM elements is a very fun concept. Designers may find relief from having to learn a new syntax, and they can mock up 100% complete designs with no worry about the backend. This also means one should be able to download any XHTML, ready-made templates and get started relatively quickly with minimal DOM changes.

It’ll be interesting to see how this project matures. The first CMS interface to be created to manage TSS transforms and content editing will be particularly interesting.

If you don’t like the way regular template engines work, maybe Transphporm is more up your alley? Take a look and let us know!

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.