After a hot discussion around the plugin bootstrap file’s vision inside a Mark Jaquith tweet, I’ve decided to show my vision for the plugin bootstrap file. I agree with a lot of points, but about these, it will be later.
Before discuss I had a draft of my plugin boilerplate for WordPress.
The code of the main file is plugin-name.php
was:
<?php
// Comments for plugin.
use PluginName\Plugin;
( new Plugin() )->run();
and main class is PluginName\Plugin
was:
<?php
namespace PluginName;
use PluginName\Admin\Settings;
use PluginName\Front\Front;
class Plugin {
public function run(): void {
is_admin()
? $this->run_admin()
: $this->run_front();
}
private function run_admin(): void {
( new Settings() )->hooks();
}
private function run_front(): void {
( new Front() )->hooks();
}
}
The huge problem of the main class is hard dependencies.
To get rid of them in the PluginName\Plugin
class, we have to use the Dependency Injection Container (DIC).
Dependency Injection Container
Installing DIC and configuring it (I prefer Symfony’s components, but you can use different DIC):
composer require symfony/dependency-injection
composer require symfony/config
Create the configuration file dependencies/services.php
(but you can also use YML or XML format):
<?php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
return function ( ContainerConfigurator $configurator ) {
$services = $configurator->services();
$services->set( 'settings', 'PluginName\Admin\Settings' );
$services->set( 'front', 'PluginName\Front\Front' );
};
In simple terms, a unique slug was created for each plugin class. For PluginName\Admin\Settings
– settings
, and for PluginName\Front\Front
– front
.
In addition, in this config, you can specify objects that need to be passed to the constructor or any other method.
To create a DIC and connect a configuration file, we need to write the following code:
<?php
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
require_once __DIR__ . '/vendor/autoload.php';
$container_builder = new ContainerBuilder();
$loader = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );
$loader->load( PLUGIN_NAME_PATH . 'dependencies/services.php' );
Let’s update the bootstrap file plugin-name.php
and pass DIC to the PluginName\Plugin
constructor:
<?php
use PluginName\Plugin;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
define( 'PLUGIN_NAME_PATH', plugin_dir_path( __FILE__ ) );
require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';
$container_builder = new ContainerBuilder();
$loader = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );
$loader->load( PLUGIN_NAME_PATH . 'dependencies/services.php' );
$plugin_name = new Plugin( $container_builder );
$plugin_name->run();
We’ve got rid of all hard dependencies completely and now this object looks much better and will be easy as pie to test.
Plugin launch on the plugins_loaded action
Postpone the plugin launch until the plugins_loaded
action. To do this, create a run_plugin_name
function in which we wrap the entire plugin call and add it to the plugins_loaded
action:
<?php
use PluginName\Plugin;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
define( 'PLUGIN_NAME_PATH', plugin_dir_path( __FILE__ ) );
add_action( 'plugins_loaded', 'run_plugin_name' );
function run_plugin_name() {
require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';
$container_builder = new ContainerBuilder();
$loader = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );
$loader->load( PLUGIN_NAME_PATH . 'dependencies/services.php' );
$plugin_name = new Plugin( $container_builder );
$plugin_name->run();
}
This trick allows you to enable/disable the plugin with just one remove_action
. This is very useful for example when calling AJAX/REST API/WP CLI. Situations are different, but we make it easy to manage the plugin in the theme or other plugins code.
Plugin launch action
Let’s go a little further. And add the plugin_name_init
action after starting the plugin:
<?php
use PluginName\Plugin;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
define( 'PLUGIN_NAME_PATH', plugin_dir_path( __FILE__ ) );
function run_plugin_name() {
require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';
$container_builder = new ContainerBuilder();
$loader = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );
$loader->load( PLUGIN_NAME_PATH . 'dependencies/services.php' );
$plugin_name = new Plugin( $container_builder );
$plugin_name->run();
do_action( 'plugin_name_init', $plugin_name );
}
add_action( 'plugins_loaded', 'run_plugin_name' );
During plugin_name_init
action, we have an object of the main plugin class and hooks of all other plugin objects already fully added.
Add the getter for DIC into the main class PluginName/Plugin
:
<?php
// ...
class Plugin {
// ...
public function get_service( string $container_name ): ?object {
return $this->container_builder->get( $container_name );
}
// ...
}
As a result, we can disable absolutely any actions/filters of our plugin. For example, in the PluginName\Front\Front
class, we’ve added styles to queue:
<?php
namespace PluginName\Front;
class Front {
public function hooks(): void {
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] );
}
public function enqueue_styles() {
//...
}
}
We can disable hooks using plugin_name_init
action, get the main plugin object and get PluginName\Front\Front
object using DIC and getter for it.
function remove_plugin_name_actions( $instance ) {
$front = $instance->get_service( 'front' );
if ( ! $front ) {
return;
}
remove_action( 'wp_enqueue_scripts', [ $front, 'enqueue_styles' ] );
}
add_action( 'plugin_name_init', 'remove_plugin_name_actions' );
Great, isn’t it?
Using DIC is cool and great, but don’t forget that other plugins/themes may also use it and one fine day, you will run into version conflicts for your dependencies. Therefore, prefixes must be added for all third party packages.
Dependency prefixes
Mozart, which offered Mark Jaquith, is generally convenient, but he couldn’t cope with the prefixes for these packages, I hope someday it will learn this one. Therefore, I’ve used php-scoper. First, you need to install it:
composer require bamarni/composer-bin-plugin --dev
composer bin php-scoper config minimum-stability dev
composer bin php-scoper config prefer-stable true
composer bin php-scoper require --dev humbug/php-scoper
Create the scoper.inc.php
configuration file:
<?php
use Isolated\Symfony\Component\Finder\Finder;
return [
'prefix' => 'PluginName\\Vendor',
'whitelist-global-constants' => false,
'whitelist-global-classes' => false,
'whitelist-global-functions' => false,
'finders' => [
Finder::create()->
files()->
in(
[
'vendor/psr/container/',
'vendor/symfony/config/',
'vendor/symfony/filesystem/',
'vendor/symfony/service-contracts/',
'vendor/symfony/dependency-injection/',
]
)->
name( [ '*.php' ] ),
],
'patchers' => [
function ( string $file_path, string $prefix, string $contents ): string {
return str_replace(
'Symfony\\\\',
sprintf( '%s\\\\Symfony\\\\', addslashes( $prefix ) ),
$contents
);
},
],
];
- In the
prefix
key add the prefix for packages - In the
finders
key specify all packages for which you need to add prefixes - In the
patchers
key we write exactly how we should add prefixes using callbacks
When the command is run, prefixes will be added for all packages:
php-scoper add-prefix --output-dir dependencies/vendor/
Don’t forget to add the dependencies/vendor/
directive for autoloading to composer.json
:
// ...
"autoload": {
// ...
"classmap": [
"dependencies/vendor/"
]
},
// ...
To get php-scoper up and run quickly, add scripts to composer.json
and run them on composer install/update event:
// ...
"scripts": {
"install-scoper": [
"composer bin php-scoper config minimum-stability dev",
"composer bin php-scoper config prefer-stable true",
"composer bin php-scoper require --dev humbug/php-scoper"
],
"scoper": "php-scoper add-prefix --config .scoper.inc.php --output-dir dependencies/vendor/",
"post-install-cmd": [
"composer install-scoper",
"composer scoper",
"composer dump-autoload"
],
"post-update-cmd": [
"composer install-scoper",
"composer scoper",
"composer dump-autoload"
]
}
// ...
After that, don’t forget to replace the original packages with prefixed packages.
Results
- There aren’t Hard Dependencies in the plugin
- The plugin isn’t loaded immediately but on the
plugins_loaded
hook, which makes it easier to work with other plugins. - You can disable the plugin with a single call to
remove_action
- You can disable any plugin actions/filters using the
plugin_name_init
hook - Composer packages don’t conflict with other plugins/themes
How do you like this plugin launch?