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 2

Tyrsson

Retired Staff
Retired Staff
Joined
Apr 29, 2023
Messages
402
Website
github.com
Credits
1,098
In Part 1 we looked at how template engines actually parse the template files and render your theme. This, however, is not the extent of functionality offered by most modern template engines. Most engines today make it very easy to provide template inheritance. We hear all the time that in Php classes use "inheritance" but you might be wondering how a file can be inherited. I know for awhile I wondered the same thing.

First, we need to make sure everyone is on the same page about what inheritance means in respect to template engines and themes. Usually, most of the time, it means that if you are using a theme approach the engine will provide some type of mechanism so that if your currently served theme does not have a particular file present then it will, or can, be served from another theme, usually the default/main.

I should also mention, I do not personally use Plates, but it is about the simplest to configure and setup so that is why I am using it in the examples here.

PHP:
$plates = Engine::fromTheme(Theme::hierarchy([
    Theme::new('/templates/main', 'Main'), // parent
    Theme::new('/templates/user', 'User'), // child
    Theme::new('/templates/seasonal', 'Seasonal'), // child2
]));

What is required to make this work?

  1. Config: In most modern applications you would have a factory that creates the Engine instance and returns it to your PSR-11 implementation. Most implementations provide access to the application configuration which means that all of the Theme::new calls could be called in a foreach and passed the aggregated config from several modules allowing each module to provide its own configuration and only pushing those template into the stack if the module is present and loading, which is the way I would recommend initializing it. So that you only incur the overhead of searching the path if the module is present. You would however need to take care that "Main" is added first. There is a lot of factors that affect application initialization, commonly known as "bootstrapping", and is beyond the scope of this write up.
  2. Resolver (sometimes an Aggregate Resolver that allows chaining multiple Resolvers together)

So let's take a look at how Plates handles it for themes:
You can find the relevant docs here:
League / Plates

ThemeResolveTemplatePath.php
PHP:
public function __invoke(Name $name): string {
        $searchedPaths = [];
        foreach ($this->theme->listThemeHierarchy() as $theme) {
            $path = $theme->dir() . '/' . $name->getName() . '.' . $name->getEngine()->getFileExtension();
            if (is_file($path)) {
                return $path;
            }
            $searchedPaths[] = [$theme->name(), $path];
        }

        throw new TemplateNotFound(
            $name->getName(),
            array_map(function(array $tup) {
                return $tup[1];
            }, $searchedPaths),
            sprintf('The template "%s" was not found in the following themes: %s',
                $name->getName(),
                implode(', ', array_map(function(array $tup) {
                    return implode(':', $tup);
                }, $searchedPaths))
            )
        );
}

We see from this that when the Resolver is invoked it expects a Name object as an argument. Here is the relevant code from that class:

Name.php
PHP:
    /**
     * Set the original name and parse it.
     * @param  string $name
     * @return Name
     */
    public function setName($name)
    {
        $this->name = $name;

        $parts = explode('::', $this->name);

        if (count($parts) === 1) {
            $this->setFile($parts[0]);
        } elseif (count($parts) === 2) {
            $this->setFolder($parts[0]);
            $this->setFile($parts[1]);
        } else {
            throw new LogicException(
                'The template name "' . $this->name . '" is not valid. ' .
                'Do not use the folder namespace separator "::" more than once.'
            );
        }

        return $this;
    }

So I am sure you are asking, ok so how does this work. Well, it's fairly simple. The resolvers usually take a directory/directories and some paths. Paths are usually in array form and operate as a stack most often. Whether that is FIFO or LIFO depends on the implementation and you will need to consult your particular engine's docs for that. Basically that just determines which end of the array they are pushed and read from during processing. This usually results in namespaced naming of template files which are then passed to the render method of the composed renderer. However, due to the way plates handles themes its render method expects a single string argument. The template name you wish to render:

PHP:
$templates->render('layout');

The magic all happens at the resolver (mostly). It is what searches from the last child template to the parent and when it finds your template it renders it as was previously outlined in Part 1. You are allowed to pass a directory/file but do not pass the / instead pass it as directory::templateName and the Name class will resolve it.
 
Welcome. I really hope it helps those folks that are trying to get their heads around the how and why their template files gets loaded the way they do. I know when I first started it was like chasing myself in circles.
 
I've actually over the past day or so built an app leveraging Laminas View to exemplify how you can build an entire website with a good template engine and a couple libraries. I will probably add a part 3 to this series as a walkthrough on how to set up a simple app and use all of the components I have posted about so far to date. It will include usage of PSR11/PSR7 and templating. The app also provides a great deep dive in how to provide aggregated configuration to an application with development override.
 

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