A Guide to Creating a PSR-4 Autoloading WordPress Plugin (and pass WP coding standards)

How to create a PSR-4 compatible WordPress plugin

PSR (PHP Standard Recommendation) has been elusive topic for me, but ever since using it, it’s been a rather eye opening experience.

For the most part, I have been coding towards the WordPress coding standards. When I saw a PSR-4 plugin in the wild, it perplexed me as far as structure. But as I incorporated it into my projects, I suddenly started seeing some nice benefits.

Rather than diving into a standard “how I build my plugins” post, I thought I’d share how to make a simple WordPress Plugin PSR-4 while also passing WordPress Coding Standards.

Some Prerequisites

You will need the following set up and configured for this tutorial. Each of these topics are a bit complicated, so for brevity, I’m including the requirements up front. Skip to PSR-4 introduction.

You will need Composer installed

We’ll be using Composer to set up the autoloader.

You will need a local development environment

This tutorial assumes you have a local environment setup and is ready to go. I will be covering things from a Mac perspective. The following downloads for local environments support both a Mac and a Windows version.

I recommend (for Mac and Windows), the following local environments:

  • MAMP Pro – a very easy local environment that is also very flexible and powerful.
  • Local – the simplest setup for a local environment, but not as configurable as MAMP Pro.

You will need node installed

Node will be used to add any NPM scripts we’ll need. If you need to install node, you can follow the installation instructions on the Node.js website.

You will need WordPress Coding Standards Setup and Configured

Setting up WordPress Coding Standards is a rabbit hole of a task, which is why it won’t be covered here.

There are plenty of tutorials on the topic, so I’ll link to some below.

For both of the links mentioned above, you will need PHPCS (PHP Code Sniffer) installed.

Let’s begin. What is PSR-4?

PSR-4 is an opinionated autoloading technique. Since the folder structure is predictable, you no longer have to guess when it comes to using your various classes or interacting with others using the same standard.

In the example below, I have a namespace of MediaRonLLC\QuotesDLX. I can reference a class file, which will look in an Admin folder for a file named Plugin_License.php.

namespace MediaRonLLC\QuotesDLX; use MediaRonLLC\QuotesDLX\Admin\Plugin_License as License;
Code language: PHP (php)

Here’s an example setup with the php directory containing most of your backend logic.

. └── quotes-dlx.php/ └── php/ └── Admin/ └── Plugin_License.php
Code language: AsciiDoc (asciidoc)

The class structure for Plugin_License.php starts as:

/** * Perform license actions. * * @package quotes-dlx */ namespace MediaRonLLC\QuotesDLX\Admin; use MediaRonLLC\QuotesDLX\Options_License as Options; // don't load directly. if ( ! defined( 'ABSPATH' ) ) { die(); } /** * Class License Check. */ class Plugin_License {
Code language: PHP (php)

Since the folder and class structure are the same, there is no longer any guesswork as to what class or file you are interacting with.

Let’s get started with how to setup PSR-4 for a WordPress plugin.

Initializing the Plugin

In the examples in this tutorial I’m using a plugin I’ve named Power Blocks. In your local development environment, create a plugin folder named power-blocks with a base file named power-blocks.php.

. └── power-blocks/ └── power-blocks.php
Code language: AsciiDoc (asciidoc)

Within power-blocks.php, we can set the plugin header:

/** * Plugin Name: Power Blocks (sample plugin) * Plugin URI: https://github.com/DLXPlugins/power-blocks * Description: A plugin demonstrating a PSR-4 structure. * Version: 1.0.0 * Requires at least: 5.9 * Requires PHP: 7.2 * Author: DLX Plugins * Author URI: https://dlxplugins.com * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: power-blocks * Domain Path: /languages * * @package PowerBlocks */
Code language: PHP (php)

Finally, let’s set up a namespace to use for the plugin.

/** * Plugin Name: Power Blocks (sample plugin) * Plugin URI: https://github.com/DLXPlugins/power-block * Description: A plugin demonstrating a PSR-4 structure. * Version: 1.0.0 * Requires at least: 5.9 * Requires PHP: 7.2 * Author: DLX Plugins * Author URI: https://dlxplugins.com * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: power-blocks * Domain Path: /languages * * @package PowerBlocks */ namespace DLXPlugins\PowerBlocks;
Code language: PHP (php)

We now have a working plugin that can be activated. Let’s set up the autoloader next.

Setting up Composer

I have to admit that setting up Composer is not a straightforward process. But Composer is by far the easiest way to ensure your plugin has a PSR-4 structure.

Composer has an autoloader built-in, which will save you from a ton of require_once calls.

To start, navigate to your plugin directory and type in: composer init.

Composer Init - Setting up the Namespace
Composer Init – Setting up the Namespace

After running composer init, you will receive several prompts to set up your project. Composer does its best to guess what namespace you’d like to use, but in this case I want to change it to dlxplugins/power-blocks.

Entering a Custom Namespace
Entering a Custom Namespace in Composer Setup

Once you press enter, you will be asked to enter a description.

Entering a Custom Description
Entering a Custom Description

From there, you can just go through the rest of the wizard by answering a few questions, and then a composer.json file will be created in the plugin directory.

Composer Init Wizard Steps
Composer Init Wizard Steps

The resulting composer.json file will look similar to this:

{ "name": "dlxplugins/powerblocks", "description": "This is a test plugin demonstrating PSR-4 structure", "authors": [ { "name": "Ronald Huereca", "email": "ronald@mediaron.com" } ], "require": {} }
Code language: JSON / JSON with Comments (json)

Adding Autoloading to Composer

With composer initiated, we can now open up the generated composer.json file in a text editor to set up autoloading.

Let’s modify our plugin’s composer.json to have its files moved to a lib folder instead of a vendor directory. Since most composer dependencies use the vendor directory, it’s hard to exclude composer dependencies from a build. Pushing what’s needed to a lib folder allows us to include the autoloader with the plugin’s release.

{ "name": "dlxplugins/powerblocks", "description": "This is a test plugin demonstrating PSR-4 structure", "authors": [ { "name": "Ronald Huereca", "email": "ronald@mediaron.com" } ], "config": { "vendor-dir": "lib" } }
Code language: JSON / JSON with Comments (json)

And finally we’ll tell composer that the structure is PSR-4 and the autoloader should look for files in a php directory.

{ "name": "dlxplugins/powerblocks", "description": "This is a test plugin demonstrating PSR-4 structure", "authors": [ { "name": "Ronald Huereca", "email": "ronald@mediaron.com" } ], "config": { "vendor-dir": "lib" }, "autoload": { "psr-4": { "DLXPlugins\\PowerBlocks\\": "php/" } } }
Code language: JSON / JSON with Comments (json)

You’ll notice that there’s a namespace of DLXPlugins\PowerBlocks for the autoloader. This will be the PHP namespace used in the plugin.

Armed with the autoloader in composer.json, we can run composer install from the project’s directory.

Running composer install to generate autoloading files
Running composer install to generate autoloading files

The plugin should now have a directory structure as follows:

. └── power-blocks/ ├── composer.json ├── power-blocks.php └── lib/ ├── autoload.php └── composer
Code language: AsciiDoc (asciidoc)

Setting up the autoloader in the plugin

Armed with a composer autoloader, we can simply reference our autoloader in our plugin file.

/** * Plugin Name: Power Blocks (sample plugin) * Plugin URI: https://github.com/DLXPlugins/power-blocks * Description: A plugin demonstrating a PSR-4 structure. * Version: 1.0.0 * Requires at least: 5.9 * Requires PHP: 7.2 * Author: DLX Plugins * Author URI: https://dlxplugins.com * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: power-blocks * Domain Path: /languages * * @package PowerBlocks */ namespace DLXPlugins\PowerBlocks; define( 'POWER_BLOCKS_VERSION', '1.0.0' ); define( 'POWER_BLOCKS_FILE', __FILE__ ); if ( file_exists( __DIR__ . '/lib/autoload.php' ) ) { require_once __DIR__ . '/lib/autoload.php'; }
Code language: PHP (php)

Finally, we can create a base class so that you can start with the plugin initialization class.

/* [... plugin heading code and autoloader ...] */ /** * PowerBlocks class. */ class PowerBlocks { /** * Holds the class instance. * * @var PowerBlocks $instance */ private static $instance = null; /** * Return an instance of the class * * Return an instance of the PowerBlocks Class. * * @since 1.0.0 * * @return PowerBlocks class instance. */ public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Class initializer. */ public function plugins_loaded() { load_plugin_textdomain( 'power-blocks', false, basename( dirname( __FILE__ ) ) . '/languages' ); // Run plugin setup code here. add_action( 'init', array( $this, 'init' ) ); } /** * Init all the things. */ public function init() { // Run plugin init code here. } } add_action( 'plugins_loaded', function() { $power_blocks = PowerBlocks::get_instance(); $power_blocks->plugins_loaded(); } );
Code language: PHP (php)

We’ll go over how to take advantage of the autoloader shortly, but first let’s tackle an error you’re probably already seeing. WordPress coding standards do not like how we named and packaged up our main plugin file.

Passing WordPress coding standards

WP Coding Standards Error
WP Coding Standards Error

As seen in the above screenshot, WordPress coding standards do not like the PSR-4 structure we have setup. Let’s tell WordPress to ignore this requirement.

With PHPCS, you can enable custom rules per project. This involves creating a phpcs.xml.dist file and adding or subtracting any rules.

In our case, we want to hide the error that the filenames are wrong. We’ll add in a simple rule to the file to tell it to ignore the filename standard.

<rule ref="WordPress-Core"> <exclude name="WordPress.Files.FileName" /> rule>
Code language: HTML, XML (xml)

A full version of phpcs.xml.dist is shown below:

<ruleset name="Power Block Standards"> <arg name="extensions" value="php" /> <arg name="colors" /> <arg value="s" /> <rule ref="WordPress-Core"> <exclude name="WordPress.Files.FileName" /> rule> <rule ref="WordPress-Docs">rule> <config name="testVersion" value="6.0-" /> <file>.file> <exclude-pattern>/node_modules/exclude-pattern> <exclude-pattern>/vendor/exclude-pattern> <exclude-pattern>/lib/exclude-pattern> ruleset>
Code language: HTML, XML (xml)

If you refresh your IDE (I use VS Code), the errors should be gone.

We now have a folder structure like this:

. └── power-blocks/ ├── composer.json ├── phpcs.xml.dist ├── power-blocks.php └── lib/ ├── autoload.php └── composer
Code language: AsciiDoc (asciidoc)

Autoloading in action: creating a simple admin panel

Let’s go ahead and create a simple admin panel in order to demonstrate how the composer autoloader works. If you want to skip straight to the code, it is available on GitHub.

Creating the structure

Let’s start off small. Let’s create an “Admin” folder inside our base (php) folder.

. └── power-blocks/ ├── composer.json ├── phpcs.xml.dist ├── power-blocks.php ├── lib/ │ ├── autoload.php │ └── composer └── php/ └── Admin/ └── ...[files here]
Code language: AsciiDoc (asciidoc)

Let’s also create a Functions.php file and place it in the root php folder. This will be used for common functions that need to run throughout the plugin.

. └── power-blocks/ ├── composer.json ├── phpcs.xml.dist ├── power-blocks.php ├── lib/ │ ├── autoload.php │ └── composer └── php/ ├── Admin/ │ └── ...[files here] └── Functions.php

In the Admin folder, let’s also create three files called Register_Menu.php, Settings_Links.php, and Settings.php.

. └── power-blocks/ ├── composer.json ├── phpcs.xml.dist ├── power-blocks.php ├── lib/ │ ├── autoload.php │ └── composer └── php/ ├── Admin/ │ ├── Register_Menu.php │ ├── Settings_Links.php │ └── Settings.php └── Functions.php
Code language: AsciiDoc (asciidoc)
  • Register_Menu.php – For initializing the admin panel menu
  • Settings_Links.php – For linking to the admin panel from the Plugins screen
  • Settings.php – For outputting our admin panel settings

And with that, our structure is complete.

Adding common helpers to Functions.php

Within Functions.php, I’m going to add three helper methods for retrieving information about the plugin:

/** * Helper functions for the plugin. * * @package PowerBlocks */ namespace DLXPlugins\PowerBlocks; /** * Class Functions */ class Functions { /** * Return the plugin slug. * * @return string plugin slug. */ public static function get_plugin_slug() { return dirname( plugin_basename( POWER_BLOCKS_FILE ) ); } /** * Return the basefile for the plugin. * * @return string base file for the plugin. */ public static function get_plugin_file() { return plugin_basename( POWER_BLOCKS_FILE ); } /** * Return the version for the plugin. * * @return float version for the plugin. */ public static function get_plugin_version() { return POWER_BLOCKS_VERSION; } }
Code language: PHP (php)

Let’s set this file aside and move onto registering the admin menu item.

Setting up menu registration

The next file is the Register_Menu.php in the Admin folder.

/** * Initialize the admin menu. * * @package PowerBlocks */ namespace DLXPlugins\PowerBlocks\Admin; use DLXPlugins\PowerBlocks\Functions as Functions; /** * Create the admin menu. */ class Register_Menu { /** * Main class runner. */ public static function run() { add_action( 'admin_menu', array( static::class, 'init_menu' ) ); } /** * Register the plugin menu. */ public static function init_menu() { add_options_page( __( 'Power Blocks', 'power-blocks' ), __( 'Power Blocks', 'power-blocks' ), 'manage_options', Functions::get_plugin_slug(), array( '\DLXPlugins\PowerBlocks\Admin\Settings', 'settings_page' ), 100 ); } } ?>
Code language: HTML, XML (xml)

Let’s go over the highlighted portions of the code.

Usually when trying to get access to methods of another class, you have to make sure that class file is included as a require_once or similar.

use DLXPlugins\PowerBlocks\Functions as Functions;
Code language: PHP (php)

With autoloading, we can reference the Functions.php file via its namespace and the autoloader will take care of loading the right file (assuming the path is correct).

The next portion will hook into the WordPress action admin_menu and reference a static method as a callback.

add_action( 'admin_menu', array( static::class, 'init_menu' ) );
Code language: PHP (php)

The static::class placeholder tells WordPress to look in the same class for the callback method, which is the init_menu callback.

The last step in this section is to create an options menu, which will live under the Settings menu in the WordPress admin area.

Since Functions.php has already been included, we can call its static methods.

Functions::get_plugin_slug()
Code language: PHP (php)

Another way of writing the above would be:

\DLXPlugins\PowerBlocks\Functions::get_plugin_slug()
Code language: PHP (php)

When creating the menu, there must be a valid callback, which will be a method named settings_page inside a Settings class.

array( '\DLXPlugins\PowerBlocks\Admin\Settings', 'settings_page' )
Code language: PHP (php)

The long namespace settings are due to how WordPress handles callbacks, so the full path must be included (no shortcuts for now, I’m afraid).

One thing to take note is that even though the callback is pretty long, you can take one look at it and know exactly which file is being called and what method within that file is being called (in our case /php/Admin/Settings.php).

Lastly, we need to initialize this menu from the main plugin file.

/** * Class initializer. */ public function plugins_loaded() { load_plugin_textdomain( 'power-blocks', false, basename( dirname( __FILE__ ) ) . '/languages' ); // Register the admin menu. Admin\Register_Menu::run(); // Run plugin setup code here. add_action( 'init', array( $this, 'init' ) ); }
Code language: PHP (php)

The above syntax simply states, to the autoloader, to find a file called Register_Menu.php in the Admin folder and call a static method named run.

If all is well, you’ll see a placeholder for our admin menu item.

PowerBlocks Settings Menu
PowerBlocks Settings Menu

Setting up admin menu output

We’re now ready to output something to the screen when someone clicks on our admin menu item. Let’s fill in settings.php.

/** * Initialize and display admin panel output. * * @package PowerBlocks */ namespace DLXPlugins\PowerBlocks\Admin; /** * Class Functions */ class Settings { /** * Registers and outputs placeholder for settings. * * @since 1.0.0 */ public static function settings_page() { ?>
class="wrap power-blocks-plugin"> <h2>Power Blocksh2> <p>Content goes herep> div> php } }
Code language: PHP (php)

We now have a valid callback. Clicking on the menu item will now display some text to the screen.

Powerblocks Admin Output
Powerblocks Admin Output

Setting up plugin setting links

In this final section, we’ll just add some items to the settings links when someone views our plugin in the plugins screen in the WordPress admin area.

Let’s fill in Settings_Links.php:

/** * Add settings links to the plugin screen. * * @package PowerBlocks */ namespace DLXPlugins\PowerBlocks\Admin; use DLXPlugins\PowerBlocks\Functions as Functions; /** * Add settings links to the plugin screen. */ class Settings_Links { /** * Main class runner. */ public static function run() { add_filter( 'plugin_action_links_' . Functions::get_plugin_file(), array( static::class, 'add_settings_link' ) ); } /** * Add a settings link to the plugin's options. * * Add a settings link on the WordPress plugin's page. * * @since 1.0.0 * @access public * * @see run * * @param array $links Array of plugin options. * @return array $links Array of plugin options */ public static function add_settings_link( $links ) { $settings_url = admin_url( 'options-general.php?page=power-blocks' ); $docs_url = 'https://docs.dlxplugins.com/'; $site_url = 'https://dlxplugins.com'; if ( current_user_can( 'manage_options' ) ) { $options_link = sprintf( '%s', esc_url( $settings_url ), _x( 'Settings', 'Options link', 'power-blocks' ) ); array_unshift( $links, $options_link ); $docs_link = sprintf( '%s', esc_url( $docs_url ), _x( 'Docs', 'Plugin documentation', 'power-blocks' ) ); $site_link = sprintf( '%s', esc_url( $site_url ), _x( 'DLXPlugins', 'Plugin site', 'power-blocks' ) ); $links[] = $docs_link; $links[] = $site_link; } return $links; } }
Code language: PHP (php)

Once again we are referencing the Functions file and adding links to our plugin only.

We’ll have to wire this up in the main plugin file as well.

/** * Class initializer. */ public function plugins_loaded() { load_plugin_textdomain( 'power-blocks', false, basename( dirname( __FILE__ ) ) . '/languages' ); // Register the admin menu. Admin\Register_Menu::run(); Admin\Settings_Links::run(); // Run plugin setup code here. add_action( 'init', array( $this, 'init' ) ); }
Code language: PHP (php)

The output results is three links added to the plugin row:

Plugin Settings Links
Plugin Settings Links

A quick WordPress coding standards test…

If all is well, we should receive passing marks for the coding standards.

Terminal Window with PHPCS running
PHPCS Output: No news is good news

Conclusion

Setting up PSR-4 in a WordPress plugin can be straightforward, but interacting with the files and structure via WordPress can be a bit of trial-and-error.

I’ve provided several examples of PSR-4 in use by setting up a basic WordPress admin panel.

If you have any comments or questions, please do leave a comment below. Thank you so much for reading.

Ronald Huereca
By: Ronald Huereca
Published On: on September 25, 2022

Ronald Huereca founded DLX Plugins in 2022 with the goal of providing deluxe plugins available for download. Find out more about DLX Plugins, check out some tutorials, and check out our plugins.

Leave a Comment

Your email address will not be published. Required fields are marked *

Default Avatar
Choose an Avatar

Shopping Cart
  • Your cart is empty.
Scroll to Top