
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,
$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);
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);
}
// ...
}
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(
protected MainPluginInitializationConfigurationInterface $pluginInitializationConfiguration,
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
public function __construct(
string $pluginFile,
string $pluginVersion,
?string $pluginName,
protected MainPluginInitializationConfigurationInterface $pluginInitializationConfiguration,
) {
parent::__construct(
$pluginFile,
$pluginVersion,
$pluginName,
);
}
// ...
}
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(
protected ?ExtensionInitializationConfigurationInterface $extensionInitializationConfiguration,
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
public function __construct(
string $pluginFile,
string $pluginVersion,
?string $pluginName,
protected ?ExtensionInitializationConfigurationInterface $extensionInitializationConfiguration,
) {
parent::__construct(
$pluginFile,
$pluginVersion,
$pluginName,
);
}
// ...
}
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
中完成的:
$pluginVersion = '2.4.0';
$pluginName = __('Gato GraphQL', 'gatographql');
PluginApp::getMainPluginManager()->register(new Plugin(
$pluginFile = __FILE__;
$pluginVersion = '2.4.0';
$pluginName = __('Gato GraphQL', 'gatographql');
PluginApp::getMainPluginManager()->register(new Plugin(
$pluginFile,
$pluginVersion,
$pluginName
))->setup();
$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(
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);
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);
}
}
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
add_action('plugins_loaded', function (): void
// 1. Initialize main plugin
// 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
// 6. Configure extension
do_action('gatographql:configureExtension');
do_action('gatographql:bootExtension');
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');
}
// ...
}
// ...
}
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');
}
// ...
}
// ...
}
扩展 setup 方法:
AbstractExtension
类在相应的钩子上执行其逻辑:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
final public function setup(): void
add_action('plugins_loaded', function (): void
// 2. Initialize extensions
'gatographql:initializeExtension',
// 4. Configure extension components
'gatographql:configureExtensionComponents',
$this->configureComponents(...)
// 6. Configure extension
'gatographql:configureExtension',
'gatographql:bootExtension',
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);
}
}
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);
}
}
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<class-string<ModuleInterface>,mixed> [key]: Module class, [value]: Configuration
public function getModuleClassConfiguration(): array
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<class-string<ModuleInterface>,mixed> [key]: Module class, [value]: Configuration
*/
public function getModuleClassConfiguration(): array
{
return [];
}
}
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<class-string<ModuleInterface>,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 AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
// ...
protected function callPluginInitializationConfiguration(): void
{
$this->pluginInitializationConfiguration->initialize();
}
}
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();
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
// ...
protected function callPluginInitializationConfiguration(): void
{
$this->extensionInitializationConfiguration?->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<class-string<ModuleInterface>> 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
abstract class AbstractPlugin implements PluginInterface
{
// ...
public function initialize(): void
{
$moduleClasses = $this->getModuleClassesToInitialize();
App::getAppLoader()->addModuleClassesToInitialize($moduleClasses);
}
/**
* @return array<class-string<ModuleInterface>> 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
}
}
abstract class AbstractPlugin implements PluginInterface
{
// ...
public function initialize(): void
{
$moduleClasses = $this->getModuleClassesToInitialize();
App::getAppLoader()->addModuleClassesToInitialize($moduleClasses);
}
/**
* @return array<class-string<ModuleInterface>> 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
// Main-plugin specific methods
function (string $pluginFile): void {
$this->maybeRegenerateContainerWhenPluginActivatedOrDeactivated($pluginFile);
function (string $pluginFile): void {
$this->maybeRegenerateContainerWhenPluginActivatedOrDeactivated($pluginFile);
public function maybeRegenerateContainerWhenPluginActivatedOrDeactivated(string $pluginFile): void
// Removed code for simplicity
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
}
// ...
}
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
}
// ...
}
同样,deactivate
方法会移除缓存,并仅在主插件 boot
时执行额外的操作钩子:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
public function deactivate(): void
$this->removeTimestamps();
protected function removeTimestamps(): void
$userSettingsManager = UserSettingsManagerFacade::getInstance();
$userSettingsManager->removeTimestamps();
public function boot(): void
function (string $classes): string {
$extensions = PluginApp::getExtensionManager()->getExtensions();
$commercialExtensionActivatedLicenseObjectProperties = SettingsHelpers::getCommercialExtensionActivatedLicenseObjectProperties();
foreach ($extensions as $extension) {
$extensionCommercialExtensionActivatedLicenseObjectProperties = $commercialExtensionActivatedLicenseObjectProperties[$extension->getPluginSlug()] ?? null;
if ($extensionCommercialExtensionActivatedLicenseObjectProperties === null) {
return $classes . ' is-gatographql-customer';
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;
}
);
}
}
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
* 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) {
'<div class="notice notice-error"><p>%s</p></div>',
__('Plugin <strong>%s</strong> is not installed or activated. Without it, plugin <strong>%s</strong> will not be loaded.'),
$extensionManager = \GatoGraphQL\GatoGraphQL\PluginApp::getExtensionManager();
if (!$extensionManager->assertIsValid(
GatoGraphQLExtension::class,
$gatoGraphQLPluginVersionConstraint
// Load Composer’s autoloader
require_once(__DIR__ . '/vendor/autoload.php');
// Create and set-up the extension instance
$extensionManager->register(new GatoGraphQLExtension(
/**
* 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(
'<div class="notice notice-error"><p>%s</p></div>',
sprintf(
__('Plugin <strong>%s</strong> is not installed or activated. Without it, plugin <strong>%s</strong> 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();
}
);
/**
* 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(
'<div class="notice notice-error"><p>%s</p></div>',
sprintf(
__('Plugin <strong>%s</strong> is not installed or activated. Without it, plugin <strong>%s</strong> 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();
}
);
请注意扩展是如何声明依赖于主插件的版本约束 ^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 $extensionVersion,
?string $extensionName = null,
?string $mainPluginVersionConstraint = null,
$mainPlugin = \GatoGraphQL\GatoGraphQL\PluginApp::getMainPluginManager()->getPlugin();
$mainPluginVersion = $mainPlugin->getPluginVersion();
$mainPluginVersionConstraint !== null && !Semver::satisfies(
$mainPluginVersionConstraint
$this->printAdminNoticeErrorMessage(
__('Extension or bundle <strong>%s</strong> requires plugin <strong>%s</strong> to satisfy version constraint <code>%s</code>, but the current version <code>%s</code> does not. The extension or bundle has not been loaded.', 'gatographql'),
$extensionName ?? $extensionClass,
$mainPlugin->getPluginName(),
$mainPluginVersionConstraint,
$mainPlugin->getPluginVersion(),
protected function printAdminNoticeErrorMessage(string $errorMessage): void
\add_action('admin_notices', function () use ($errorMessage): void {
$adminNotice_safe = sprintf(
'<div class="notice notice-error"><p>%s</p></div>',
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 <strong>%s</strong> requires plugin <strong>%s</strong> to satisfy version constraint <code>%s</code>, but the current version <code>%s</code> 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(
'<div class="notice notice-error"><p>%s</p></div>',
$errorMessage
);
echo $adminNotice_safe;
});
}
}
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 <strong>%s</strong> requires plugin <strong>%s</strong> to satisfy version constraint <code>%s</code>, but the current version <code>%s</code> 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(
'<div class="notice notice-error"><p>%s</p></div>',
$errorMessage
);
echo $adminNotice_safe;
});
}
}
针对 WordPress 服务器运行集成测试
为了让第三方开发人员更容易为您的插件创建扩展,请为他们提供开发和测试工具,包括持续集成和持续交付(CI/CD)流程的工作流。
在开发过程中,任何人都可以使用 DevKinsta 轻松启动网络服务器,安装他们为之编码扩展的插件,并立即验证扩展是否与插件兼容。
要在 CI/CD 期间自动进行测试,我们需要通过网络将网络服务器接入 CI/CD 服务。InstaWP 等服务可以为此创建一个安装了 WordPress 的沙盒网站。
如果扩展的代码库托管在 GitHub 上,开发人员可以使用 GitHub Actions 针对 InstaWP 服务运行集成测试。以下工作流程在 InstaWP 沙盒网站上安装扩展(与主插件的最新稳定版本一起),然后运行集成测试:
name: Integration tests (InstaWP)
workflows: [Generate plugins]
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: Retrieve the GitHub Action artifact URLs to install in InstaWP
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: "ramsey/composer-install@v2"
- name: Retrieve artifact URLs from GitHub workflow
uses: actions/github-script@v6
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
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'
"https://downloads.wordpress.org/plugin/gatographql.latest-stable.zip"
return artifactURLs.join(',');
- name: Artifact URL for InstaWP
run: echo "Artifact URL for InstaWP - ${{ steps.artifact-url.outputs.result }}"
artifact_url: ${{ steps.artifact-url.outputs.result }}
name: Launch InstaWP site from template 'integration-tests' and execute integration tests against it
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: "ramsey/composer-install@v2"
- name: Create InstaWP instance
uses: instawp/wordpress-testing-automation@main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INSTAWP_TOKEN: ${{ secrets.INSTAWP_TOKEN }}
INSTAWP_TEMPLATE_SLUG: "integration-tests"
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 }}"
- name: Extract InstaWP domain
id: extract-instawp-domain
instawp_domain="$(echo "${{ steps.create-instawp.outputs.instawp_url }}" | sed -e s#https://##)"
echo "instawp-domain=$(echo $instawp_domain)" >> $GITHUB_OUTPUT
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
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INSTAWP_TOKEN: ${{ secrets.INSTAWP_TOKEN }}
INSTAWP_TEMPLATE_SLUG: "integration-tests"
INSTAWP_ACTION: destroy-site
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
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
该工作流程通过 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
UpdateReplaceReleaseWorker::class,
SetCurrentMutualDependenciesReleaseWorker::class,
AddTagToChangelogReleaseWorker::class,
TagVersionReleaseWorker::class,
PushTagReleaseWorker::class,
SetNextMutualDependenciesReleaseWorker::class,
UpdateBranchAliasReleaseWorker::class,
PushNextDevReleaseWorker::class,
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,
]);
};
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 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
'/Stable tag:\s+[a-z0-9.-]+/' => 'Stable tag: ' . $version->getVersionString(),
$this->replaceContentInFiles(['/readme.txt'], $replacements);
* @param array<string,string> $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);
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<string,string> $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);
}
}
}
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<string,string> $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-tag
name: Deploy to WordPress.org Plugin Directory (SVN)
- uses: actions/checkout@master
- name: WordPress Plugin Deploy
uses: 10up/action-wordpress-plugin-deploy@stable
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
SLUG: ${{ secrets.SLUG }}
# 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 }}
# 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 }}
此工作流程使用 10up/action-wordpress-plugin-deploy
操作,该操作会从 Git 代码库中获取代码并将其推送到 WordPress.org SVN 代码库,从而简化了操作。
小结
在为 WordPress 创建可扩展插件时,我们的目标是尽可能方便第三方开发人员对其进行扩展,从而最大限度地围绕我们的插件培育出一个充满活力的生态系统。
虽然提供大量文档可以指导开发人员如何扩展插件,但更有效的方法是为开发、测试和发布扩展提供必要的 PHP 代码和工具。
通过在插件中直接包含扩展所需的附加代码,我们简化了开发人员的开发过程。
您打算让自己的 WordPress 插件具有可扩展性吗?请在评论区告诉我们。
评论留言