The development of a WordPress plugin that is extensible using the help of PHP classes (r)

Aug 13, 2024

-sidebar-toc> -language-notice>

  1. In installing hooks (actions and filters) for plugins extensions, which add their own functions
  2. Through offering PHP classes that extensions plugins may take

The first method relies more on documentation, describing the hooks available and how they are used. The second method, in contrast, has ready-to-use software that is extensible to improve functionality, eliminating the requirement to provide a detailed document. This is advantageous since the writing of documentation and the coding could hinder the administration of plugins as well as distribution.

We'll look at some methods to do this in order to establish an integrated ecosystem around WordPress. WordPress plugin.

The basic PHP classes required by the WordPress plugin

Let's take a look at how this is possible to implement in the open source Gato GraphQL plugin.

AbstractPlugin class:

AbstractPlugin is called a plug-in which is compatible to 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 extends AbstractPlugin so that it can enable it to reflect 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:

The same is true for AbstractExtension. AbstractExtension can expand AbstractPlugin into 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 this is because AbstractExtension is an integral part of the plugin. It allows you to join and start an extension. It's however only utilized by extensions, not by the plugin itself.

AbstractPlugin can be described as one of the AbstractPlugin classes. AbstractPlugin is a class, which has a shared initialization process that is invoked at different dates. The procedures are formulated in the ancestral level, however they are also used by classes that inherit according to the duration of their lives.

The primary plugin and its extensions are launched by running the initialization procedure in the class that is invoked inside the base WordPress plugin's program.

For instance, in Gato GraphQL, this is carried out through gatographql.php:

$pluginFile = __FILE__; $pluginVersion = '2.4.0'; $pluginName = __('Gato GraphQL', 'gatographql'); PluginApp::getMainPluginManager()->register(new Plugin( $pluginFile, $pluginVersion, $pluginName ))->setup(); 

Method for setting up

If the extension is the parent, the configuration includes the logic common between the extension as well as the plugin, such as unregistering them after the plugin is deleted. The method does not have to be a definitive one and may be altered by inheriting classes 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); 

The method of setting up the main plugin:

The primary configuration method starts the entire procedure of setting up the plugin. Its primary method for executing functions is through procedures like the initialization, configureComponents, configure, and begin as well as activating 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 process

AbstractExtension class AbstractExtension class implements its logic on the hooks used in the corresponding classes:

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);

Techniques for begin, configureComponents, configure, and start are shared by the primary plugin, as are extensions. They may also be based on the same logic. The logic used by these methods is part of 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, in addition has its own implementation

abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface // ... protected function callPluginInitializationConfiguration(): void $this->extensionInitializationConfiguration?->initialize(); 

Methods which initiate, configureComponents and the time to start will be determined by the parent. They are able to be altered 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

Every method is able to be altered by using AbstractMainPlugin or AbstractExtension to add functions of your choosing.

The main plugin's configuration method removes also all cached WordPress instances where the plugin, or an extension is deactivated or activated:

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, similarly, the deactivate method eliminates cache and boots also performs additional actions hooks that are unique to the plugin. However, they are exclusively 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; );

Validating the dependency and declaring that the dependency is an version

As the extension derives from the PHP class utilized by the extension, it is important to ensure that the right version of the plugin has been installed. Failure to check this may cause problems that could result in the demise of the site.

This is the case for instance, if AbstractExtension has been updated and the AbstractExtension class is modified to incorporate significant changes which break the code, and is made available in the upgrade version 4.0.0 from the earlier version 3.4.0, loading the extension without verifying its version could result in a PHP error that prevents WordPress from loading.

To prevent this from happening, it's crucial for the extension to ensure that the plugin runs versions 3.x.x. If Version 4.0.0 is installed the extension is removed to prevent errors.

This extension has the ability to verify this through the process described in this article and is executed by the plugins_loaded hook (since the extension's core plugin is in place to this point) inside the extension's plugin's main file. The logic is able to gain access to extensions using extension manager classes. extensions management class that is included in the core plugin. It handles extensions

Code >/** Create and set up an extension. the Add_action( "plugins_loaded" Funktion () Extension will be deleted. /*** The extension's name as well as the extension's version. It is recommended to use an extension suffix that has been 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 permit the extension to be active. */ $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 and the plugin is not active, it will not be installed. '), __('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(); );

It is important to note that the extension declares the dependency of its extension on the Version of the constraint ^1.0 of the main extension (using Composer's version constraints). Also, if Version 2.0.0 of Gato GraphQL is installed but isn't enabled, then it has been 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 show an error to WP's administrator WP, and then results in a false. String

Testing integrations using the WordPress server

In order to automate the testing process during CI/CD, you need to connect via an internet connection that connects to the CI/CD server. Software like InstaWP let you create Sandbox sites using WordPress which can serve to serve as a 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 which allows access to artifacts through GitHub without the need to log in. This workflow also assists in setting up the installation of InstaWP.

Extension plugins are available.

We have tools that can assist in the release of extensions, while making the process as automated as we can.

It is an extension of the Monorepo Builder is a tool that is used to manage each PHP project, and that includes WordPress. WordPress plugin. It comes with the monorepo-builder release command, which lets you publish an update on your project. It will increase or decrease the size of either the major, minor or patch components of the upgrade based on semantics the language used for versions.

The command is able to run a set of release workers which include PHP classes that perform certain processes. There are built-in builders that default, one which creates a git tag with the latest version. The other pushes tags to remote repository. The custom-designed worker can be added before, after, or in between these steps.

The release worker is configured via a configuration files

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 have a dedicated release staff to assist in the release process specifically designed to meet the needs 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 can be downloaded on the WP.org directory

It is also possible to take the route of distributing an automated workflow that will help in releasing the extension into WordPress's WordPress plugin directory. After tagging the plugin on the remote repository, and then adhering to the procedure below is used for publishing your WordPress extension plugin into 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 to WordPress we will allow third party developers to extend it in order to enhance the chances of forming a lively community for the plugins that we create.

While providing detailed documentation can assist developers to understand how to expand this plugin better method is to provide all the PHP tools and the code needed for developing, testing the extensions, and then release them.

By incorporating additional code needed by extensions directly into our plugin, it is easier for extension developers.

     Have you thought of making your WordPress plugin extensible? Do let us know your thoughts in the comment box.

Leonardo Losoviz

Leo is blogger, who is a blogger who writes about the most current Web ways of developing most often in connection with PHP, WordPress and GraphQL. Leo is accessible via leoloso.com and twitter.com/losoviz.

The post was published on this website.

Article was posted on here