如何使用PHP类扩展WordPress插件
WordPress 插件可以通过附加功能进行扩展,WooCommerce 和 Gravity Forms 等流行插件就证明了这一点。在 “构建支持扩展的 WordPress 插件” 一文中,我们了解到使 WordPress 插件具有可扩展性的两种主要方法:
- 为扩展插件设置钩子(动作和过滤器),以便其注入自己的功能
- 分享扩展插件可以继承的 PHP 类
第一种方法更多地依赖于文档,详细说明可用的钩子及其用法。相比之下,第二种方法为扩展分享即用代码,减少了对大量文档的需求。这样做的好处是,在创建代码的同时创建文档,会使插件的管理和发布复杂化。
直接分享 PHP 类可以有效地用代码取代文档。插件不是教授如何实现某个功能,而是分享必要的 PHP 代码,从而简化了第三方开发人员的工作。
让我们来探讨一些实现这一目标的技巧,最终目标是围绕我们的 WordPress 插件建立一个集成生态系统。
在 WordPress 插件中定义基础 PHP 类
WordPress 插件将包含供扩展插件使用的 PHP 类。这些 PHP 类可能不会被主插件本身使用,而是专门分享给其他插件使用。
让我们看看开源的 Gato GraphQL 插件是如何实现的。
AbstractPlugin 类:
AbstractPlugin
表示一个插件,包括 Gato GraphQL 主插件及其扩展插件:
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 类:
AbstractMainPlugin
扩展了 AbstractPlugin
以表示主插件:
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 类:
同样,AbstractExtension
扩展了 AbstractPlugin
以表示扩展插件:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface{public function __construct(string $pluginFile,string $pluginVersion,?string $pluginName,protected ?ExtensionInitializationConfigurationInterface $extensionInitializationConfiguration,) {parent::__construct($pluginFile,$pluginVersion,$pluginName,);}// ...}
请注意,AbstractExtension
包含在主插件中,分享了注册和初始化扩展的功能。不过,它只被扩展程序使用,而不是被主插件本身使用。
AbstractPlugin
类包含在不同时间调用的共享初始化代码。这些方法是在祖先级别定义的,但继承类会根据其生命周期进行调用。
主插件和扩展通过执行相应类的 setup
方法进行初始化,该方法在 WordPress 主插件文件中调用。
例如,在 Gato GraphQL 中,这是在 gatographql.php
中完成的:
$pluginFile = __FILE__;$pluginVersion = '2.4.0';$pluginName = __('Gato GraphQL', 'gatographql');PluginApp::getMainPluginManager()->register(new Plugin($pluginFile,$pluginVersion,$pluginName))->setup();
setup 方法:
在祖先级别(ancestor level),setup
包含插件及其扩展之间的共同逻辑,例如在插件停用时取消注册它们。该方法不是最终方法;继承类可以重写该方法,以添加自己的功能:
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);}}
主插件的 setup 方法:
主插件的 setup
方法初始化应用程序的生命周期。它通过initialize
、configureComponents
、configure
和boot
等方法执行主插件的功能,并为扩展触发相应的操作钩子:
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 extensionsdo_action('gatographql:initializeExtension');// 3. Configure main plugin components$this->configureComponents();// 4. Configure extension componentsdo_action('gatographql:configureExtensionComponents');// 5. Configure main plugin$this->configure();// 6. Configure extensiondo_action('gatographql:configureExtension');// 7. Boot main plugin$this->boot();// 8. Boot extensiondo_action('gatographql:bootExtension');}// ...}// ...}
扩展 setup 方法:
AbstractExtension
类在相应的钩子上执行其逻辑:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface{// ...final public function setup(): void{parent::setup();add_action('plugins_loaded', function (): void{// 2. Initialize extensionsadd_action('gatographql:initializeExtension',$this->initialize(...));// 4. Configure extension componentsadd_action('gatographql:configureExtensionComponents',$this->configureComponents(...));// 6. Configure extensionadd_action('gatographql:configureExtension',$this->configure(...));// 8. Boot extensionadd_action('gatographql:bootExtension',$this->boot(...));}, 20);}}
initialize
, configureComponents
, configure
, 和 boot
方法对主插件和扩展插件都是通用的,并且知识兔可以共享逻辑。这些共享逻辑存储在 AbstractPlugin
类中。
例如,configure
方法配置插件或扩展程序,调用 callPluginInitializationConfiguration
(主插件和扩展程序有不同的实现方式)和 getModuleClassConfiguration
(分享默认行为,但可根据需要重载):
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 [];}}
主插件为 callPluginInitializationConfiguration
分享其实现:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface{// ...protected function callPluginInitializationConfiguration(): void{$this->pluginInitializationConfiguration->initialize();}}
同样,扩展类也分享其实现:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface{// ...protected function callPluginInitializationConfiguration(): void{$this->extensionInitializationConfiguration?->initialize();}}
方法 initialize
、configureComponents
和 boot
定义在祖先级别,继承类可以重载这些方法:
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}}
所有方法都可以被 AbstractMainPlugin
或 AbstractExtension
改写,以扩展它们的自定义功能。
对于主插件,当插件或其任何扩展被激活或停用时,setup
方法也会删除 WordPress 实例中的任何缓存:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface{public function setup(): void{parent::setup();// ...// Main-plugin specific methodsadd_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}// ...}
同样,deactivate
方法会移除缓存,并仅在主插件 boot
时执行额外的操作钩子:
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;});}}
从上面介绍的所有代码中,我们可以清楚地看到,在设计和编码 WordPress 插件时,我们需要考虑其扩展的需求,并尽可能在它们之间重复使用代码。实施合理的面向对象编程模式(如 SOLID 原则)有助于实现这一目标,使代码库具有长期可维护性。
声明并验证版本依赖关系
由于扩展继承自插件分享的 PHP 类,因此验证插件是否存在所需的版本至关重要。否则可能会导致冲突,导致网站瘫痪。
例如,如果知识兔 AbstractExtension
类在更新时发生了重大变化,并从以前的 3.4.0
版本发布了 4.0.0
主版本,那么在未检查版本的情况下加载扩展可能会导致 PHP 错误,从而阻止 WordPress 加载。
为避免出现这种情况,扩展必须验证所安装的插件版本为3.x.x
。当安装 4.0.0
版本时,扩展将被禁用,从而避免出现错误。
扩展可以使用以下逻辑完成验证,该逻辑在扩展的主插件文件中的 plugins_loaded
钩子上执行(因为此时主插件已经加载)。该逻辑访问 ExtensionManager
类,该类包含在主插件中,知识兔用于管理扩展:
/*** Create and set-up the extension*/add_action('plugins_loaded',function (): void {/*** Extension's name and version.** Use a stability suffix as supported by Composer.*/$extensionVersion = '1.1.0';$extensionName = __('Gato GraphQL - Extension Template');/*** The minimum version required from the Gato GraphQL plugin* to activate the extension.*/$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. Without it, plugin %s 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 autoloaderrequire_once(__DIR__ . '/vendor/autoload.php');// Create and set-up the extension instance$extensionManager->register(new GatoGraphQLExtension(__FILE__,$extensionVersion,$extensionName,))->setup();});
请注意扩展是如何声明依赖于主插件的版本约束 ^1.0
(使用Composer 的版本约束)。因此,当安装 Gato GraphQL 2.0.0
版本时,扩展将不会被激活。
版本约束通过 ExtensionManager::assertIsValid
方法进行验证,该方法调用Semver::satisfies
(由 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 prints an error on the WP admin and returns false** @param string|null $mainPluginVersionConstraint the semver version constraint required for the plugin (eg: "^1.0" means >=1.0.0 and <2.0.0)*/public function assertIsValid(string $extensionClass,string $extensionVersion,?string $extensionName = null,?string $mainPluginVersionConstraint = null,): bool {$mainPlugin = \GatoGraphQL\GatoGraphQL\PluginApp::getMainPluginManager()->getPlugin();$mainPluginVersion = $mainPlugin->getPluginVersion();if ($mainPluginVersionConstraint !== null && !Semver::satisfies($mainPluginVersion,$mainPluginVersionConstraint)) {$this->printAdminNoticeErrorMessage(sprintf(__('Extension or bundle %s requires plugin %s to satisfy version constraint%s
, but the current version%s
does not. The extension or bundle has not been loaded.', 'gatographql'),$extensionName ?? $extensionClass,$mainPlugin->getPluginName(),$mainPluginVersionConstraint,$mainPlugin->getPluginVersion(),));return false;}return true;}protected function printAdminNoticeErrorMessage(string $errorMessage): void{\add_action('admin_notices', function () use ($errorMessage): void {$adminNotice_safe = sprintf('%s
',$errorMessage);echo $adminNotice_safe;});}}
针对 WordPress 服务器运行集成测试
为了让第三方开发人员更容易为您的插件创建扩展,请为他们分享开发和测试工具,包括持续集成和持续交付(CI/CD)流程的工作流。
在开发过程中,任何人都可以使用 DevKinsta 轻松启动网络服务器,安装他们为之编码扩展的插件,并立即验证扩展是否与插件兼容。
要在 CI/CD 期间自动进行测试,我们需要通过网络将网络服务器接入 CI/CD 服务。InstaWP 等服务可以为此创建一个安装了 WordPress 的沙盒网站。
如果知识兔扩展的代码库托管在 GitHub 上,开发人员可以使用 GitHub Actions 针对 InstaWP 服务运行集成测试。以下工作流程在 InstaWP 沙盒网站上安装扩展(与主插件的最新稳定版本一起),然后知识兔运行集成测试:
name: Integration tests (InstaWP)on:workflow_run:workflows: [Generate plugins]types:- completedjobs:provide_data:if: ${{ github.event.workflow_run.conclusion == 'success' }}name: Retrieve the GitHub Action artifact URLs to install in InstaWPruns-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: shivammathur/setup-php@v2with:php-version: 8.1coverage: noneenv:COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}- uses: "ramsey/composer-install@v2"- name: Retrieve artifact URLs from GitHub workflowuses: actions/github-script@v6id: artifact-urlwith: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 InstaWPrun: echo "Artifact URL for InstaWP - ${{ steps.artifact-url.outputs.result }}"shell: bashoutputs:artifact_url: ${{ steps.artifact-url.outputs.result }}process:needs: provide_dataname: Launch InstaWP site from template 'integration-tests' and execute integration tests against itruns-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: shivammathur/setup-php@v2with:php-version: 8.1coverage: noneenv:COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}- uses: "ramsey/composer-install@v2"- name: Create InstaWP instanceuses: instawp/wordpress-testing-automation@mainid: create-instawpwith:GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}INSTAWP_TOKEN: ${{ secrets.INSTAWP_TOKEN }}INSTAWP_TEMPLATE_SLUG: "integration-tests"REPO_ID: 25INSTAWP_ACTION: create-site-templateARTIFACT_URL: ${{ needs.provide_data.outputs.artifact_url }}- name: InstaWP instance URLrun: echo "InstaWP instance URL - ${{ steps.create-instawp.outputs.instawp_url }}"shell: bash- name: Extract InstaWP domainid: 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 testsrun: |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 instanceuses: instawp/wordpress-testing-automation@mainid: destroy-instawpif: ${{ always() }}with:GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}INSTAWP_TOKEN: ${{ secrets.INSTAWP_TOKEN }}INSTAWP_TEMPLATE_SLUG: "integration-tests"REPO_ID: 25INSTAWP_ACTION: destroy-site
该工作流程通过 Nightly Link 访问 .zip 文件。服务允许在不登录的情况下访问 GitHub 上的工件,从而简化了 InstaWP 的配置。
发布扩展插件
我们可以分享工具帮助发布扩展插件,尽可能实现程序自动化。
Monorepo Builder 是一个用于管理任何 PHP 项目(包括 WordPress 插件)的库。它分享了 monorepo-builder release
命令,知识兔用于发布项目版本,并根据语义版本法递增版本的 major、minor 或 patch 组件。
该命令会执行一系列发布 Worker,即执行特定逻辑的 PHP 类。默认的 Worker 包括一个创建新版本 git tag
的 Worker 和一个将标签推送到远程仓库的 Worker。自定义 Worker 可以在这些步骤之前、之后或之间注入。
发布 Worker 通过配置文件进行配置:
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,]);};
我们可以根据 WordPress 插件的需要分享定制的发布程序,以增强发布流程。例如,InjectStableTagVersionInPluginReadmeFileReleaseWorker
会将新版本设置为扩展程序 readme.txt 文件中的 “Stable tag” 条目:
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 Builderprivate 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);}}}
通过在配置列表中添加 InjectStableTagVersionInPluginReadmeFileReleaseWorker
,每当执行 monorepo-builder release
命令发布新版本插件时,扩展的 readme.txt 文件中的 “Stable tag” 就会自动更新。
向 WP.org 目录发布扩展插件
我们还可以发布一个工作流程,知识兔帮助将扩展发布到 WordPress 插件目录。在远程版本库中标记项目时,以下工作流程将把 WordPress 扩展插件发布到目录中:
# See: https://github.com/10up/action-wordpress-plugin-deploy#deploy-on-pushing-a-new-tagname: Deploy to WordPress.org Plugin Directory (SVN)on:push:tags:- "*"jobs:tag:name: New tagruns-on: ubuntu-lateststeps:- uses: actions/checkout@master- name: WordPress Plugin Deployuses: 10up/action-wordpress-plugin-deploy@stableenv:SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}SVN_USERNAME: ${{ secrets.SVN_USERNAME }}SLUG: ${{ secrets.SLUG }}
此工作流程使用 10up/action-wordpress-plugin-deploy
操作,该操作会从 Git 代码库中获取代码并将其推送到 WordPress.org SVN 代码库,从而简化了操作。
下载仅供下载体验和测试学习,不得商用和正当使用。