Table of Contents
Video 11: Code Generators, Part 1: Setting Up/Creating a Recommendation Module
The eleventh VuFind® instructional video explains how to set up a local code module and use VuFind's code generation tools to build a custom Recommendation module.
Welcome to the 11th VuFind tutorial video. This month we are going to start looking at code generators and talk about how to manage local custom code and how to build a simple recommendation module using a code generator. So in past videos we've talked about two ways in which you can customize your VuFind instance, one by maintaining a local configuration directory for your config files and another by creating a local team. So the third way is to create a local code module where you can extend override or add to VuFind's code.
So before we start creating a code module, let me just talk a little bit about code modules in general. So if I go to the VuFind home directory and I look at the files that are here. There's a directory called module. And if I look in there, I see that there are a number of sub directories, VuFind, VuFind API, VuFind admin, etc. Each of these directories is a Laminas code module. Laminas has a module manager, which allows you to organize your code into modules, each of which is bundled with a bit of configuration. And a Laminas application can be built by composing together a number of these modules to get all of the functionality into one place. And the neat thing about the code plus configuration design here is that you can add a module of your own and load it last. Then the configuration of your module will override equivalent configuration in any of the earlier module. Thus, you can build your own module for VuFind, put your code and configuration in there, and this gives you the power to add to or replace any of the core functionality.
The modules that come with VuFind bundle different parts of its functionality. The majority of the application is in the main VuFind module. Some more specialized pieces have been broken out into separate chunks. And it's entirely possible that over time, the code will become more modular as we split it up into more logical pieces. But that's an ongoing process. In any case, to get started, let's create a local module where we can begin to work. And when I create it also show you what it looks like so you can have a better understanding of how these modules are structured. If you want a module VuFind's install script, it gives you an option to create one. So depending on how you set up your VuFind instance, you might actually already have one. But if you don't, don't worry, you can just run the installer again. Keep all of your options the same except add a module, and that'll do the job for you. So that's exactly what I'm going to do right now. The tutorial VuFind instance we've been working with was installed from a Debian package, which does not create a local code module by default.
So I'm just going to, from my VuFind home directory, run
php install.php to rerun the installer, and this will give me the option to create a module. So I have to be sure that all of the other choices I make here are the same as the choices that were made the last time the installer was run. So it asks me where I want my local settings, vufind/local. That's fine. I want to keep the default.
Now it asks me what module name I want to use and this is where I need to make a decision. There can be any legal PHP namespace, which means practically anything. You just want to be careful that you don't choose a name that either conflicts with an internal PHP command, or conflicts with an existing VuFind module, or a dependency. Usually if you name it for say your local institution, you'll be very safe for the purposes of the tutorial though I'm just going to call my local module tutorial. So now it asks me what path to use and VuFind URL. I want to keep that the same: /vufind; and I'm done.
So all that the installer has done is it's created the empty local module for me, and it has updated my Apache configuration to include a setting to load that module. So I got VuFind under Git control last week, or rather last month. I can do
git status to see what the installer actually did. And as I can see it has created a module tutorial directory. And it's modified two files: my env.bat file, which I don't care about because that only is used under Windows, and my local/http-vufind.conf, my VuFind Apache configuration. So let me do a quick get to inspect exactly what has changed. So first of all, what happened in the environment that which again doesn't matter because we're not in Windows, but if we were in Windows, we would care that it has added an environment variable. Pointing to tutorial. The more important changes are in the Apache configuration. You can see that it has uncommented the setting to set up the VuFind local modules environment variable and set it to tutorial. So this is what tells you find which module code to load. So if I had made any mistakes when running the install.php script, I would see other unwanted changes in here, like changes to VuFind directory path. So if that had happened, I could have done a get reset to back out the changes and I could try again. But in this case I've gotten it right the first time. So I am just going to commit these changes so that I have all of this going forward. So I'm going to do a get status again to see what I'm working with.
I'm going to get add the changes to my env.bat. I'm also going to delete this HTTP VuFind dot com dot back dot timestamp file. That's a backup that the install script made of my Apache configuration, which is a really nice safety mechanism to have if you're not using it. But since we're using get to manage our changes in our our versioning, we really don't need to keep backup files around. We can revert using it if we really have to. So I'm going to commit my tutorial module.
There are a couple more things that I also am going to need to do to fully prepare this module. First of all, because I've changed my Apache configuration, I need to restart Apache to load the new configuration. So I can do that with
sudo systemctl restart apache to I'm just going to go over to my web browser and refresh my VuFind to confirm that it still works. I haven't broken everything by adding my new module. That's good.
The other thing I want to do is be sure that I have my module defined as an environment variable at the command line. Because if I use VuFind from the end line utilities, I want those to have access to my custom code as well. So I do this by editing the
/etc/profile.d/vufind.sh script. And I just add to that
export VUFIND_LOCAL_MODULES=Tutorial. Then I'll reload my profile
source /etc/profile/d. And now if I echo $VUFIND_LOCAL_MODULES. I get “Tutorial” so my environment variables are all set up. And I'm ready to begin working with this module.
But before I add anything to it. Let's take a quick look at what's actually in there. And I'll actually open up the code to do that, because I can be easier to look at in a code editor. Alright, so here I am in VS code. And if I look under my module directory I see all of you find core modules as well as the tutorial module I've created. And if I expand that out I see we have a few things here. We have a Module.php file every laminas module contains a Module.php, which is used for loading the module. And if you look at this file. There's not a whole lot in here. It loads the configuration file for us. It's an auto loader, so that the rest of the framework knows where to find the code that lives in the module. And it has a couple of empty hook methods, where you could theoretically add actions that occur when the module initializes or bootstraps but you rarely need to do that. So this is a file you shouldn't have to touch much it's just part of the setup of the module.
Then under config there's a module.config.php file containing the modules configuration. Right now this is simply an empty array, because we haven't put anything in here yet. But we could in theory copy configuration from other modules and change it, and it would be overridden, because our tutorial module is going to load last, which makes it take precedence over the things that are loaded before it. And finally, we have a source/Tutorial directory, and this is where all of our code will live, but we haven't added any code the module yet so right now it's simply an empty directory.
Thus, we need to add some code. And VuFind contains several different code generator tools that can be used to add code to a local module. So you're going to look specifically at the plugin generator, which is what you use when you want to create a new plugin of an existing type. So to run the plugin generator. You simply run
php $VUFIND_HOME/public/index.php generate plugin, and then some parameters. But with any of VuFind command line tools if you don't know what the parameters are you can add
–help, and you'll get a usage screen. That we can provide the plugin generator with the name of the class we wanted to generate and optionally the name of a factory that we want to use to instantiate our class when VuFind builds it. If you don't specify a factory, the generator will actually create a new factory for you that will generate an instance of your class. There's also an optional top level switch that you can use if you want to create a plugin in VuFind top level service manager. Otherwise, the generator is going to look at the class name you provided it and try to figure out what kind of plugin you're generating based on the namespace and then put it in the appropriate context. And as I mentioned at the beginning, what we're going to generate today is a recommendation module. We talked about recommendation modules quite a few videos ago, when we were talking about search configuration. So recommendation modules can be turned on in the any files and they provide additional information, usually above or to the side of search results. So what I'm going to do is create a recommendation module that will display a message at the top of the combined search screen. So this shows you how if you wanted to put a welcome message or some kind of additional local information, you can easily add it to that top level screen.
So I'm going to call this recommendation module tutorial recommend combined notes. So, we'll go back to this generate command and we will say
php $VUFIND_HOME/public/index.php generate plugin Tutorial\\Recommend\\CombinedNotes. I'm not going to specify a factory so this will allow it to generate one for us. And a couple of notes about what I've done. So I want to create a class called tutorial recommend combined notes. Each part of this is important. The “Tutorial”, the beginning of the name, the first part of the namespace tells VuFind and the generator what module this belongs to. Everything in the tutorial namespace needs to live inside the tutorial module and all code in the tutorial module needs to belong to the tutorial namespace.
The next part is “Recommend” and all of our recommendation modules have the second part of their namespace set to recommend. So most of VuFind's core recommendation modules have classes that are named VuFind\Recommend\(something). So we're following that same pattern. We're just using our Tutorial module. And of course, “CombinedNotes” is the name of the actual class we want to create within this namespace.
The reason I have to double up my backslashes is because backslashes are how fully qualified PHP class names represent the parts of or connect together the parts of a namespace. Backslashes have a special meaning on the Linux command line. So I have to double them up so that they get passed through correctly to the PHP code. If I only put single backslashes here, weird stuff would happen. So that's a common gotcha. Be sure when you're providing class names to generators that you double up all the backslashes.
Anyway, now I can run my command. So I have to do some messages. It created my combined notes class. It created a combined notes factory. It backed up my module configuration and then it updated the module configuration. So if I do a get status now. Some files have been added to my module tutorial source directory and my module configuration has been modified. I'm going to switch back over to vs code to just show you what we've got. So as you can see my module configuration has gotten some stuff added to it. There's now a plugin manager's array inside of VuFind array and it contains a recommend section which is where recommendation modules are defined. There's a factories element, which specifies that this new tutorial recommend combined notes class that I created should be built by the combined notes factory. There's an alias set up so that when I configure in my configuration files a recommendation module named combined notes, it knows that that maps to the combined notes class that we've just created.
And going into depth on how this whole service manager configuration works would be a good topic for a future video. But for now, just take for granted that the generator configured this all correctly for you so that you don't have to worry about it. You can just fill in this class and this factory and use this alias name in your configuration files.
So let's look at the source that was generated. These are combined notes.php. As you can see it has the right tutorial recommend name space on it. It's named combined notes. It implements the appropriate interface for a recommendation module, which the generator figured out based on the name space, we told it to use when we specified our class name. But there's nothing in here we're going to have to write this class. But first let's take a look at the factory that was created. And all this does is return an instance of the requested service, which based on the way the service manager works is always going to be the name of the class that we want to build, which would be tutorial recommend combined notes. So based on what we're doing today there is actually no reason to build a special factory to generate this I just wanted to demonstrate that it's possible, so that if you're building something with dependencies, you have this class available to do some dependency injection within. But for today's demo we're just going to leave this at the default and not worry about it any further. Because our combined notes class implements an interface, we're not going to be able to use it until we've defined all of the methods that the interface requires. So let's talk a little bit about the recommendation module interface and how it works. So I can take advantage of the fact that I can right click on, well, normally. All right, that's not working. So I need to look up this VuFind recommend recommend interface to see exactly what methods I have to define in my local class. I mentioned our name space tells us what module our code lives in so I know that this is defined in the VuFind module under the source directory. So here's the VuFind name space. Recommend. And recommend interface. So a PHP interface just defines which methods a class needs to define. So I'm going to paste all of these method definitions from the interface. Into my new combined notes class. And for starters, I'm going to just make them all empty methods that don't do anything. There are three methods that recommendation modules have to define. The first is set config, which receives extra configuration settings that the user defines in the any file and makes use of them. So we can leave this empty if we don't have any configuration. Then there's an init method, which gets called before VuFind runs a search, but after the user's search request has been processed. We want a recommendation module to analyze user input or actually make changes to the request that we're going to make. We can do that here. This is not done very often, but for example, the side facets recommend module uses the init method to turn on the asset values that the user has configured.
For today's example, we're not going to be touching in it. And finally, there's a process method, which gets called after the search has been performed, and which can be used to extract information from the search results if we want to use that to impact the message that we ultimately display to the user. But it's perfectly valid to leave all of this empty. And then you just have a recommendation module that doesn't do anything except display a template. So, let's, let's turn this on and just see what happens and spoiler alert, it's not going to work yet, because we haven't made a template, but I just want to demonstrate what this all looks like.
So I'm going to go into my local config directory. Into combined i and i, because I want to display this recommendation module at the top of my combined search results. And at the bottom of combined i and i there's a recommendation module section that lets me turn these things on. So I'm going to put it on in the top, the name, as you recall from the alias that was generated in my configuration is called combined notes. So just by adding this top equals combined notes to the recommendation modules list in combined i and i, I should now have turned on my new recommendation module, and it should appear at the top of my combined search results. Except we get an error, because I haven't created a template yet. But as you can see what i'm looking at here is the generic please contact us for help message, which doesn't really tell us anything about what went wrong. And that's because by default of VuFind runs in production mode, which does not display detailed error messages because we don't want to accidentally display something that exposes a security vulnerability to the general public. However, when we're actually working on developing VuFind on a development server. It's often helpful to turn on development mode, which will display richer error messages when things like this go wrong.
Let me quickly show you how to turn on development mode. I'm going to edit the Apache configuration in local slash httvufine.com and scroll down a little bit. There's a commented outline that says set end view find end development if I uncomment that. And then restart Apache. Now if VuFind is in development mode and if I refresh this page, I get a much more useful message, which is actually telling me that I didn't set up my recommendation module correctly, I have a suspicion that I didn't get saved after I made my changes over. Sure enough, that's what happened. So it was originally complaining because I hadn't saved my method definitions, and so I wasn't conforming to the interface. I've now gone back save that and fixed it so if I refresh again I should get the next error message, which is now that it's looking for a combined notes template so that it can display the recommendations, but I haven't created that yet.
Can't find it, therefore it can't display recommendations and it throws an error. And so, this points out that by convention, every recommendation module needs to have a PhDML file in the recommend template directory of your team, whose name corresponds with the name of the class. Any VuFind plugin that displays something follows this convention of having a template directory containing PhDML files whose names correspond with class names. And if you look in the plugins page of the VuFind wiki, you'll find all of the plugin types you can build as well as notes on these types of conventions on a plugin by plugin basis.
Now the important thing is, in order to proceed, we need to create this template file. And because I don't want to change core VuFind code, we should turn on our custom theme again so that we can make our custom files in our custom theme. I'm just going to bring up my config.ini file in my local configuration directory. And I'm going to comment out the boot print three theme and uncomment the tutorial theme that we set up a few months ago. Inside my theme directory, inside my tutorial theme, inside its templates directory, I'm going to create a new folder called recommend. And inside that folder, I'm going to create a new template file called combined notes.phtml. And inside that file, I'm just going to put the HTML. You are looking at combine search results. And I will actually save that this time. So now if I go back to my browser and I refresh the page again, everything is working. My custom theme is turned on, which is why things look different. And my recommendation module has loaded and rendered right above my search results.
This would be a really good time to go back to the command line and commit all of this stuff that I have just done. So I'm going to get add module tutorial source. And I'm going to get add this tutorial templates. And I'm going to commit that as create combined notes recommendation module. I can also get add my HTTP VuFind comp where I turned on development mode. Let's say turned on development mode. And for now I'm not going to commit my configuration changes. We'll see if we want to keep things this way after we've done a little bit more work.
So at this point I've already demonstrated how to set up a local module, how to use a plugin generator to create a plugin within that module, how to turn that on and how to fill it in enough that it will work. But let's do a little bit more advanced work and talk a little bit more about recommendation modules while we have this opportunity, because it's great that we can display this template in this place.
But it would be more fun to see how all the different pieces work together. So let's start by adding a configuration setting so that if we want to change the name of our combined search results, we can set that in the i and i file. As I mentioned earlier, the set config method of a recommendation module allows you to receive configuration settings that come from the i and i. So if I open up my combined notes. Let's create a protected property here called name. And then in set config. Let's just pass the settings we receive into that name property. And let's create a new public method called get name, which simply returns that name property that I just set up. So now, I'm going to go to combined i and i. I can add parameters to a recommendation module by just putting in a colon and then adding stuff after the colon. So, let's say the tutorial results, put this whole thing in double quotes since it has spaces in it. We want to be sure the configuration parses correctly. The tutorial results will get passed to the set config method. Then it will get stored in the this name property and it will be available for retrieval through this public get name method. And of course, the place where we want to retrieve it is in our template, which by convention is going to have access to our. CombinedNotes class using the name this arrow recommend. So what I can do is say this escape HTML, because we want to be sure that we don't miss render special characters in our text. This recommend get name. And I will delete. Combined search results. And actually, just for completeness, I'm going to go back and make my second big a little bit smarter, say. If the settings are empty, we'll call it combined search results. Otherwise, we'll use the settings. So this way, it has a reasonable default, but it can be overwritten. So again, the flow. I put a parameter on the recommendation module configuration in my any file. It passes to set config which stores it. My class exposes the stored property through a get name property, and I can call that get name property in my template. By referring to the this recommend object, which is always going to be available in any recommendation module template pointing at the instance of the recommendation module class.
So now I come back here and I refresh the page. Combined search results has changed to the tutorial results, and I've showed you both how to configure a recommendation module and how to use properties of the recommendation module to render a response.
Let's just do one more clever thing and show the user's search query along with the message. We can do that by going to be going to the process method, which receives an instance of VuFind search results object after the search has been completed. Well, first, let's create another property. We'll call this query. The process, we can say this query equals results get params. Get display query, which is how you get access to the user's input in a human readable form from a search results object. We also need to create a get query method to expose this property so that the view template can read it. So we just create a query that returns this query, save all of that. In our template here, we can add for query colon, and then embed. This escape HTML, this recommend get query close friends, and save that. We can actually do a search that has a query, make this more interesting. And sure enough, now it says you're looking at the tutorial results for query web. So again, my search query got processed, the recommendation module pulled that out of the results object, and we displayed it right here.
Of course, there's a lot more to learn. There's a lot of depth to what VuFind is doing. But the point here is that you can build small plugins like this, which have access to other pieces of VuFind, and you can pull things out and push them to the display. So that you can do some fairly sophisticated customizations from within your local module with relatively little effort. And next time around, I will go into some more code generators and show you how to do some other advanced customizations. Until then, as always, feel free to reach out with questions through the mailing lists or Slack, and have a good month.
This is an edited version of an automated transcript. Apologies for any errors.