When VuFind 2.0 came out in 2013, it marked a big step forward for VuFind, replacing a largely homegrown application design with Zend Framework 2. Using standard components clarified many aspects of VuFind’s design and removed some significant design limitations.
A lot has changed since 2013; the Composer tool has standardized the way PHP libraries and components are installed and shared, and a series of recommendations by the PHP-FIG has led to greater interoperability between established projects. As a result, once-monolithic projects like Zend Framework have broken into smaller parts, and it has become easier to pick and choose between components in order to find the best tools for solving particular problems.
The VuFind community is now in the process of developing VuFind 7.0, and one of the significant achievements of this release will be bringing VuFind’s dependencies up to date once again, to reflect the latest changes in the field. One big part of this was the recent renaming of Zend Framework to Laminas, though this change has proven to be more cosmetic than functionally significant. Another major development is the abandonment of the laminas-console (formerly zend-console) package; this has required significant work to accommodate, as will be detailed below.
Strengths and Weaknesses of laminas-console
When VuFind first adopted Zend Framework, it needed a method for developing command line utilities. At that point in time, there was really only one logical choice: the zend-console functionality built into the monolithic framework.
The zend-console approach to building command-line tools was designed to be similar to the framework’s general model-view-controller architecture. Commands were designed as actions within controller classes, and they could be run through the same public/index.php file that was also used for rendering web results — the code simply detected whether it was running in a command line or web context and ran appropriate code accordingly. This made it very easy to switch between developing web code and console code, and all of the standard VuFind resources could be accessed in exactly the same way.
The zend-console/laminas-console component also provided abstractions for input and output (allowing command line tools to behave the same across operating systems without requiring programmers to worry about the details) and used the framework’s routing logic to automate some of the work of processing command line arguments and options. All of this made code easier to read and write.
However, zend-console/laminas-console had some significant weaknesses, and these became more apparent as time went on. The use of controller classes led to unrelated actions being grouped together into the same files (because it’s easier to add an action to an existing controller than to build a whole new controller), resulting in bloated and hard-to-manage classes. The widespread use of static methods like Console::isConsole() and Console::write() made code difficult or impossible to test with PHPUnit, since using test doubles was not convenient. The command line argument/option processing lacked richness and did very little to enforce consistency of terminology between actions.
While we could certainly have continued to live with the drawbacks of laminas-console, the previously-mentioned developments in the PHP community mean that we no longer have to. Just as Zend Framework eventually evolved into a set of independent Laminas components, so did the competing Symfony framework split into smaller, reusable parts. While I continue to feel that Laminas is a good fit for VuFind’s overall use case, we are now in the position to take Symfony’s superior console support and incorporate that into VuFind without losing the other benefits of Laminas throughout the rest of the application. I applaud the Laminas development team for recognizing their own strengths and weaknesses, and making the wise decision to deprecate laminas-console; now, VuFind and other projects can take advantage of a more mature and feature-rich console component, and the Laminas team can focus more effort on their areas of strength.
Strengths of symfony-console
Symfony’s console component shares most of the existing strengths of laminas-console while also overcoming most of its weaknesses. It, too, provides abstractions of input and output and a mechanism for managing command line arguments and options… it just does it all a little bit better than laminas-console did. Rather than using a “controller and action” design, each Symfony console command is a single class, which makes the code organization cleaner and fits better with VuFind’s leaning toward explicit dependency-injection. Rather than relying on an external router configuration, the argument and option configuration is handled internally by the command classes. Argument and option configuration is also richer and more readable (if also a bit more verbose) than the laminas-console equivalent, with automatic, built-in support for consistent and attractive help screens. Symfony’s console component also ships with tools that make testing commands through PHPUnit extremely simple.
The bottom line: migrating from laminas-console to symfony-console offered an opportunity to not just update some underlying components but also to improve the design, user interface and test coverage of every command line tool in VuFind, all of which will contribute to easier maintenance and better user satisfaction in the future.
Migration Goals and Challenges
VuFind always aims to maintain backward compatibility to the greatest degree possible. Obviously, there is no way to avoid the fact that code implementing command line tools needs to be significantly rewritten… but just because code internals change, there is no reason for external interfaces to be redesigned; this migration should not, for example, break people’s existing cron jobs that depend on command line tools.
VuFind already supports two different methods for running some of its command-line tools: there is the “Laminas way” of calling public/index.php from the command line with controller and action parameters, but there are also some stand-alone PHP scripts (which are simply wrappers around public/index.php) that were created for backward-compatibility with tools developed as part of VuFind 1.x. The migration to Symfony console was designed with the goal of keeping all of these things working.
To make a long story short, the project was a success, and all of the work can be seen in pull request #1571. There were a few noteworthy challenges encountered along the way, however….
The first challenge was figuring out how to meld together the Symfony and Laminas ways of doing things. Since VuFind is largely Laminas-driven, we wanted to be sure that normal Laminas bootstrapping took place so that command line tools could be coded consistently with web-based tools, as in the past. However, Symfony offered some minor wrinkles that differed from past practice — most significantly, the fact that every command needed to have a single name, rather than the two-part controller and action convention used by laminas-console.
While it might have been possible to create a separate entry point to the Symfony console code, it seemed to make sense to continue to run command line tools through public/index.php as in the past. This not only helps with backward compatibility, but also allows general framework bootstrapping code to exist in just one place. The main difference is that, rather than being able to allow Laminas to internally detect console mode and behave differently, we instead need to run an entirely different bootstrap process in command line mode. The VuFindConsole\ConsoleRunner class was introduced to do this new work. It works together with VuFindConsole\Command\PluginManager to neatly fit Symfony console commands into the Laminas environment. The single-name vs. two-part name problem was solved by giving the Symfony commands slash-separated names; it’s illegal to put spaces in a Symfony command name, but slash is allowed. Thus, a “controller action”-style laminas-console command like “language normalize” becomes “language/normalize” in Symfony. VuFindConsole\Command\PluginManager is a standard Laminas plugin manager which allows Symfony commands to be built using standard Laminas factories, and which allows the commands to be looked up using their names as aliases. The VuFindConsole\ConsoleRunner does a bit of clever manipulation on the incoming command line parameters to normalize old-style “controller action” requests into new-style “controller/action” requests, then pulls the appropriate command out of the PluginManager and runs it. (If no command is specified, it loads ALL of the commands in the plugin manager so it can display a comprehensive help message). End result: seamless integration between Laminas and Symfony.
Once this framework was put in place, the rest of the work was simply a matter of converting controllers into command classes and writing tests (which brings the VuFindConsole module from having no test coverage at all, to having a very high degree of coverage). Everything that laminas-console could do, Symfony console could do as well or better. A few details of option processing occasionally required some searching and trial-and-error to figure out (for example, dealing with options that accept optional values was not immediately intuitive), but the documentation is good and the patterns are consistent, so the work went quite smoothly.
If there is any disadvantage at all to moving from controllers to stand-alone command classes, it is that shared convenience/utility methods are less readily available. However, this was really not much of a problem during the migration process. In the few cases where shared functionality was needed, it could be made available using either inheritance or traits. Many of the existing support methods in the controllers were used to fetch dependencies on the fly, a bad habit that was made unnecessary by switching to factory-driven dependency injection when refactoring the code.
As an added bonus, it was even possible to reimplement VuFind’s install.php script as a Symfony command, moving what was previously a completely stand-alone PHP script into a more object-oriented space (and bringing with it the benefits of testing and style normalization).
Migrating Custom Code
If you have built any custom command-line tools in your local module, you will have to migrate these to Symfony when you upgrade to VuFind 7.0. Fortunately, by looking at VuFind’s many existing commands and by reading Symfony’s excellent documentation, you should be able to do this with relative ease. By the time 7.0 is released, the plugin generator should be able to help you with the creation of new commands; watch pull request #1573 for progress on that detail. In the meantime, you can always reach out for support as needed.