Wordpress - Autoloading & Namespaces in WordPress Plugins & Themes: Can it Work?
Okay, I've had two big projects where I've been in control of the server enough to namespace and rely on autoloading.
First up. Autoloading is awesome. Not worrying about requires is a relatively good thing.
Here's a loader I've been using on a few projects. Checks to make sure the class is in the current namespace first, then bails if not. From there it's just some string manipulation to find the class.
<?php
spl_autoload_register(__NAMESPACE__ . '\\autoload');
function autoload($cls)
{
$cls = ltrim($cls, '\\');
if(strpos($cls, __NAMESPACE__) !== 0)
return;
$cls = str_replace(__NAMESPACE__, '', $cls);
$path = PLUGIN_PATH_PATH . 'inc' .
str_replace('\\', DIRECTORY_SEPARATOR, $cls) . '.php';
require_once($path);
}
One could easily adapt this for use without namespaces. Assuming you prefix your plugin's/theme's classes uniformly, you could just test for that prefix. Then use underscores in the class name to as placeholders for directory separators. If you're using a lot of classes, you'll likely want to use some sort of classmap autoloader.
Namespaces and Hooks
WordPress' hooks system works by using call_user_func
(and call_user_func_array
), which takes function names as strings and calls them when the do_action
(and, subsequently, call_user_func
) function call is made.
With namespaces, that means you'll need to pass fully qualified function names that include the namespace into hooks.
<?php
namespace WPSE\SomeNameSpace;
add_filter('some_filter', 'WPSE\\SomeNameSpace\\the_function');
function the_function()
{
return 'did stuff';
}
It would probably be better to make liberal use of the __NAMESPACE__
magic constant if you want to do this.
<?php
namespace WPSE\SomeNameSpace;
add_filter('some_filter', __NAMESPACE__ . '\\the_function');
function the_function()
{
return 'did stuff';
}
If you always put your hooks into classes, it's easier. The standard create instance of a class and all hooks in the constructor with $this
works fine.
<?php
namespace WPSE\SomeNameSpace;
new Plugin;
class Plugin
{
function __construct()
{
add_action('plugins_loaded', array($this, 'loaded'));
}
function loaded()
{
// this works!
}
}
If you use static methods like I want to do, you'll need to pass the fully qualified class name as the first argument of the array. That's a lot of work, so you can just use the magic __CLASS__
constant or get_class
.
<?php
namespace WPSE\SomeNameSpace;
Plugin::init();
class Plugin
{
public static function init()
{
add_action('plugins_loaded', array(__CLASS__, 'loaded'));
// OR: add_action('plugins_loaded', array(get_class(), 'loaded'));
}
public static function loaded()
{
// this works!
}
}
Using Core Classes
PHP's classname resolution is a bit wonky. If you're going to use core WP classes (WP_Widget
in the example below) you must provide use
statements.
use \WP_Widget;
class MyWidget extends WP_Widget
{
// ...
}
Or you can use the fully qualified class name -- basically just prefixing it with a backslash.
<?php
namespace WPSE\SomeNameSpace;
class MyWidget extends \WP_Widget
{
// ...
}
Defines
This is more general PHP, but it bit me, so here it is.
You may want to define things you'll use often, like the path to your plugin. Using the define statement puts things in the root namespace unless you explicitly pass the namespace into the first argument of define.
<?php
namespace WPSE\SomeNameSpace;
// root namespace
define('WPSE_63668_PATH', plugin_dir_path(__FILE__));
// in the current namespace
define(__NAMESPACE__ . '\\PATH', plugin_dir_path(__FILE__));
You can also use the const
keyword in the root level of a file with PHP 5.3 plus. consts
s are always in the current namespace, but are less flexible than a define
call.
<?php
namespace WPSE\SomeNameSpace;
// in the current namespace
const MY_CONST = 1;
// this won't work!
const MY_PATH = plugin_dir_path(__FILE__);
Please feel free to add any other tips you might have!
Here's a 2017 answer.
Autoloading is awesome. Namespacing is awesome.
Although you can roll it yourself, in 2017 it makes the most sense to use the magnificent and ubiquitous Composer to handle your PHP requirements. Composer supports both PSR-0 and PSR-4 autoloading, but the former has been deprecated since 2014, so use PSR-4. It reduces the complexity of your directories.
We keep each of our plugins/themes in its own Github repository, each with their own composer.json
file and composer.lock
file.
Here's the directory structure we use for our plugins. (We don't really have a plugin called awesome-plugin
, but we should.)
plugins/awesome-plugin/bootstrap.php
plugins/awesome-plugin/composer.json
plugins/awesome-plugin/composer.lock
plugins/awesome-plugin/awesome-plugin.php
plugins/awesome-plugin/src/*
plugins/awesome-plugin/vendor/autoload.php
plugins/awesome-plugin/vendor/*
If you provide an appropriate composer.json
file, Composer handles the name-spacing and autoloading here.
{
"name": "awesome-company/awesome-plugin",
"description": "Wordpress plugin for AwesomeCompany website, providing awesome functionality.",
"type": "wordpress-plugin",
"autoload": {
"psr-4": {
"AwesomeCompany\\Plugins\\AwesomePlugin\\": "src"
}
}
}
When you run composer install
, it creates the vendor
directory, and the vendor/autoload.php
file, which will autoload all your name-spaced files in src/
, and any other libraries you might require.
Then at the top of your main plugin-file (which for us is awesome-plugin.php
), after your plugin metadata, you simply need:
// Composer autoloading.
require_once __DIR__ . '/vendor/autoload.php';
...
Bonus Feature
Not a necessity, but we use the Bedrock Wordpress boilerplate to use Composer from the very start. Then we can use Composer to assemble the plugins we need via Composer, including your own plugin you wrote above. Additionally, thanks to WPackagist, you can require any other plugin from Wordpress.org (see example of cool-theme
and cool-plugin
below).
{
"name": "awesome-company/awesome-website",
"type": "project",
"license": "proprietary",
"description": "WordPress boilerplate with modern development tools, easier configuration, and an improved folder structure",
"config": {
"preferred-install": "dist"
},
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
},
{ // Tells Composer to look for our proprietary Awesome Plugin here.
"url": "https://github.com/awesome-company/awesome-plugin.git",
"type": "git"
}
],
"require": {
"php": ">=5.5",
"awesome-company/awesome-plugin": "dev-production", // Our plugin!
"wpackagist-plugin/cool-plugin": "dev-trunk", // Someone else' plugin
"wpackagist-theme/cool-theme": "dev-trunk", // Someone else' theme
"composer/installers": "~1.2.0", // Bedrock default
"vlucas/phpdotenv": "^2.0.1", // Bedrock default
"johnpbloch/wordpress": "4.7.5", // Bedrock default
"oscarotero/env": "^1.0", // Bedrock default
"roots/wp-password-bcrypt": "1.0.0" // Bedrock default
},
"extra": {
// This is the magic that drops packages with the correct TYPE in the correct location.
"installer-paths": {
"web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
"web/app/plugins/{$name}/": ["type:wordpress-plugin"],
"web/app/themes/{$name}/": ["type:wordpress-theme"]
},
"wordpress-install-dir": "web/wp"
},
"scripts": {
"test": [
"vendor/bin/phpcs"
]
}
}
Note 1: Comments aren't legal in JSON, but I've annotated the above file for more clarity.
Note 2: I've chopped out some bits of the boilerplate Bedrock file for brevity.
Note 3: This is why the type
field in the first composer.json
file is significant. Composer automatically drops it into the web/app/plugins
directory.
I use autoloading (as my plugin has loads of classes - partly because it includes Twig), never had an issue brought to my attention (plugin installed > 20,000 times).
If you are confident that you will never ever need to use a php installation that doesn't support namespaces then again you are fine (~70% of current wordpress blogs do not support namespaces). A few things to note:
I seem to remember that namespaces are not case sensitive in regular php but are when using fastcgi php on iis - this causes some headaches if you test on linux and don't spot a rogue lowercase letter.
Also even if you are sure that the code you are currently developing will only be used on > 5.3.0 you will not be able to reuse any code with projects that do not have that luxury - that is the main reason why I haven't used namespaces on internal projects. I have found that namespaces really don't add that much when compared with the possible headache of having to remove the dependency on them.