Autoloading, coding standards and file structure in WordPress plugin development

{{< toc >}}

In the last article, I explained the use of namespaces, which we need today to establish autoloading, which simultaneously defines our file and folder structure.

Declare classes in their own PHP files

To keep our code clear, we distribute the code sensibly across several PHP files. Each class should be declared in its own file. This file must then be loaded into the main code (e.g. via require_once()) before the class can be instantiated.

Let’s assume the following folder structure of an example plugin:

1my-plugin
2├── class-01.php
3├── class-02.php
4└── my-plugin.php
5Content class-01.php:
1<?php
2class class01
3{
4 public function __construct()
5 {
6 echo 'class01 instantiated.';
7 }
8}

If we now want to instantiate the class01 in the plugin file my-plugin.php, we must first include the file and then we can instantiate the class:

1<?php
2/**
3 * Plugin Name: MyPlugin
4 */
5 
6require_once __DIR__ . '/class-01.php';
7 
8$klasse01 = new class01(

If we also need class02 in this script, we also have to include the corresponding file first and then we can instantiate the class:

1<?php
2/**
3 * Plugin Name: MyPlugin
4 */
5 
6require_once __DIR__ . '/class-01.php';
7require_once __DIR__ . '/class-02.php';
8 
9$klasse01 = new class01();
10$klasse02 = new class02();

This can quickly become confusing even with a small number of classes and lead to long lists of require_once().

Now a requirement for our plugin changes and we have to introduce a service that must be used by class02, for example.

We add our service as a new class in the service-01.php file:

1my-plugin
2├── class-01.php
3├── class-02.php
4├── my-plugin.php
5└── service-01.php
1<?php
2class service01
3{
4 public function __construct()
5 {
6 echo 'service01 instantiated.';
7 }
8}

And change class02:

1<?php
2class class01
3{
4 protected service01 $service01;
5 
6 public function __construct(service01 $service01)
7 {
8 $this->service01 = $service01;
9 
10 echo 'class02 and service01 instantiated.';
11 }
12}

Now we have to include the file service-01.png in the main file of the plugin:

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6require_once __DIR__ . '/class-01.php';
7require_once __DIR__ . '/class-02.php';
8require_once __DIR__ . '/service-01.php';
9 
10$service01 = new service01();
11 
12$klasse01 = new class01();
13$klasse02 = new class02($service01);

And at this point, at the latest, it becomes confusing. First, there is a long list of require_once(), and with each new class we need, this list becomes even longer. Moreover, our folder structure no longer looks very clear. In the next step, an inc folder could be introduced, containing all the files we want to include using require_once():

1my-plugin
2├── inc
3│ ├── class-01.php
4│ ├── class-02.php
5│ └── servie-01.php
6└── my-plugin.php

In principle, there is nothing wrong with such a setup at this stage. However, as the plugin becomes more extensive and we have different classes like models, services, and classes with hooks, we also need a folder structure that organizes all the new files in a sensible and clear manner.

In addition, we want to move away from the very long list of require_once() calls.

Preparing for Autoloading and code standards

Autoloading refers to the automatic loading of required files, and to reliably use this, we need to equip our existing code with namespaces and follow certain coding standards. Coding standards affect both the code itself and the folder structure we will use. These standards help bring some order to the chaos that PHP generally allows. There are standards for indenting code (the eternal war: tabs or spaces?), naming variables, functions, classes, and methods, etc.

Why are standards important?

Coding standards lead to more clarity, and they generally provide a schema for how something should look so that developers don’t have to worry about it, and so that everyone can quickly and easily find their way into unfamiliar code, especially in a team.

Coding standards

Because it would be too easy otherwise, there are different standards. 😅 Coding standards are defined by different sources. There are standards set by WordPress itself, standards set by the PHP Framework Interoperability Group (PHP-FIG), and individual companies and agencies can also define their own standards, of course.

At this point, we are primarily interested in two definitions:

So which standards should we follow?

The answer you won’t like: it depends.

It depends on your personal taste, and there is no right or wrong here. You can also define your own standards (which I do not recommend). The important thing is to actually stick to one thing.

Which standards will we follow in this series?

I don’t want to dictate which standards you should follow, but I stick to the PSR, as they are also followed by other PHP frameworks like Symfony and Laravel. If you rely on PSR, you are not trapped in the WordPress universe and can seamlessly work on Symfony or Laravel projects as well. Opinions differ here, though; there are numerous advocates of the WordPress standard when programming for WordPress. If you ever want to become a core developer, you should definitely be aware that different coding standards exist.

Please take the time, if you are not familiar with PSR, to review the following PSRs:

  • PSR-1: Basic Coding Standard
  • PSR-2: Coding Style Guide
  • PSR-12: Extended Coding Style

IMPORTANT:

PHP files that contain either pure PHP code or end with a PHP code block must not contain a closing PHP tag (?>) (see PSR-2, 2.2 Files)! This can especially lead to a “Headers already sent” error in plugin development if whitespace or a blank line follows the ?>.

Standard for folder structure

Since we have to follow the namespaces in folder naming for autoloading, I don’t want to define fixed standards for the folder structure. This is ultimately more a matter of taste, especially in WordPress plugin development. Frameworks, however, are much stricter.

For later (and also for PSR-4 autoloading, see below), it is important that all PHP files, except for the main plugin file, are located within a src folder. This becomes especially important when we get to unit tests, as the actual source code is in src and all associated tests are in tests. But also for autoloading, as we will see shortly, this is important.

Our previous example code is now standardized

To establish autoloading well, we now fully align our code with the standards and introduce namespaces. We make the following changes:

  • Class names are written in StudlyCaps, so Class01 instead of class01
  • The file names are identical to the class names, so Class01.php instead of class-01.php
  • All files in src have namespaces, with the base namespace MK\MyPlugin following the Manufacturer\Plugin pattern, and our classes 1 and 2 are in the Main sub-namespace, while the service is in Service.

Folder structure

We now establish the folder structure that follows the naming of the namespaces:

1my-plugin
2├── src
3│ ├── Main
4│ │ ├── Class01.php
5│ │ └── Class02.php
6│ └── Service
7│ └── Service01.php
8└── my-plugin.php

The files

src/Main/Class01.php

1<?php
2 
3namespace MK\MyPlugin\Main;
4 
5class Class01
6{
7 public function __construct()
8 {
9 echo 'Class01 instantiated.';
10 }
11}

src/Main/Class02.php

1<?php
2 
3namespace MK\MyPlugin\Main;
4 
5use MK\MyPlugin\Service\Service01;
6 
7class Class02
8{
9 protected Service01 $service01;
10 
11 public function __construct(Service01 $service01)
12 {
13 $this->$service01 = $service01;
14 
15 echo 'Class02 instantiated.';
16 }
17}

src/Service/Service01.php

1<?php
2 
3namespace MK\MyPlugin\Main;
4 
5class Service01
6{
7 public function __construct()
8 {
9 echo 'Service01 instantiated.';
10 }
11}

my-plugin.php

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6require_once __DIR__ . '/src/Main/Class01.php';
7require_once __DIR__ . '/src/Main/Class02.php';
8require_once __DIR__ . '/src/Service/Service01.php';
9 
10use MK\MyPlugin\Main\Class01;
11use MK\MyPlugin\Main\Class02;
12use MK\MyPlugin\Main\Service01;
13 
14$service01 = new Service01();
15 
16$class01 = new Class01();
17$class02 = new Class02($service01);

Using autoloading in WordPress plugins

Basically, there are two ways to use autoloading:

  • Self-configured autoloading via spl_autoload_register()
  • PSR-4 autoloading via Composer

I generally use autoloading via Composer and highly recommend it. However, to cover the most important things here, I also deal with spl_autoload_register().

Autoloading with spl_autoload_register()

With the PHP function spl_autoload_register(), we can implement our own autoloading. To do this, let’s look at the function call:

1spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool

So, the function expects at least one callback function, and this function is given the name of the class to be loaded:

1callback(string $class): void

We can now comment out the require_once() calls in our main file my-plugin.php and insert spl_autoload_register() for testing, using an anonymous function as the callback:

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6// require_once __DIR__ . '/src/Main/Class01.php';
7// require_once __DIR__ . '/src/Main/Class02.php';
8// require_once __DIR__ . '/src/Service/Service01.php';
9 
10use MK\MyPlugin\Main\Class01;
11use MK\MyPlugin\Main\Class02;
12use MK\MyPlugin\Main\Service01;
13 
14spl_autoload_register(function(string $className) {
15 var_dump($className);
16});
17 
18$service01 = new Service01();
19 
20$class01 = new Class01();
21$class02 = new Class02($service01);

By disabling the require_once() calls and integrating spl_autoload_register(), PHP tries to get the appropriate PHP file via the autoloader when calling new Service01(). To do this, PHP passes the fully qualified class name to the callback function that we passed to spl_autoload_register().

All our callback does at the moment is to output the class name using var_dump(). So our above code generates the following message:

1string(29) "MK\MyPlugin\Service\Service01"
2Fatal error: Uncaught Error: Class "MK\MyPlugin\Service\Service01" not found in
3[(...)/wp-content/plugins]/autoload-plugin-02/autoload-plugin-01.php:18
4Stack trace: #0 (...)/wp-settings.php(453): include_once() #1 (...)/wp-config.php(93):
5require_once '...' #2 (...)/wp-load.php(50): require_once '...' #3 (...)/wp-admin/admin.php(34):
6require_once '...' #4 (...)/wp-admin/plugins.php(10): require_once '...' #5 {main}
7thrown in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php on line 18

In this code, I have replaced my local paths with (…) and added manual line breaks to avoid making the message too long.

First, the output of var_dump() appears:

1string(29) "MK\MyPlugin\Service\Service01"

This is the fully qualified name of the class that is instantiated first.

Then follows the Fatal Error, as the class could not be found. We now need to ensure in our callback that the appropriate file is loaded.

Since we have made it very easy for ourselves with the namespaces and the corresponding folder structure, we can get the filename with a few simple adjustments to the fully qualified class name:

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6// require_once __DIR__ . '/src/Main/Class01.php';
7// require_once __DIR__ . '/src/Main/Class02.php';
8// require_once __DIR__ . '/src/Service/Service01.php';
9 
10use MK\MyPlugin\Main\Class01;
11use MK\MyPlugin\Main\Class02;
12use MK\MyPlugin\Main\Service01;
13 
14spl_autoload_register(function(string $className) {
15 // MKMyPlugin vom Klassennamen durch den Pfad zu src ersetzen:
16 $className = str_replace('MK\\MyPlugin\\', __DIR__ . '/src/', $className);
17 
18 // Die restlichen Backslashes durch Verzeichnis-Trenner (Slashes) ersetzen und .php anhängen
19 $classFile = str_replace('\\', '/', $className) . '.php';
20 
21 // Klassen-Datei laden
22 require_once $classFile;
23});
24 
25$service01 = new Service01();
26 
27$class01 = new Class01();
28$class02 = new Class02($service01);

In the example of the Service01 class, the class name is MK\MyPlugin\Service\Service01. The part MK\MyPlugin can be seen as an alias to the src directory of our plugin.

So, to get the path for the class file from this, in the first step we only need to replace the part MK\MyPlugin with __DIR__ . '/src/. Afterward, we exchange all backslashes (\) for the directory separator (slash, /) and add the extension .php.

So, MK\MyPlugin\Service\Service01 becomes (…)/wp-content/plugins/my-plugin/src/Service/Service01.php, which exactly matches our file. The part (…) corresponds to your local path to the WordPress installation.

With the above code, our plugin should now automatically reload all class files. So now we just need to remove our redundant, commented out code:

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6use MK\MyPlugin\Main\Class01;
7use MK\MyPlugin\Main\Class02;
8use MK\MyPlugin\Main\Service01;
9 
10spl_autoload_register(function(string $className) {
11 // Replace MK\MyPlugin in the class name with the path to src:
12 $className = str_replace('MK\\MyPlugin\\', __DIR__ . '/src/', $className);
13 
14 // Replace the remaining backslashes with directory separators (slashes) and append .php
15 $classFile = str_replace('\\', '/', $className) . '.php';
16 
17 // Load class file
18 require_once $classFile;
19});
20 
21$service01 = new Service01();
22 
23$class01 = new Class01();
24$class02 = new Class02($service01);

However, if we now implement this code and reload the page in our WordPress installation, we will be confronted with a new error:

1Warning: require_once(WP_Site_Health.php): Failed to open stream: No such file or directory
2in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php on line 27
3
4Fatal error: Uncaught Error: Failed opening required 'WP_Site_Health.php' (include_path='.:/usr/share/php:/www/wp-content/pear')
5in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php:27
6Stack trace: #0 [internal function]: {closure}('WP_Site_Health')
7#1 (...)/wp-settings.php(604): class_exists('WP_Site_Health') #2 (...)/wp-config.php(93):
8require_once('...') #3 (...)/wp-load.php(50): require_once('...')
9#4 (...)/wp-admin/admin.php(34): require_once('...') #5 (...)/wp-admin/plugins.php(10):
10require_once('...') #6 {main} thrown in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php on line 27

What’s happening here?

PHP tries to load the WordPress core class WP_Site_Health, from which our callback code makes the file WP_Site_Health.php, and of course, this cannot be found.

The autoloader callback that we register with spl_autoload_register() is declared in the global namespace. So, we need to ensure that we only process class names that are actually present in our namespace, as PHP uses the callback for every loaded class that is loaded after registration. This includes WordPress core classes.

To ensure that only class names in our namespace are handled in our autoloader, we add the following check:

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6use MK\MyPlugin\Main\Class01;
7use MK\MyPlugin\Main\Class02;
8use MK\MyPlugin\Main\Service01;
9 
10spl_autoload_register(function(string $className) {
11 if (false === strpos($className, 'MK\\MyPlugin')) {
12 return;
13 }
14 
15 // Replace MK\MyPlugin in the class name with the path to src:
16 $className = str_replace('MK\\MyPlugin\\', __DIR__ . '/src/', $className);
17 
18 // Replace the remaining backslashes with directory separators
19 $classFile = str_replace('\\', '/', $className) . '.php';
20 
21 // Load class file
22 require_once $classFile;
23});
24 
25$service01 = new Service01();
26 
27$class01 = new Class01();
28$class02 = new Class02($service01);

With if (false === strpos($className, 'MK\MyPlugin')), we check if the beginning of our namespace is present in the class name. If not (strpos returns false), we terminate the callback with return.

Now we have a working autoloader that only handles the class names of our plugin.

Autoloading with Composer

The manual transformation of the class name in the callback of spl_autoload_register() and the exception handling of class names outside the namespace of the plugin are both cumbersome and prone to errors.

In terms of code reusability, this version of autoloading doesn’t score well, as we always have to manually adjust the namespace in at least two places.

It would be much nicer if we could use an established solution here. That’s where Composer comes in. Composer is a cross-platform dependency management tool for PHP that helps developers easily manage and automatically download required libraries and packages for their projects. At the same time, Composer also offers a PSR-4-based autoloading that we want to use for our plugin from now on.

Setting up Composer

To use Composer in the plugin, we first need to set up Composer. Composer must already be installed on your device; you can follow this tutorial to install it.

Once Composer is installed, please open a terminal (I recommend Warp if you’re on a Mac, or hyper.js otherwise) and switch to your plugin directory.

Then enter the following command: composer init. The Composer config generator will guide you through the setup. In most cases, you can use the default settings.

Since we don’t want to define any dependencies yet, you can answer the questions “Would you like to define your dependencies (require) interactively” and “Would you like to define your dev dependencies (require-dev) interactively” with no.

Then comes the question we need:

Add PSR-4 autoload mapping? Maps namespace "Marcuskober\MyPlugin" to the entered relative path. [src/, n to skip]:

Here, Composer automatically generates a namespace from the initially specified package name, which by default consists of the name you chose when setting up Composer and the directory name. We confirm the selection here with enter to choose the name and the suggested src directory.

The setup must then be confirmed with a yes.

Composer now creates a vendor directory for your plugin and the config file composer.json. The vendor directory already contains the files that Composer needs for autoloading, and packages will be placed in this directory later if you install them via Composer.

My config file now looks like this:

1{
2 "name": "marcuskober/my-plugin",
3 "autoload": {
4 "psr-4": {
5 "Marcuskober\\MyPlugin\\": "src/"
6 }
7 },
8 "authors": [
9 {
10 "name": "Marcus Kober",
11 "email": "marcus.kober@gmail.com"
12 }
13 ]
14}

In my case, I need to correct the namespace under "psr-4” so that it matches our chosen namespace. Depending on your choice of name, directory, and namespaces, you may also need to do this.

So, we’ll change this (from Marcuskober to MK) accordingly:

1"psr-4": {
2 "MK\\MyPlugin\\": "src/"
3}

After changing the namespace in composer.json, we need to inform Composer to adjust its autoload files accordingly. To do this, we enter the following command in the terminal: composer dump-autoload. If Composer returns Generated autoload files in the terminal, we have correctly set up Composer.

Using composer autoload in our plugin

To use Composer autoloading in our plugin, all we need to do is remove the require_once() calls and load the file vendor/autoload.php:

1<?php
2/**
3* Plugin Name: MyPlugin
4*/
5 
6use MK\MyPlugin\Main\Class01;
7use MK\MyPlugin\Main\Class02;
8use MK\MyPlugin\Main\Service01;
9 
10require __DIR__ . '/vendor/autoload.php';
11 
12$service01 = new Service01();
13 
14$class01 = new Class01();
15$class02 = new Class02($service01);

And simply by adding the line require __DIR__ . '/vendor/autoload.php';, the autoloading with Composer works directly out of the box!

Conclusion and Outlook

As you can see, autoloading with Composer is not rocket science and, unlike spl_autoload_register(), is quickly and easily implemented.

Once your plugins have reached a certain size, I recommend using autoloading with Composer. Even though you can freely adjust the settings, autoloading still forces you to use clean class naming, a logical folder structure, and the use of a single src directory for your PHP files. And believe me, the constraint is purely positive at this point.

In the upcoming articles in this series, we will delve deeper into the development and structuring of complex plugins, and the code will become more tangible as we will see usable code after these rather theoretical first articles!

In the next article, we will focus on hooks and their registration from your plugin.