The creation of a WordPress plugin extensible with the help of PHP classes (r)
-sidebar-toc> -language-notice>
- In installing hooks (actions and filters) for extensions plugins that inject their own functionality
- By offering PHP classes from which extension plugins can inherit
The first method relies more on documentation, detailing available hooks and their usage. The second approach, by contrast, has ready-to-use code that can be extended to improve functionality, eliminating the requirement to provide a detailed document. This is advantageous because the writing of documentation as well as the code can hinder the management of the plugins and distribution.
Let's look at a few strategies to accomplish this however, with the aim to build an ecosystem of integrations built around the WordPress plugin.
Define the basic PHP classes for the WordPress plugin
Let's take a look at how this can be implemented in the open source Gato GraphQL plugin.
AbstractPlugin class:
AbstractPlugin
can be described as a plug-in compatible with Gato GraphQL plugin and its extensions:
abstract class AbstractPlugin implements PluginInterface protected string $pluginBaseName; protected string $pluginSlug; protected string $pluginName; public function __construct( protected string $pluginFile, protected string $pluginVersion, ?string $pluginName, ) $this->pluginBaseName = plugin_basename($pluginFile); $this->pluginSlug = dirname($this->pluginBaseName); $this->pluginName = $pluginName ? ? $this->pluginBaseName; public function getPluginName(): string return $this->pluginName; public function getPluginBaseName(): string return $this->pluginBaseName; public function getPluginSlug(): string return $this->pluginSlug; public function getPluginFile(): string return $this->pluginFile; public function getPluginVersion(): string return $this->pluginVersion; public function getPluginDir(): string return dirname($this->pluginFile); public function getPluginURL(): string return plugin_dir_url($this->pluginFile); // ...
AbstractMainPlugin class:
AbstractMainPlugin
expands AbstractPlugin
to be able to represent the plugin's main features:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface public function __construct( string $pluginFile, string $pluginVersion, ?string $pluginName, protected MainPluginInitializationConfigurationInterface $pluginInitializationConfiguration, ) parent::__construct( $pluginFile, $pluginVersion, $pluginName, ); // ...
AbstractExtension class:
In the same way, AbstractExtension
is able to expand AbstractPlugin
to make it an extension plugin:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface public function __construct( string $pluginFile, string $pluginVersion, ?string $pluginName, protected ?ExtensionInitializationConfigurationInterface $extensionInitializationConfiguration, ) parent::__construct( $pluginFile, $pluginVersion, $pluginName, ); // ...
It is important to note it is the case that AbstractExtension
is an integral part of the plugin. It allows you to sign up and begin an extension. However, it's only used by extensions, not by the plugin in general.
AbstractPlugin is one of the AbstractPlugin classes. AbstractPlugin
class which includes the shared initialization method that is invoked at different times. These procedures are established at the ancestor level but are also invoked by inheriting classes according to their lifespan.
The primary plugin and its extensions begin by running the initialization
procedure in the class that is invoked inside the base WordPress plugin's code.
For instance, in Gato GraphQL, this is performed by gatographql.php
:
$pluginFile = __FILE__; $pluginVersion = '2.4.0'; $pluginName = __('Gato GraphQL', 'gatographql'); PluginApp::getMainPluginManager()->register(new Plugin( $pluginFile, $pluginVersion, $pluginName ))->setup();
Setup method
In the case of the parent, the configuration
includes the logic common between the extension and the plugin, for example, unregistering them after the plugin is removed. The method does not have to be a final one and may be altered by inheriting classes in order to enhance their capabilities:
abstract class AbstractPlugin implements PluginInterface // ... public function setup(): void register_deactivation_hook( $this->getPluginFile(), $this->deactivate(...) ); public function deactivate(): void $this->removePluginVersion(); private function removePluginVersion(): void $pluginVersions = get_option('gatographql-plugin-versions', []); unset($pluginVersions[$this->pluginBaseName]); update_option('gatographql-plugin-versions', $pluginVersions);
Method of setting up the main plugin:
The plugin's primary setup
method initiates the entire process of establishing the plugin. Its primary method for executing its function through processes such as the initialization
, configureComponents
, configure
, and start
and also activates action hooks for extensions.
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface public function setup(): void parent::setup(); add_action('plugins_loaded', function (): void // 1. Initialize main plugin $this->initialize(); // 2. Initialize extensions do_action('gatographql:initializeExtension'); // 3. Configure main plugin components $this->configureComponents(); // 4. Configure extension components do_action('gatographql:configureExtensionComponents'); // 5. Configure main plugin $this->configure(); // 6. Configure extension do_action('gatographql:configureExtension'); // 7. Boot main plugin $this->boot(); // 8. Boot extension do_action('gatographql:bootExtension'); // ... // ...
Extension setup procedure
AbstractExtension class AbstractExtension
class implements its logic on the corresponding hooks:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface // ... final public function setup(): void parent::setup(); add_action('plugins_loaded', function (): void // 2. Initialize extensions add_action( 'gatographql:initializeExtension', $this->initialize(...) ); // 4. Configure extension components add_action( 'gatographql:configureExtensionComponents', $this->configureComponents(...) ); // 6. Configure extension add_action( 'gatographql:configureExtension', $this->configure(...) ); // 8. Boot extension add_action( 'gatographql:bootExtension', $this->boot(...) ); , 20);
Methods to start
, configureComponents
, configure
, and begin
are common to the main plugin along with extensions, and could share the same reasoning. The logic used by these techniques is contained in the AbstractPlugin
class.
For example, the configure
method configures the plugin or extensions, calling callPluginInitializationConfiguration
, which has different implementations for the main plugin and extensions and is defined as abstract and getModuleClassConfiguration
, which provides a default behavior but can be overridden if needed:
abstract class AbstractPlugin implements PluginInterface // ... public function configure(): void $this->callPluginInitializationConfiguration(); $appLoader = App::getAppLoader(); $appLoader->addModuleClassConfiguration($this->getModuleClassConfiguration()); abstract protected function callPluginInitializationConfiguration(): void; /** * @return array,mixed> [key]: Module class, [value]: Configuration */ public function getModuleClassConfiguration(): array return [];
The main plugin provides its implementation for callPluginInitializationConfiguration
:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface // ... protected function callPluginInitializationConfiguration(): void $this->pluginInitializationConfiguration->initialize();
The extension class also, offers its own implementation
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface // ... protected function callPluginInitializationConfiguration(): void $this->extensionInitializationConfiguration?->initialize();
Methods that initiate
, configureComponents
and the start
are decided by the parent. They are able to be modified by inheriting classes
abstract class AbstractPlugin implements PluginInterface // ... public function initialize(): void $moduleClasses = $this->getModuleClassesToInitialize(); App::getAppLoader()->addModuleClassesToInitialize($moduleClasses); /** * @return array> List of `Module` class to initialize */ abstract protected function getModuleClassesToInitialize(): array; public function configureComponents(): void $classNamespace = ClassHelpers::getClassPSR4Namespace(get_called_class()); $moduleClass = $classNamespace . '\\Module'; App::getModule($moduleClass)->setPluginFolder(dirname($this->pluginFile)); public function boot(): void // By default, do nothing
Each method can be modified with AbstractMainPlugin
or AbstractExtension
to extend them with functions of your choosing.
The primary plugin's configuration
method also removes the cache of WordPress instances when the plugin or one extension is activated or deactivated:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface public function setup(): void parent::setup(); // ... // Main-plugin specific methods add_action( 'activate_plugin', function (string $pluginFile): void $this->maybeRegenerateContainerWhenPluginActivatedOrDeactivated($pluginFile); ); add_action( 'deactivate_plugin', function (string $pluginFile): void $this->maybeRegenerateContainerWhenPluginActivatedOrDeactivated($pluginFile); ); public function maybeRegenerateContainerWhenPluginActivatedOrDeactivated(string $pluginFile): void // Removed code for simplicity // ...
In the same way, the deactivate
method eliminates cache and boots
performs other actions hooks, which are unique to the plugin, but only for:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface public function deactivate(): void parent::deactivate(); $this->removeTimestamps(); protected function removeTimestamps(): void $userSettingsManager = UserSettingsManagerFacade::getInstance(); $userSettingsManager->removeTimestamps(); public function boot(): void parent::boot(); add_filter( 'admin_body_class', function (string $classes): string $extensions = PluginApp::getExtensionManager()->getExtensions(); $commercialExtensionActivatedLicenseObjectProperties = SettingsHelpers::getCommercialExtensionActivatedLicenseObjectProperties(); foreach ($extensions as $extension) $extensionCommercialExtensionActivatedLicenseObjectProperties = $commercialExtensionActivatedLicenseObjectProperties[$extension->getPluginSlug()] ? ? null; if ($extensionCommercialExtensionActivatedLicenseObjectProperties === null) continue; return $classes . ' is-gatographql-customer'; return $classes; );
Invalidating and declaring that the dependency is a version
Because the extension is derived from the PHP class used by the extension, it is essential to verify that the proper Version of the plugin has been set up. Failure to check this may create problems which could lead to the loss of the site.
This is the case for instance, if the AbstractExtension
class has been updated to include significant changes that break the code and is made available as an upgrade version 4.0.0
from the earlier version 3.4.0
, loading the extension without checking its version can produce a PHP error, preventing WordPress from loading.
In order to avoid this, it's essential for the extension to ensure that the plugin is running version 3.x.x
. If the version 4.0.0
is installed the extension will be removed, thus preventing from making mistakes.
This extension is able to perform this verification using the logic described below, which is run on the plugins_loaded
hook (since the core plugin has been installed at this point) in the extension's main plugin file. The logic is able to access extensions via the extensions manager
class which is part of the core extension plugin. It manages extensions
Code >/** * Create and set up the extension** Create an Add_action( "plugins_loaded" Function () The extension will be deleted. /*** the extension's name, and the version. It is recommended to use an extension suffix that is stable as it is accepted by Composer. */ $extensionVersion = '1.1.0';$extensionName is __('Gato GraphQL - Extension Template'); *** The minimum version required from Gato GraphQL is 1.1.0. Gato GraphQL plugin * to allow the extension to be activated. */ $gatoGraphQLPluginVersionConstraint = '^1.0'; /** * Validate Gato GraphQL is active */ if (!class_exists(\GatoGraphQL\GatoGraphQL\Plugin::class)) add_action('admin_notices', function () use ($extensionName) printf( '%s', sprintf( __('Plugin %s is not installed or activated. If the plugin isn't active, the plugin will not be loaded. '), __('Gato GraphQL'), $extensionName ) ); ); return; $extensionManager = \GatoGraphQL\GatoGraphQL\PluginApp::getExtensionManager(); if (!$extensionManager->assertIsValid( GatoGraphQLExtension::class, $extensionVersion, $extensionName, $gatoGraphQLPluginVersionConstraint )) return; // Load Composer's autoloader require_once(__DIR__ . '/vendor/autoload.php'); // Create and set-up the extension instance $extensionManager->register(new GatoGraphQLExtension( __FILE__, $extensionVersion, $extensionName, ))->setup(); );
Note how the extension declares the dependency of its extension on the Version of the constraint ^1.0
of the main plugin (using Composer's version constraints). So, if the the version 2.0.0
of Gato GraphQL is installed and it is not enabled then it is activated.
The version constraint is validated via the ExtensionManager::assertIsValid
method, which calls Semver::satisfies
(provided by the composer/semver
package):
use Composer\Semver\Semver; class ExtensionManager extends AbstractPluginManager /** * Validate that the required version of the Gato GraphQL for WP plugin is installed. If the assertion fails it will display an error to the administrator of WP, and then results in false. string
Test integrations on a WordPress server
To automate testing during CI/CD, it is necessary to access the server through an internet connection to the CI/CD server. Software such as InstaWP let you create Sandbox sites using WordPress that can be utilized as an sandbox.
name: Integration tests (InstaWP) on: workflow_run: workflows: [Generate plugins] types: - completed jobs: provide_data: if: $ github.event.workflow_run.conclusion == 'success' name: Retrieve the GitHub Action artifact URLs to install in InstaWP runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 coverage: none env: COMPOSER_TOKEN: $ secrets.GITHUB_TOKEN - uses: "ramsey/composer-install@v2" - name: Retrieve artifact URLs from GitHub workflow uses: actions/github-script@v6 id: artifact-url with: script: | const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts( owner: context.repo.owner, repo: context.repo.repo, run_id: context.payload.workflow_run.id, ); const artifactURLs = allArtifacts.data.artifacts.map((artifact) => return artifact.url.replace('https://api.github.com/repos', 'https://nightly.link') + '.zip' ).concat([ "https://downloads.wordpress.org/plugin/gatographql.latest-stable.zip" ]); return artifactURLs.join(','); result-encoding: string - name: Artifact URL for InstaWP run: echo "Artifact URL for InstaWP - $ steps.artifact-url.outputs.result " shell: bash outputs: artifact_url: $ steps.artifact-url.outputs.result process: needs: provide_data name: Launch InstaWP site from template 'integration-tests' and execute integration tests against it runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: 8.1 coverage: none env: COMPOSER_TOKEN: $ secrets.GITHUB_TOKEN - uses: "ramsey/composer-install@v2" - name: Create InstaWP instance uses: instawp/wordpress-testing-automation@main id: create-instawp with: GITHUB_TOKEN: $ secrets.GITHUB_TOKEN INSTAWP_TOKEN: $ secrets.INSTAWP_TOKEN INSTAWP_TEMPLATE_SLUG: "integration-tests" REPO_ID: 25 INSTAWP_ACTION: create-site-template ARTIFACT_URL: $ needs.provide_data.outputs.artifact_url - name: InstaWP instance URL run: echo "InstaWP instance URL - $ steps.create-instawp.outputs.instawp_url " shell: bash - name: Extract InstaWP domain id: extract-instawp-domain run: | instawp_domain="$(echo "$ steps.create-instawp.outputs.instawp_url " | sed -e s#https://##)" echo "instawp-domain=$(echo $instawp_domain)" >> $GITHUB_OUTPUT - name: Run tests run: | INTEGRATION_TESTS_WEBSERVER_DOMAIN=$ steps.extract-instawp-domain.outputs.instawp-domain \ INTEGRATION_TESTS_AUTHENTICATED_ADMIN_USER_USERNAME=$ steps.create-instawp.outputs.iwp_wp_username \ INTEGRATION_TESTS_AUTHENTICATED_ADMIN_USER_PASSWORD=$ steps.create-instawp.outputs.iwp_wp_password \ vendor/bin/phpunit --filter=Integration - name: Destroy InstaWP instance uses: instawp/wordpress-testing-automation@main id: destroy-instawp if: $ always() with: GITHUB_TOKEN: $ secrets.GITHUB_TOKEN INSTAWP_TOKEN: $ secrets.INSTAWP_TOKEN INSTAWP_TEMPLATE_SLUG: "integration-tests" REPO_ID: 25 INSTAWP_ACTION: destroy-site
This workflow downloads files in the .zip file via Nightly Link the service that allows access to artifacts through GitHub without logging into it It also makes setting up the installation of InstaWP.
The extension plugin is now available.
We can provide tools to help release the extensions while automating the processes as much as possible.
It is an extension of the Monorepo Builder is a program that can be used to manage each PHP project, including WordPress. WordPress plugin. It comes with the monorepo-builder release
command that allows you to publish an update for the project. It will increase either the major, minor or patch parts of the update based on semantic the language of versioning.
The command is able to run a set of release workers which include PHP classes that perform certain process. There are default builders, one which creates a git tag
using the updated version. The other pushes tags to remote repositories. Customized workers may be added before, after, or in between these steps.
The release worker is set up by a configuration file
use Symplify\MonorepoBuilder\Config\MBConfig; use Symplify\MonorepoBuilder\Release\ReleaseWorker\AddTagToChangelogReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\PushNextDevReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\PushTagReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\SetCurrentMutualDependenciesReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\SetNextMutualDependenciesReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\TagVersionReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\UpdateBranchAliasReleaseWorker; use Symplify\MonorepoBuilder\Release\ReleaseWorker\UpdateReplaceReleaseWorker; return static function (MBConfig $mbConfig): void // release workers - in order to execute $mbConfig->workers([ UpdateReplaceReleaseWorker::class, SetCurrentMutualDependenciesReleaseWorker::class, AddTagToChangelogReleaseWorker::class, TagVersionReleaseWorker::class, PushTagReleaseWorker::class, SetNextMutualDependenciesReleaseWorker::class, UpdateBranchAliasReleaseWorker::class, PushNextDevReleaseWorker::class, ]); ;
We offer custom release staff to assist with the process of release specifically designed to meet the requirements of a WordPress plugin. For example, the InjectStableTagVersionInPluginReadmeFileReleaseWorker
sets the new version as the "Stable tag" entry in the extension's readme.txt file:
use Nette\Utils\Strings; use PharIo\Version\Version; use Symplify\SmartFileSystem\SmartFileInfo; use Symplify\SmartFileSystem\SmartFileSystem; class InjectStableTagVersionInPluginReadmeFileReleaseWorker implements ReleaseWorkerInterface public function __construct( // This class is provided by the Monorepo Builder private SmartFileSystem $smartFileSystem, ) public function getDescription(Version $version): string return 'Have the "Stable tag" point to the new version in the plugin\'s readme.txt file'; public function work(Version $version): void $replacements = [ '/Stable tag:\s+[a-z0-9.-]+/' => 'Stable tag: ' . $version->getVersionString(), ]; $this->replaceContentInFiles(['/readme.txt'], $replacements); /** * @param string[] $files * @param array $regexPatternReplacements regex pattern to search, and its replacement */ protected function replaceContentInFiles(array $files, array $regexPatternReplacements): void foreach ($files as $file) $fileContent = $this->smartFileSystem->readFile($file); foreach ($regexPatternReplacements as $regexPattern => $replacement) $fileContent = Strings::replace($fileContent, $regexPattern, $replacement); $this->smartFileSystem->dumpFile($file, $fileContent);
By adding InjectStableTagVersionInPluginReadmeFileReleaseWorker
to the configuration list, whenever executing the monorepo-builder release
command to release a new version of the plugin, the "Stable tag" in the extension's readme.txt file will be automatically updated.
The extension plugin is uploaded on the WP.org directory
There is also the option of distributing an automated workflow to help to release the extension to the WordPress Plugin Directory. When tagging the project on the remote repository and following the process below is used for publishing the WordPress extension plugin to the directory:
# See: https://github.com/10up/action-wordpress-plugin-deploy#deploy-on-pushing-a-new-tag name: Deploy to WordPress.org Plugin Directory (SVN) on: push: tags: - "*" jobs: tag: name: New tag runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: WordPress Plugin Deploy uses: 10up/action-wordpress-plugin-deploy@stable env: SVN_PASSWORD: $ secrets.SVN_PASSWORD SVN_USERNAME: $ secrets.SVN_USERNAME SLUG: $ secrets.SLUG
Summary
If we develop an extension-friendly plugin for WordPress We aim to make it easy for third party developers to extend it to increase the possibility of creating a vibrant community around the plugins we create.
Though providing thorough documentation may aid developers in understanding how to extend the plugin, a better method is to provide all the PHP tools and code to develop, test, and releasing their extensions.
With the addition of additional code needed by extensions directly into our plugin, it is easier for extension developers.
Have you thought of making your WordPress plugin extensible? Let us know in the comments section.
Leonardo Losoviz
Leo is blogger, who writes about the latest Web methods of development, mostly in relation to PHP, WordPress and GraphQL. Leo is available at leoloso.com and twitter.com/losoviz.
This post was posted on here