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.
What is required to make this work?
So let's take a look at how Plates handles it for themes:
You can find the relevant docs here:
League / Plates
ThemeResolveTemplatePath.php
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
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:
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.
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?
- 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.
- 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.