One of VuFind’s most important features is its theme inheritance system, which allows users to customize the interface by creating sub-themes that only override the templates that need to be changed. This helps isolate user changes from the core code and simplifies upgrades.
The Zend Framework 1 Solution
Since theme inheritance is such a core feature of VuFind, it was the first challenge I tackled when adapting the code to Zend Framework. Fortunately, the list8d project had already solved the problem for me and documented it in a very helpful blog post, so I was able to implement the feature quickly. Although VuFind’s implementation adds some features and changes a few details, it hasn’t strayed too far from the original list8d code.
Differences from VuFind 1.x
The biggest difference between VuFind 1.x themes and the list8d solution is that in VuFind 1.x, you had to create a comma-separated list of themes in the configuration file to specify how inheritance worked. In VuFind 2, with the list8d-inspired system, inheritance is controlled by a “theme.ini” file within each theme which tells VuFind whether or not the theme has a parent. The VuFind 2 approach is preferable for two reasons: it makes the config file more concise and easier to understand, and it prevents users from creating illegal inheritance chains by entering invalid comma-separated sequences.
Moving to Zend Framework 2
Now that I am moving from Zend Framework 1 to Zend Framework 2, I have again started by tackling the theme problem. Fortunately, the list8d solution still works, though it requires a few significant adaptations. The remainder of the article will highlight key changes. All of my code is available through Git on VuFind’s Sourceforge project; feel free to borrow anything that you find useful.
Change 1: Exposing Public Resources
The list8d article talks about creating a link under the public webroot to expose theme resources. I instead opted to handle this through Apache configuration:
allow from all
(Some lines wrapped and indented for clearer display)
Note that the actual file path to the themes may be subject to change. I’m still debating whether themes belong inside or outside the VuFind-specific module (right now I chose outside, since this allows multiple modules to share the same Apache mappings) and whether or not the themes folder needs to be broken into subdirectories for disambiguation (that accounts for the current redundant “vufind” in the path, but I might decide to eliminate it for simplicity at risk of clashing with other modules). Feedback is welcome.
Also note that VuFind comes with an install script that automatically customizes the Apache configuration to adjust VuFind’s base URL and installed path, so you don’t actually have to edit all of this stuff by hand if you use non-default settings.
Change 2: Initializing Themes
The list8d solution proposes setting up themes by implementing a base controller that all other controllers inherit from. This controller’s init() method is then responsible for reading in the theme.ini file and setting everything up (which mostly consists of manipulating the framework’s search paths so it finds the appropriate templates and helpers in the appropriate places).
When I adapted this for my initial ZF1-based VuFind prototype, I tried to make it more stand-alone by creating a Zend Controller Plug-in to do the work rather than embedding it in a base class… but this didn’t really change anything significantly; it just moved the logic from one somewhat obscure place to a different somewhat obscure place.
Fortunately, Zend Framework 2 has a more comprehensible event-driven architecture for plugging things into the workflow. Rather than using base classes or weird plug-ins, you can hook events from a module’s bootstrap method. This allows much better separation of concerns: I was able to create a VuFindThemeInitializer class which does the actual theme startup, and then I attach different methods of the initializer to appropriate events as part of VuFind’s bootstrapping process.
Change 3: Custom Template Injector
One of the features of Zend Framework 2 is that, if no template is explicitly specified, the framework injects a default template name into the view model. This default template name is the namespace of the module containing the controller, then the name of the controller, then the name of the action. That interferes with the theme system — we don’t want the namespace on the template name. I created a custom template injector that eliminates the namespace and (due to my own personal preference) also makes sure that URLs are case-insensitive by stripping out dashes caused by camelCase action/controller names. This is set up as part of the theme initialization routine (see the configureTemplateInjection method).
Change 4: View Helper Loading
The original list8d theme solution simply injects helper paths into the helper broker. The framework then searches up the theme inheritance tree until it finds a matching helper. This is easy (no configuration necessary) but it is also slow (every helper initialization requires a search of the file system). Because ZF2 deals with helpers a little differently, I decided to make helper configuration more explicit. Each theme.ini file now includes a helper_namespace setting which specifies where helpers live, and a helpers_to_register array which lists all of the helpers that need to be made available. This explicit configuration is obviously less convenient than “magic” auto-loading, but since adding helpers is a relatively infrequent task, the performance benefits seem to justify the change.
I initially set things up so that themes had their own unique namespaces and the theme initializer found the active Zend Autoloader and informed it how to find the helpers in that namespace under the themes directory. I eventually decided this was unnecessarily overcomplicated and scrapped it — now all of VuFind’s view helpers live in the namespace VuFind/Theme/[theme_name]/Helper (which means their code is inside the VuFind module rather than under the theme directories) and take advantage of the default autoloader settings. I’m reasonably happy with this solution, but it’s not hard to change if a better layout is determined in the future.
Change 5: The Tools Class
As I already mentioned, I set up a VuFindThemeInitializer class to do the work of setting up themes. The initializer in turn needs to know a few things: for example, the base path of the application and the place in the session to persist theme settings (to reduce redundant file accesses). Rather than hard-coding these details into the Initializer, I created a VuFindThemeTools class which provides these details to the Initializer’s constructor. This provides an opportunity for using dependency injection to change default behavior and implement unit tests. It also reduces redundancy, since other classes that need access to the same resources (i.e. theme-aware view helpers) can pull data from the Tools class rather than duplicating the dependency initialization.
Ideally, I should probably define a ToolsInterface to guide implementation of alternate tools classes. It may also make sense to split this class into separate pieces to handle different sets of functionality. For now, I’m just using a single catch-all Tools class to keep things simple; it’s always possible to refactor when all the use cases become more clear.
Change 6: The ResourceContainer Class
In the list8d implementation of themes, you can specify CSS and JS files in your theme.ini file to ensure that they are loaded on every page within a given theme. The VuFind implementation extends this to support favicons as well. In the Zend Framework 1 version of all of this code, the theme initialization not only loads these settings from the configuration file but also parses it and configures the framework appropriately.
This is potentially inefficient — some controller actions won’t ever render a page; others will forward from action to action, causing redundant work to be performed.
When I reimplemented this in ZF2, I added an extra layer. The VuFindThemeInitializer loads the settings from the configuration file into a VuFindThemeResourceContainer object provided by the VuFindThemeTools object. The settings are not actually processed until it is actually time to render a page. At that point, a call to a new HeadThemeResources view helper causes the files to get loaded. As with the changes to helper loading, this extra explicit step is slightly inconvenient, but the performance benefits should outweigh that disadvantage — creating a new layout is an uncommon activity, so adding one step to that process shouldn’t inconvenience anyone too severely as long as the process is well documented.
Change 7: View Helpers
The list8d solution provides a custom HeadLink helper that searches the themes in inheritance order to find the best matching CSS file. VuFind’s solution adds a similar custom HeadScript helper and also adds an ImageLink helper which finds the most appropriate image file. Since all of these helpers use similar logic to locate files, they rely on a shared method in VuFindThemeTools to do the bulk of their work.
This is a very young solution, and it’s entirely possible that I’ll run into problems that will require some changes and refactoring. I’m also aware that my class names and file locations may not be ideal, and I’m open to feedback on possible improvements there. Finally, it may eventually make sense to build a stand-alone ZF2 ThemeInheritance module and further separate VuFind-specific behavior from the generic theme-related tools. I don’t think this would actually be a huge amount of work, though for now my first priority is to finish the VuFind 2.0beta prototype. Once that is done and the code has been more thoroughly exercised, it may be worth revisiting whether it can be further modularized for better sharing.
I’m grateful to the list8d team for sharing my work, and I hope that my additions and changes will be of use to others. This has been another long, rambling post, and I’m sure there are some details I failed to touch on. Let me know if you have questions about anything.