TemplatePathStack resolver loop

Hey folks,

I was using the vendor/bin/templatemap_generator.php to generate some of the module template mapping. But was wondering why we did it.

So the question that popped my mind was: how is Zend Framework finding the template file. So I started debugging to see what happend in the template resolvers. With the templatemaps it’s easy for the Renderer to find the matching template as it’s in the template_map key. And thus the first resolver returning the destination of the file is Zend\View\Resolver\TemplateMapResolver.

What about without the template mapping. I removed the view name it was looking for from the template_map.config.php to see what happend as it was no longer in the template configuration. The second resolver, TemplatePathStack, will return the file.

Notice: using the default view structure as in module/<ModuleName>/view/<ControllerName>/<action-name>.phtml

After a little debugging of the Zend\View\Resolver\TemplatePathStack I saw it has paths of every module offering any view files. So it basically loops each and every path to see if the path of the model exists in any of these paths. The problem that I’ve with this is that if the module is one of the latest mentioned in the paths it has, it had to check each and every other path whether the file existed in it. While the path of the model is using a common format (convention) of <module>/<controller>/<action-name>.

So is there a reason it is looping through each and every path, while we could find the path of the Module using the requested path of the viewModel, as in application/index/index(.phtml)?

We can only overwrite the paths of a view by redefining the key within the template map. It is not that one module is able to match anothers view path, using the format/convention.

When debugging I was using the following package version of "zendframework/zend-view" : 2.9.0
Talking about the following file/line combination:

There is indeed a reason: it’s to allow modules to override templates defined in other modules.

That loop short-circuits on the first path that has a readable file; as such, if the module that provides an override has the related template and is registered early, that loop might run as little as one time.

Or perhaps I’m not understanding your concern? Could you provide some examples, perhaps?

Lets assume we have multiple modules within our application and even some modules from composer\vendors.

'modules' => [
    'DoctrineModule',
    'DoctrineORMModule',
    'ZfcBase',
    'ZfcUser',
    'ZfcUserDoctrineORM',
    'ZfcRbac',
    'Application',
    'Account',
    'Acme',
    'Derp',
    'Console',
    'Common',
    'Log'
] 

Then we can override the templates of another module using: $config["view_manager"]

So an exmaple of overriding the mapping in our module.config.php of the Account module, handling the user:

'view_manager' => [
    // Full module override
    // The TemplatePathResolver now matches the zfcuser module to the account/view folder.
    'template_path_stack' => [
        'zfc-user' => __DIR__ . '/../view',
    ],
    // File override or map
    // The TemplateMapResolver now matches path to view file 
    'template_map' => [
        'zfc-user\user\login' => __DIR__ . '/../view/account/user/login.phtml',
    ],
],

Little sidenote: overriding is only possible when the module is loaded later than the one it is overriding.

The PhpRenderer is using the defualt Zend\View\Resolver\AggregateResolver. The first one in the iterator is the TemplateMapResolver checking whether the path of the model is in the applications template_map. But when we override a full module by using the template_path_stack key in the view_manager it will result in the following paths within the TemplatePathResolver::getPaths()->toArray():

0 => '/var/www/test-project/module/Account/src/../view/',
1 => '/var/www/test-project/module/Account/src/../view/',
// Key original value was:
// 1 => '/var/www/test-project/vendor/zf-commons/zfc-user/src/../view/'

We’ve got a duplicate value as the account view path replaced the zfcuser template_path_stack. So now when it is looking whether it can match a path as in: zfc-user/user/login, it will iterate over all the modules and will find a match if the file (new SplFileInfo($path . $name))->isReadable() in the modules view folder. So that basically means: module/Account/view/zfc-user/user/login.phtml. Note: other templates could now check the same path twice as it is a duplicate within the paths.

Another finding I encountered is that when the template_map_path is not mentioned and the file ::isReadable() it will allow for overriding. This can only happen if the path of the module is a match with a file. So basically when I don’t mention the template_path_stack in the module/Account/config/module.config but I do have the right folder structure, this could override the template. Not sure if this is expected or wanted behaviour.

So back to my original concern is that when we read the paths of the TemplatePathResolver there might be something like this using the modules provided in the example:

0 => '/var/www/test-project/module/Application/src/../view/',
1 => '/var/www/test-project/module/Account/src/../view/',
2 => '/var/www/test-project/module/Acme/src/../view/',
3 => '/var/www/test-project/module/Derp/src/../view/',
4 => '/var/www/test-project/module/Console/src/../view/',
5 => '/var/www/test-project/module/Common/src/../view/',
6 => '/var/www/test-project/module/Logger/src/../view/',
7 => '/var/www/test-project/vendor/zf-commons/zfc-user/src/../view/',

If we haven’t mentioned a path in the template map but it has to check whether the path: common/index/view returns any result it’s basically checking 5 other modules before finding a file matching the path.

Isn’t there a way we can make it more obvious as the path pretty much shows that we need the common module due to the format of the path or can the path of the view file change? If not we can first check if there is a file match inside the module using the path of the model and if that doesn’t result in a file it could always iterate the whole application.

Hope this clears it up a bit. If not, I could always consider setting up a basic repository as an exmaple if it’s appreciated.