Welcome to Admin Junkies, Guest — join our community!

Register or log in to explore all our content and services for free on Admin Junkies.

PHP Template Engines - Deep Dive part 1

Tyrsson

Retired Staff
Retired Staff
Joined
Apr 29, 2023
Messages
402
Website
github.com
Credits
1,098
Nearly all modern software today will implement some type of template engine. XF, phpBB, Flarum and any other Php application that renders on the web, most likely is using some variation of what is becoming known as TemplateRendererInterface. Today we are going to cover how 3 different template engine renderer's, actually perform the work of rendering your templates i.e. your theme.

Nearly all will leverage buffering. Buffering, for those that want to know has intersectionality with session handling as well, but that will not be covered in this write up.

So, on to what buffering is. In the Php manual we find this explanation:
ob_start()
This function will turn output buffering on. While output buffering is active no output is sent from the script (other than headers), instead the output is stored in an internal buffer.

There is another 16 core functions that are related to buffer handling. The related manual page will list them all as well as further reading on ob_start and can be found by clicking the function name above.

So let us take a look at a template engine and how it utilizes this function to make all your template magic happen.
thephpleague/plates does it like this:
PHP:
    public function render(array $data = array())
    {
        $this->data($data);
        $path = ($this->engine->getResolveTemplatePath())($this->name);

        try {
            $level = ob_get_level();
            ob_start();

            (function() {
                extract($this->data);
                include func_get_arg(0); // this is where the initial template file is run.
            })($path);
             // this is where we get the result of running the template file from the buffer
            $content = ob_get_clean();
            // if you have a layout this is where they inject the first template into the layout
            if (isset($this->layoutName)) {
                $layout = $this->engine->make($this->layoutName);
                $layout->sections = array_merge($this->sections, array('content' => $content));
                $content = $layout->render($this->layoutData);
            }

            return $content;
        } catch (Throwable $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }

            throw $e;
        }
    }

This is where we find the action, where the work is actually being done. To see it in Plates you have to look very closely. I have notated the code with comments for those that like making themes but may not be real knowledgeable in php.

Advanced Usage Tip:
The Layout functionality has wider implications in most applications than one might first consider. It allows Ajax request to work the way you need them too. The approach is usually to load the full layout which is basically the head and foot of the site. Of course this will load your assets, etc. During your Ajax request you can disable the layout and only update a section of the DOM with the response. This can be an extremely effective way to improve user experience and overall site responsiveness if done properly. Many times you will not need to update the DOM and your application can return a pure json response. An example that comes to mind is for system messaging where the application will just display a modal etc to the user notifying them that their action has been completed or failed. Quoting messages here works in a similar fashion. The "when" and "how" of disabling the layout will depend on the workflow of the application and the template engine.

So, I guess you might be thinking that Plates Engine is special at this point right? Nope, not at all. Twig works in a very similar way, albeit with a lot more functionality. For brevity I'm only posting the entry point to that below as it has the same signature as the other two engines we're looking at.
PHP:
    public function render(array $context)
    {
        $level = ob_get_level();
        if ($this->env->isDebug()) {
            ob_start();
        } else {
            ob_start(function () { return ''; });
        }
        try {
            $this->display($context);
        } catch (\Throwable $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }

            throw $e;
        }

        return ob_get_clean();
    }

Looks familiar does it not?

Well, that's great but you are probably wondering how the code in the template file gets executed, like all your foreach etc for presentation logic right? Well, they are taking advantage of this:

include

From the manual page:
The include expression includes and evaluates the specified file.

I will use an excerpt from the Laminas View render method as an example because it makes it a little more clear what is going on:
PHP:
try {
     ob_start(); // starts buffering
     $includeReturn   = include $this->__file; // evaluates the template file, captures the result of processing
     $this->__content = ob_get_clean(); // stores the buffer content into a variable for use
} catch (Throwable $ex) {
    ob_end_clean(); // discards the buffer contents
    throw $ex;
}

With the actual execution of the template files themselves out of the way we need to cover: "What makes the extensions/helpers/plugins work?". To answer that question we have to look deeper into the Engines themselves, which are just a collection of collaborating Php Object instances. The short answer literally is... They usually use Php "magic" in the form of __call(), which is quite literally a "magic" method of a class that is called every time you call a method through the objects scope.

Note: Twig diverges from Plates and Laminas View here due to them using compiled templates. I'm only trying to show the similarities here without causing too much distraction by getting you sidetracked with details you may never need to know, even when using Twig.

It looks like this in Plates:
PHP:
    /**
     * Magic method used to call extension functions.
     * @param  string $name
     * @param  array  $arguments
     * @return mixed
     */
    public function __call($name, $arguments)
    {
        return $this->engine->getFunction($name)->call($this, $arguments);
    }

So, as we can see, nearly all php template engines in some regards work in very similar ways, they usually share a common approach to the same problem. Essentially they create a family of objects and execute the template file in the scope of one of those objects, usually known as the Renderer, Template etc. The names differ from one implementation to another but the approach doesn't change drastically. Well, please bare in mind I am speaking in general terms when I say the approach does not change much. As an example, previously, we saw an example of how Plates calls extensions. In Laminas View they are known has helpers and here is how they are called through the scope of the PhpRenderer Object:
PHP:
    /**
     * Overloading: proxy to helpers
     *
     * Proxies to the attached plugin manager to retrieve, return, and potentially
     * execute helpers.
     *
     * * If the helper does not define __invoke, it will be returned
     * * If the helper does define __invoke, it will be called as a functor
     *
     * @param  string $method
     * @param  array $argv
     * @return HelperInterface|callable|mixed
     */
    public function __call($method, $argv)
    {
        /** @psalm-suppress MixedAssignment $plugin */
        $plugin = $this->plugin($method);

        if (is_callable($plugin)) {
            return call_user_func_array($plugin, $argv);
        }

        return $plugin;
    }
It just looks more complicated because Laminas View uses a specialized Service Manager instance to manage View Helpers.

I have found that the hardest to understand concept within all of this interaction for most people is that every template is executed within the scope of the Renderer Object (or a collaborator). It happens when the template file gets included. In modern web applications the only file you ever really access in the entire application (usually) is the index.php file. All other code is executed as a result of application state and context based on the request.

So basically that is "How" your template engine parses your theme in "most" modern web applications. Yes, I realize there are many caveats to this discussion but usually even if an application is rolling their own template engine... If its in php 85% of the time it will share at least some of the techniques we covered in this write up.

Hope it helps some of the new folks understand its not as complicated as you first think it is.

Learning programming is usually a 2 step process:
1.) Create a Backup (Git Branch)
2.) Break stuff
 
Last edited:
Thanks, I got a few more planned. It's hard to find the balance of, enough information, and not losing the reader. I usually tend to try and include everything, which I am trying to break that habit :p
 
I'll be adding a programming and language category to our articles today and convert this to an article. Great informative post!
 

Log in or register to unlock full forum benefits!

Log in or register to unlock full forum benefits!

Register

Register on Admin Junkies completely free.

Register now
Log in

If you have an account, please log in

Log in
Activity
So far there's no one here

Users who are viewing this thread

Would You Rather #9

  • Start a forum in a popular but highly competitive niche

    Votes: 5 21.7%
  • Initiate a forum within a limited-known niche with zero competition

    Votes: 18 78.3%
Win this space by entering the Website of The Month Contest

Theme editor

Theme customizations

Graphic Backgrounds

Granite Backgrounds