Video 22: Customizing Record Views
The twenty-second VuFind® instructional video provides an overview of VuFind®'s record display code, including Record Drivers and the Record Data Formatter, and includes a demonstration of adding a custom field to your VuFind display.
Hello, and welcome to this month's VuFind tutorial video. This month, we're going to be talking about customizing VuFind's record views.
So to give a quick overview, I should first say that this is a very in-depth detailed topic, and we can only scratch the surface of it in a brief video. So I encourage you to look at the learning VuFind book, look at the code, look at the wiki, and ask questions of the community if you need more help. But I hope that this video will provide some useful context, and at least show you some of the starting points you'll need to explore to get a deep understanding of all of these concepts.
So in this video, we're going to talk about record drivers, which are the way VuFind internally represents individual records regardless of where they come from. We're going to talk about the record data for matter, which is a tool that VuFind uses to display tables of data taken from records, which makes it easy to customize particular parts of the user interface in sophisticated ways. And I'm going to do a quick demo of how you can add a custom field to your VuFind instance if you have something you want to display that's not part of the core system.
So let's start by talking about record drivers. What is a record driver? A record driver is an abstraction. It is a PHP object wrapped around some metadata with accompanying custom templates. And this is how VuFind displays all of the data coming from all of the records that it interacts with.
The idea here is that VuFind only interacts with data using record driver code. It does not directly interact with the data. What do I mean by that? For example, in a MARC record, the title is stored in a 245 field. But information about things like 245 fields only exists inside record driver code. Outside of the record driver, VuFind only cares about getting a title. It asks the record driver, please give me a title. And the record driver, if it's wrapped around a MARC record, gives it the 245 field. If it's wrapped around a Dublin core record, it gives it the DC title field, etc, etc. So it lets us encapsulate all of the specific metadata handling in one place and write more generic, more reusable, more flexible code that can interact with all kinds of things at a higher level.
VuFind uses what is called duck typing to determine compatibility between its various features and particular records. So for example, you know, if we want to display a title, it's going to check with the record driver if it's capable of displaying a title and then only display it if appropriate. Now, of course, everything has a title, but there are some more advanced features that are more metadata format specific. And for these purposes, the record drivers are able to conditionally provide data only when it's supported and a lot of VuFind's code is designed to either load particular logic based on the presence of capabilities or to gracefully degrade if we want to do something and we can't. All of this is designed again to make it possible to write fairly generic, fairly reusable code and have it interact successfully with all kinds of different data because VuFind is designed to let you index practically anything and also to interact with a number of external systems that format data in different ways. Of course, with all of the possibilities that are out there, it would be easy to end up with a lot of redundant and complicated and contradictory code, but VuFind uses features of the PHP language, particularly treats and inheritance to make code as reusable as possible and to share features across multiple drivers. So in order to understand record drivers, having a good understanding of PHP object-oriented logic is helpful. So let's go into a little more detail on the PHP side of things. So every record driver is a PHP class and all of these classes can be found in the VuFind record driver namespace. There are a few record driver classes that are particularly important. One is called abstract base. This is an abstract base class as the name suggests and all of the record drivers extend to this class either directly or indirectly. And this is where the absolute bare minimum functionality for record drivers lives. All of the things that every record driver has to be able to do are implemented here. And this is fairly minimal because VuFind is designed to be very flexible. But at the very least, every record needs to have an identifier and every record needs to be able to be displayed through some textual representation. And so these bare minimum methods are there in abstract base.
Now, even though VuFind is designed to be extremely flexible and to support a lot of use cases, the most common use case in VuFind is displaying bibliographic records, records representing books or other materials. And so there is a record driver called default record which extends abstract base and adds a whole lot of methods that support bibliographic record functionality, things like titles and authors and subjects and so forth. So this allows us to create sort of a standard implementation of bibliographic record information which is shared by a large number of other record drivers. So whether we're working with data from Solr or from a third-party discovery system, we're probably in a subclass of default record which just fills in a standard set of methods using data from appropriate places depending on the data source.
Record drivers have their own plug-in manager just like pretty much any extensible component of VuFind. So while the default is to use the record drivers from the VuFind record driver name space, you can very easily create your own record drivers or extend the built-in record drivers to add methods, change behavior, etc. All of this is extensible and over-rideable. It's also worth noting that the way the plug-in manager is used varies from search backend to search backend. And search backends were talked about in last month's video when we went over the search system. But the important thing is that whenever we're retrieving data from a search backend, VuFind has code that turns it into record drivers so that the rest of the system can interact with it.
In many cases, this is pretty simple, a lot of the web-based discovery systems just have a single record driver. So for something like EDS or Summon, we're just going to pull a copy of the appropriate record driver out of the plug-in manager and populate it with data for every record that we've got. But for records coming from the local Solr index, we do things in a slightly more sophisticated way because it's possible that we have sort of heterogeneous data in our Solr index. We might have some MARC records populated from one place, some Dublin core records populated from another and so forth. And of course, depending on what kind of data is stored in the Solr index, we may need to display it in different ways.
And so this is where Solr's record format field comes into play. For each record that we fetch from Solr, we look at the record format field and we look to see if there is a record driver registered in the plug-in manager with the name Solr and the value of record format following that. So for example, if record format is set to MARC, we look for a Solr MARC record driver. If we find a match, we use that. If we don't find a match, we default to Solr default, which is another core record driver that provides all of its data based on what has been indexed into Solr. Some of the other drivers like Solr MARC, which I mentioned, augment the data in the Solr index with more specific things pulled directly from the raw source record. So again, that's a fair amount of detail, but the point is we have a lot of flexibility in how we load these record drivers and it lets us be very specific in what we present while interacting with the rest of the VuFind system in a very standardized and generic way. So as I mentioned at the beginning of the discussion of record drivers, the record drivers are not just PHP classes. They are also sets of templates. The reason for this is that some parts of VuFind displays are rendered using record driver specific templates. So for example, when you get a screen of search results, every individual search result that you see is getting rendered using a different template pulled from your theme. The reason for this is as I say, you might have a mix of different kinds of records in your results and they may need to be displayed very differently. So VuFind has the ability to display a totally different template for multiple records in the list based on what kind of records they are. And again, in practice, most of what VuFind does by default is very similar, but the design is such that it can be extended a great deal and you could have an extremely diverse set of displays if you need them. So the templates that are used for these types of displays are found in the record driver subdirectory of the templates directory of your theme. Inside this record driver directory, there are driver specific directories whose names match the record driver class names minus the namespaces.
So for example, if we had templates that were specific to VuFind record driver MARC, they would be in a folder called templates record driver MARC, not using the VuFind piece of the name. Again, most of VuFind's default templates are actually defined in the default record directory because most of what we display by default is bibliographic record type stuff. So we just have these core templates that we reuse across all the drivers, but if you need to customize something more specifically to a particular record driver, all you have to do is create a folder, put an override template in there, and that will be displayed only for records matching that particular type. And of course, VuFind uses record driver class inheritance to load the templates, so it tries to find the most specific available match. So for example, if we're displaying a MARC record based on the discussion earlier, MARC is a child of Solr default, Solr default is a child of default record, default record is a child of abstract base. There's a fairly deep inheritance tree in record because of the way we reuse functionality. Anyway, given given the MARC record, if VuFind needs to display it in search results, it's going to look and see, do we have a MARC search result template? If so, use it. If not, let's check if we have a Solr default result template. If we do use it, no, we don't. So we fall back another level. There is the default record search result template. That's what's going to end up getting used. So again, understanding these relationships between the templates and the classes and the ability to override is useful both to find the things that you want to customize and also to override things at the right level of specificity. So enough about record drivers, let's dive a little bit deeper into the record data formatter. The record data formatter is a view helper that was introduced in VuFind 4 and its purpose is to display tables of data extracted from record drivers using a configuration driven approach. The reason that this was built is that prior to VuFind 4 many of the custom templates record driver specific templates that I described earlier were basically really long HTML tables full of labels and values.
The record data formatter is a view helper. It has a factory that builds it with default settings for VuFind. So if you look at the record data formatter factory, you will find all of the default configurations from the various places where we display tables of data. These are things like the sort of core part of the record page where we display primary data about a record areas in some of the tabs like the description tab.
The record data formatter replaces these difficult to maintain gigantic templates with configuration. Of course, with every advantage comes a disadvantage and the obvious disadvantage here is the record data formatter has a much steeper learning curve, customizing a giant HTML template is a matter of cut and paste. The act of doing it is easy. It's the maintenance that's hard and the record data formatter kind of reverses that making it harder to set things up in the first place because you have to learn more, but making the long term maintenance considerably simpler. So it's not perfect, but it's worth the investment in the long term. I think it saves many people some trouble.
So let's talk about how the record data formatter configuration works since understanding this is very important to actually making use of it. So the record data format is a view helper. It has a factory that builds it with default settings for VuFind. So if you look at the record data formatter factory, you will find all of the default configurations from the various places where we display tables of data. These are things like the sort of core part of the record page where we display primary data about a record areas in some of the tabs like the description tab.
And they were difficult to maintain and also very difficult to customize because if you wanted to change the order of two fields or add something new, you had to copy this entire huge template, customize it in your local theme. And then every time you had an upgrade to VuFind, you would have to reconcile any changes in that template with your local customizations. It was an error prone tedious process and just not very flexible. So the idea was instead of hard coding these tables into these templates, let's build a helper that takes a configuration that tells it what fields to display, how to display them, what order to display them in. And then if you want to customize something instead of copying and pasting potentially hundreds of lines of HTML and PHP, you can just adjust a configuration slightly. And thus you express your customization in a more specific and concise way, it's easier to reconcile during updates, etc. So of course that's the advantage here using the record data format or replaces these difficult to maintain gigantic templates with configuration. Of course, with every advantage comes a disadvantage and the obvious disadvantage here is the record data format has a much steeper learning curve, customizing a giant HTML template is a matter of cut and paste. The the act of doing it is easy. It's the maintenance that's hard and the record data format are kind of reverses that making it harder to set things up in the first place because you have to learn more, but making the long term maintenance considerably simpler. So it's not perfect, but it's worth the investment in the long term. I think it saves many people some trouble. So let's talk about how the record data format or configuration works since understanding this is very important to actually making use of it. So the record data format is a view helper. It has a factory that builds it with default settings for VuFind. So if you look at the record data format or factory, you will find all of the default configurations from the various places where we display tables of data. These are things like the sort of core part of the record page where we display primary data about a record areas in some of the tabs like the description tab.
Each of these areas has its own configuration established in the factory and there's a method that builds each of the default configurations which all makes it easy to extend and override and customize. The configuration of each area is just a big PHP array with various settings explaining to the helper how it needs to display all the fields. But because this is a pretty large and unwieldy data structure, we've built a record data format or spec builder class which provides some convenience methods for building this array and reorganizing it.
The most common ways of describing fields in this configuration are either just specifying there's a method on the record driver call that and display all the the values you get back from it or in a slightly more sophisticated way rendering a custom template. So call record driver method and then render this template using that data as input. So the direct method approach is useful if you just want to display a value and you don't need to do anything special with it. The template approach is more useful if you need to do special formatting or make it have outgoing links or things like that.
I should also note that if you use the custom template approach, it follows the same mechanism as other record driver related templates so you can potentially tell the record data format or to use a particular template to display a particular kind of value and then you can implement that template differently for each of your record drivers if you need to. Again, there's a great deal of flexibility here but it can get a little complicated. So as I say, given the amount of time we have for this video, I can't do too deep of a dive into all of this but I encourage you take a look at the record data format or factory code. This will show you how the record data format is set up by default and also take a look at the record data format or wiki page which explains all of the settings that can go into the PHP array to configure the record data format and also lists all of the spec builder methods that you can use to build the configuration. So for most things, you can get away with fairly simple calls to just add a method-based field or add a custom template but if you need to get deeper into it, there are some really sophisticated things you can do like building custom functions that return a map of labels to values.
If, for example, you need to call a single method of the record driver and have that lead to multiple labeled sections in your final display. You can get quite complex. But in the interest of demonstrating this while keeping things simple, I'm now going to dive into a demo of how to use the record data format and record drivers to add a custom field to VuFind.
So the use case for this demo is that we have a local note field in this example 597A which contains a note about a donor and we just want to create a donor note field in VuFind that shows the contents of this MARC field. And here is a sample record which we're going to be working with just to show what this looks like. As you can see, it's just a bare minimum MARC record with an author, a title, publisher, and a donor note.
So with that, I am going to switch over to my tutorial test machine. So this is the same test machine that we've been building on through all of the tutorial videos and for the purposes of this example, the key details of note are that we are using a local code module called tutorial and also a local custom theme called tutorial. Both of these were built in earlier videos so if you need to learn more about custom code or custom themes, just go back through the list, you'll find them. But I am just going to dive right in here and start building. I'll explain as I go.
So I'm going to go to the command line and I'm going to switch to the VuFind home directory. And I am actually going to also bring up my code editor since we'll be editing some files and running some commands.
So the first step in adding a custom field to VuFind is to index the data that we want to display in the field. Now as I mentioned, we do store the entire raw records in the index. So in theory, since this is a MARC file and the data is in a 597, I could just skip the indexing step, write some custom code that pulls the 597 out of the stored MARC record and be done with it.
But in general, I think it's a little more useful to pull the data into Solr because this allows you to search it. And it's also more generic if you potentially have the same type of data coming from multiple sources. If you can just index it into Solr from various formats, then you only need to write one set of code to pull the data from the Solr index. You don't have to worry about all the metadata specifics in the code you're writing.
So for this example, first thing we're going to do is index the 597A. So I'm just going to go to my local import MARC local properties file, going to go down to the bottom, and I'm just going to create a custom field called donor STR MV, which is a multivalued string field, taking advantage advantage of the dynamic field suffixes defined in VuFind Solr schema. I'm going to say donor string MV equals 597A. I'm going to save that file, then I'm going to go to the command line and I'm going to run import MARC.sh on my example MARC record to index it. This will take just a a moment. And there we go, our record, if an ID of donor example has been indexed into our VuFind.
So now if I go to the catalog and I just look that up by ID, here is our record. And if I look at the staff view, I can even see there's my 597A, but of course we're not displaying our donor note up here in the core metadata like we want to because we haven't done that customization yet. So let me show you how to add that. And this is going to require us to customize both record driver and record data for matter. So the first step is we need to create a custom record driver that has a method in it that can retrieve the donor information. And I'm going to use the extend class code generator, which we demonstrated in an earlier video, to build myself a new record driver. So I'm going to say PHP, public index.php, generate, extend class. And I'm going to extend the VuFind record driver Solr MARC driver into the tutorial module.
So in this instance, we know that we're only going to have this donor note in our MARC records. And we just need to extend that class so we can add a method to it. And we'll do that in the tutorial module. If in a hypothetical scenario, we had multiple types of records that all needed to have this implemented, we might have to extend multiple classes and we could perhaps create a custom trait to share this functionality between them. But for the purposes of this demo, we're doing things in the simplest possible way. I'm just extending the one class. So my generator has now set up for me in my tutorial module, this Solr MARC record driver, which has nothing in it. So I just need to add a public function here to provide the donor information so that we can get access to it from the record data for matter. So I'm just going to paste some code in here. We create public function, get donors, and this just returns this field donor STRMV. And if there's nothing there, it defaults to an empty array. So in all of the record drivers extending Solr default, there's a property called this fields. And it's just an array indexed by Solr field name. So this is how you can gain direct access to anything you put in the Solr index. So that is all I need to do there. Now, because I've created a new class, which has edited my module configuration, I do also need to remember to clear my configuration cache so that VuFind knows to load the updated configuration. So I'm just going to quickly, before I forget, delete the contents of the local cache config directory. So that will not confuse me later. Now, the next step is I want to create a custom record data for matter factory, so I can adjust the configuration of the record data for matter to display this new donor field. So first thing I'm going to do is just create an empty directory in my tutorial module for the factory. Since we haven't created anything custom related to view helpers yet, this directory doesn't exist. And I'm just going to create it at the command line here so that I don't have to create a whole bunch of nested folders in VS code, which is less efficient.
The core default record data for matter settings are found in VuFind view helper root record data for matter factory.
I'm creating an equivalent directory structure in my tutorial module where I can extend that.
We don't currently have a code generator that is particularly well suited to overriding this kind of factory.
I'm creating record data for matter factory.php in that directory that I just built.
Here is our custom factory.
For namespace, of course, we have to match our directory structure.
So it's tutorial view helper root.
We're going to make use of that spec builder helper class for setting up the configuration.
I just have a use statement to make that code accessible here.
My class definition, my local record data for matter factory extends the core record data for matter factory.
The core record data for matter factory has a method for each of the default configurations that it manages.
There's one called get default core specs, which is how we set up the core metadata.
I want to simply add a line to that.
I create a new spec builder, which is that helper class from manipulating these configurations and I initialize it using the output of the parent version of this method.
I load the existing default core specs into the spec builder.
This takes the PHP array of settings, turns it into an object with convenience methods for manipulating things.
Next, I call spec set line, which is one of the helper methods for adding something.
Set line is the simplest option.
This just says, I want to label this donor note, and I wanted to display any or all values coming from the record drivers get donor's method, which we just defined earlier.
My method returns a call to spec get array, which turns my spec builder object back into a PHP configuration array.
So we just take what we've already got by default, we add something to it, and we return it.
You can do other kinds of manipulations here. You can change the order of fields, you can remove fields you don't want, you can add fields in a whole variety of ways. But this is the simplest possible example. So now there's just one last thing we need to do. We've created this custom factory, but we need to actually register it in our theme. So because this factory is building a view helper and view helpers are specific to themes, I need to go into my custom theme and find the theme.config.php file there. And I just need to create a helper's section inside that file. So again, I'm just going to paste some code in here, add a comma, the end of our CSS there so that it parses correctly, and then paste in this helpers. So the helper section of your theme configuration is where all the view helpers are defined. Inside the helper section, there's a factories section. This is how we tell view find which factory to use to build which helper. So here we just want to say, for the existing core record data formatter class, we want to use our local custom factory, which has added a little bit of extra special logic. So all I have to do is save that and we're done. We have a configuration telling view find to build the record data formatter using a custom factory. That custom factory just adds a line to one of the default configurations for record display which makes use of a custom record driver that we just built. So we've essentially edited three files and we're now ready to test this out. So if I refresh this page and there it is. Donor note number 41 donated by Damian Katz. And that's it. So thank you for sitting through all of this background and this demo. As I say, there's a lot more that you can learn. I certainly encourage you if you're interested to examine the code, read the learning view find book and reach out with questions. If there's anything else, the community can help you with. Thanks for your time and bye for now.
This is an edited version of an automated transcript. Apologies for any errors.