Support for Composer Autoloading w/ Custom Fallback
Because it’s a part of my every day workflow, I spend a lot of time thinking about WordPress. And, because I think there are lots of ways that the WordPress codebase could improve from an architectural perspective, I also spend a lot of time paying attention to how other open-source PHP projects are structured. Lastly, because a lot of my day-to-day work involves custom plugin development for WordPress, I think about ways to make things better organized, more streamlined, and easier to follow along so that, when I come back to those projects months or years later, I can easily pick back up right where I left off.
There are a few things I find particularly interesting from a plugin development perspective, and those things are dependency management, plugin structure, plugin bootstrapping, and class autoloading.
I wrote a bit about how I like to approach plugin development a few months ago. At the time, I’d only really considered using Composer’s autoloader – if I was submitting a plugin to the WordPress repository or had plans to otherwise make that plugin available on sites that didn’t use a Composer-based install, my approach was to version control the vendor directory and ship it with the plugin.
In many cases, I still feel that using Composer’s autoloader is the best way to go, because it alleviates the need to write your own custom autoloader for every single project. That said, this week, I had a situation come up where I was forced to implement my own autoloader, and it gave me an idea. Let’s examine.
Here’s an example plugin bootstrap file:
<?php
/**
* Plugin Name: WP Plugin Starter
* Description: A boilerplate project to quickly scaffold up new WordPress plugins.
* Author: Jeremy Ward
* Author URI: https://jmichaelward.com
* Version: 0.1.0
*
* @package JMichaelWard\WPPluginStarter
*/
namespace JMichaelWard\WPPluginStarter;
if ( ! class_exists( 'JMichaelWard\WPPluginStarter\Plugin' ) ) {
try {
require_once plugin_dir_path( __FILE__ ) . 'src/autoload.php';
spl_autoload_register( __NAMESPACE__ . '\autoload' );
} catch ( \Throwable $e ) {
// @TODO Implement actual error handling.
return;
}
}
add_action( 'plugins_loaded', [ new Plugin(), 'run' ] );
This example plugin is shipping with a composer.json file that defines a PSR-4 style autoloader. In the bootstrap file, I run a check to see whether the main Plugin
class exists at the plugin’s namespace. If it does, great! It’s likely the plugin was installed using Composer, and that the vendor/autoload.php
file was required somewhere further upstream, either in wp-config.php
or maybe via a loader file in an mu-plugin.
It’s what happens if the class file isn’t found is what I think is interesting. In that situation, we’ll try and require a fallback autoloader class from within the plugin itself, and set up our own autoloader. Here’s what mine looks like:
/**
* Quick and dirty custom autoloader if the class files are not already available via Composer.
*
* This function ignores all non JMichaelWard\WPPluginStarter namespaced classes, then performs a require on the class
* based on the PSR-4 standard. This presumes that the class file names match the name of the class itself, and
* is following the directory structure as defined by the namespace.
*
* @param string $class_name Name of the class to autoload.
*
* @author Jeremy Ward <[email protected]>
* @since 2019-10-12
* @return void
*/
function autoload( $class_name ) {
if ( false === strpos( $class_name, __NAMESPACE__ ) ) {
return;
}
$parts = explode( '\\', $class_name );
$file_path = implode( DIRECTORY_SEPARATOR, array_splice( $parts, 2 ) );
require_once trailingslashit( __DIR__ ) . $file_path . '.php';
}
This autoloader bails early if the class name passed into it doesn’t share the same namespace. Otherwise, it explodes the class name into an array – in this case, it would be JMichaelWard
, WPPluginStarter
, and Plugin
– and then recreates the class file path after splicing out the first two values form the namespace, so we’re left with Plugin.php
as the include.
In this situation, the autoload.php
file is kept in the same src/
directory with the rest of our class files, so the autoloader’s path is relative to all of those classes.
Because we’re following the PSR-4 convention, which essentially maps file names to the directory structure, this means we can located classes in nested directories, such as JMichaelWard\WPPluginStarter\Content\ContentRegistrar.php
, which would be stored in a Content
directory within src/
. As long as engineers follow the naming convention, classes will be discovered correctly and no errors will be thrown.
The benefit of this approach is that you can quickly scaffold up a plugin following a PSR-4 styled approach, and allow others to install it either by following a Composer-based process, or just by including it in their plugins directory. The downside, of course, is if there are any third-party dependencies – this approach won’t play nice, because you’d need to refactor the autoload.php
file to check for namespaces that aren’t your own, and would have to engineer a way to locate those files.
That said, for a quick, lightweight, out-of-the-box solution, I think this one will meet a lot of needs.
You can view the full code for this example plugin on GitHub. I’m working on that repo to create a plugin starter that will ship with a Symfony Console command that will run on the composer create-project
event. When installed as a new package, the console will trigger a wizard that will ask a series of questions in order to automatically update namespaces, author details, and more, so that developers can more quickly get to the focus of their project: making the plugin do whatever it needs to actually do. That’s still in progress, so look for another post in the coming weeks.