Os plugins de WordPress podem ser estendidos com funcionalidades adicionais, conforme demonstrado por plugins populares como WooCommerce e Gravity Forms. No artigo Como Projetar um Plugin de WordPress para Oferecer Suporte a Extensões, aprendemos que há duas maneiras principais de tornar um plugin de WordPress extensível:

  1. Configurando hooks (ações e filtros) para que plugins de extensão injetem sua própria funcionalidade
  2. Fornecendo classes PHP que plugins de extensão possam herdar

O primeiro método se baseia mais na documentação, detalhando os hooks disponíveis e sua utilização. O segundo método, por outro lado, oferece código para extensões pronto para uso, reduzindo a necessidade de ampla documentação. Isso é vantajoso porque criar documentação junto com o código pode complicar o gerenciamento e o lançamento do plugin.

Fornecer diretamente as classes PHP substitui efetivamente a documentação pelo código. Em vez de ensinar como implementar um recurso, o plugin fornece o código PHP necessário, simplificando a tarefa para desenvolvedores terceiros.

Vamos explorar algumas técnicas para conseguir isso, com o objetivo final de promover um ecossistema de integrações em torno do nosso plugin de WordPress.

Definindo classes PHP básicas no plugin de WordPress

O plugin de WordPress incluirá classes PHP destinadas ao uso por plugins de extensão. Essas classes PHP podem não ser usadas pelo próprio plugin principal, mas são fornecidas especificamente para serem usadas por outros.

Vejamos como isso é implementado no plugin de código aberto Gato GraphQL.

Classe AbstractPlugin:

AbstractPlugin representa um plugin, tanto para o plugin principal do Gato GraphQL quanto para suas extensões:

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

  // ...
}

Classe AbstractMainPlugin:

AbstractMainPlugin estende AbstractPlugin para representar o plugin principal:

abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
  public function __construct(
    string $pluginFile,
    string $pluginVersion,
    ?string $pluginName,
    protected MainPluginInitializationConfigurationInterface $pluginInitializationConfiguration,
  ) {
    parent::__construct(
      $pluginFile,
      $pluginVersion,
      $pluginName,
    );
  }

  // ...
}

Classe AbstractExtension:

Similarmente, AbstractExtension estende AbstractPlugin para representar um plugin de extensão:

abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
  public function __construct(
    string $pluginFile,
    string $pluginVersion,
    ?string $pluginName,
    protected ?ExtensionInitializationConfigurationInterface $extensionInitializationConfiguration,
  ) {
    parent::__construct(
      $pluginFile,
      $pluginVersion,
      $pluginName,
    );
  }

  // ...
}

Observe que AbstractExtension está incluída no plugin principal, fornecendo funcionalidade para registrar e inicializar uma extensão. Todavia, é usada apenas por extensões, não pelo plugin principal.

A classe AbstractPlugin contém código de inicialização compartilhado invocado em momentos diferentes. Esses métodos são definidos no nível do ancestral, mas são invocados pelas classes herdadas de acordo com seus ciclos de vida.

O plugin principal e as extensões são inicializados executando o método setup na classe correspondente, invocado de dentro do arquivo principal do plugin de WordPress.

Por exemplo, no Gato GraphQL, isso é feito em gatographql.php:

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

Método setup:

No nível do ancestral, setup contém a lógica comum ao plugin e suas extensões, como o cancelamento do registro delas, quando o plugin é desativado. Esse método não é final; pode ser substituído pelas classes herdadas para adicionar sua funcionalidade:

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

Método setup do plugin principal:

O método setup do plugin principal inicializa o ciclo de vida do aplicativo. Executa a funcionalidade do plugin principal por meio de métodos como initialize, configureComponents, configure e boot, e aciona os hooks de ação correspondentes para extensões:

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

    // ...
  }
  
  // ...
}

Método setup de extensão:

A classe AbstractExtension executa sua lógica nos hooks correspondentes:

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

Os métodos initialize, configureComponents, configure e boot são comuns ao plugin principal e às extensões, e podem compartilhar a lógica. Essa lógica compartilhada é armazenada na classe AbstractPlugin.

Por exemplo, o método configure configura o plugin ou as extensões, chamando callPluginInitializationConfiguration, que tem implementações diferentes para o plugin principal e as extensões e é definido como abstrato, e getModuleClassConfiguration, que fornece um comportamento padrão, mas pode ser substituído se necessário:

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 [];
  }
}

O plugin principal fornece sua implementação para callPluginInitializationConfiguration:

abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
  // ...

  protected function callPluginInitializationConfiguration(): void
  {
    $this->pluginInitializationConfiguration->initialize();
  }
}

Da mesma forma, a classe de extensão fornece sua implementação:

abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
  // ...

  protected function callPluginInitializationConfiguration(): void
  {
    $this->extensionInitializationConfiguration?->initialize();
  }
}

Os métodos initialize, configureComponents e boot são definidos no nível do ancestral e podem ser substituídos por classes herdadas:

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
  }
}

Todos os métodos podem ser substituídos por AbstractMainPlugin ou AbstractExtension para estendê-los com sua funcionalidade personalizada.

No caso do plugin principal, o método setup também remove qualquer cache da instância do WordPress quando o plugin ou qualquer uma de suas extensões é ativado, ou desativado:

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
  }

  // ...
}

Da mesma forma, o método deactivate remove o cache e o boot executa hooks de ação adicionais somente para o plugin principal:

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

Com base em todo o código apresentado acima, fica claro que, ao projetar e codificar um plugin de WordPress, precisamos considerar as necessidades de suas extensões e reutilizar o código entre elas o máximo possível. A implementação de padrões sólidos de programação orientada a objetos (como os princípios SOLID) ajuda a conseguir isso, tornando a base de código sustentável a longo prazo.

Declarando e validando a dependência de versão

Como a extensão herda de uma classe PHP fornecida pelo plugin, é fundamental validar que a versão necessária do plugin está presente. Falhar em fazer isso pode causar conflitos que derrubarão o site.

Por exemplo, se a classe AbstractExtension for atualizada com alterações defeituosas e lançar uma versão principal 4.0.0 em relação à anterior 3.4.0, carregar a extensão sem verificar a versão poderá resultar em um erro de PHP, impedindo o carregamento do WordPress.

Para evitar isso, a extensão deve validar que o plugin instalado é da versão 3.x.x. Quando a versão 4.0.0 for instalada, a extensão será desativada, evitando assim os erros.

A extensão pode realizar essa validação usando a seguinte lógica, executada no hook plugins_loaded (uma vez que o plugin principal já estará carregado) no arquivo de plugin principal da extensão. Essa lógica acessa a classe ExtensionManager que está incluída no plugin principal para gerenciar as extensões:

/**
 * 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();
  }
);

Observe como a extensão declara uma dependência na restrição de versão ^1.0 do plugin principal (usando as restrições de versão do Composer). Assim, quando a versão 2.0.0 do Gato GraphQL for instalada, a extensão não será ativada.

A restrição de versão é validada por meio do método ExtensionManager::assertIsValid, que chama Semver::satisfies (fornecido pelo pacote composer/semver):

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

Executando testes de integração em um servidor WordPress

Para facilitar a criação de extensões para seus plugins por desenvolvedores terceiros, forneça a eles ferramentas para desenvolvimento e teste, incluindo fluxos de trabalho para seus processos de integração contínua e entrega contínua (CI/CD).

Durante o desenvolvimento, qualquer pessoa pode facilmente ativar um servidor web usando o DevKinsta, instalar o plugin para o qual está codificando a extensão e validar imediatamente que a extensão é compatível com o plugin.

Para automatizar os testes durante a CI/CD, precisamos ter o servidor web acessível em uma rede para o serviço de CI/CD. Serviços como o InstaWP podem criar um site sandbox com o WordPress instalado para essa finalidade.

Se a base de código da extensão estiver hospedada no GitHub, os desenvolvedores poderão usar o GitHub Actions para executar testes de integração no serviço InstaWP. O fluxo de trabalho a seguir instala a extensão em um site sandbox do InstaWP (juntamente com a versão estável mais recente do plugin principal) e, em seguida, executa os testes de integração:

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

Esse fluxo de trabalho acessa o arquivo .zip por meio do Nightly Link, um serviço que permite acessar um artefato do GitHub sem fazer login, simplificando a configuração do InstaWP.

Lançamento do plugin de extensão

Podemos fornecer ferramentas para ajudar a lançar as extensões, automatizando os procedimentos o máximo possível.

O Monorepo Builder é uma biblioteca para gerenciar qualquer projeto PHP, inclusive um plugin de WordPress. Ele fornece o comando monorepo-builder release para lançar uma versão do projeto, incrementando o componente principal, secundário ou de correção da versão de acordo com o controle de versão semântico.

Esse comando executa uma série de workers de lançamento, que são classes PHP que executam determinada lógica. Os workers padrão incluem um que cria um git tag com a nova versão e outro que envia a tag para o repositório remoto. Os workers personalizados podem ser injetados antes, depois ou entre essas etapas.

Os workers de lançamento são configurados por meio de um arquivo de configuração:

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,
  ]);
};

Podemos fornecer workers de lançamento personalizados para aumentar o processo de lançamento adaptado às necessidades de um plugin de WordPress. Por exemplo, o arquivo InjectStableTagVersionInPluginReadmeFileReleaseWorker define a nova versão como a entrada “Stable tag” no arquivo readme.txt da extensão:

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

Ao adicionar InjectStableTagVersionInPluginReadmeFileReleaseWorker à lista de configuração, sempre que você executar o comando monorepo-builder release para lançar uma nova versão do plugin, a “tag Stable” no arquivo readme .txt da extensão será atualizada automaticamente.

Publicando o plugin de extensão no diretório WP.org

Também podemos distribuir um fluxo de trabalho para ajudar a lançar a extensão no Diretório de Plugins do WordPress. Ao marcar o projeto no repositório remoto, o fluxo de trabalho a seguir publicará o plugin de extensão do WordPress no diretório:

# 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 }}

Esse fluxo de trabalho usa a ação 10up/action-wordpress-plugin-deploy, que recupera o código de um repositório Git e o envia ao repositório SVN do WordPress.org, simplificando a operação.

Resumo

Ao criar um plugin extensível para o WordPress, nosso objetivo é facilitar ao máximo para que desenvolvedores terceiros possam estendê-lo, maximizando assim as chances de promover um ecossistema vibrante em torno de nossos plugins.

Ao passo que fornecer uma ampla documentação pode orientar os desenvolvedores sobre como estender o plugin, uma abordagem ainda mais eficaz é fornecer o código PHP e as ferramentas necessárias para desenvolver, testar e lançar suas extensões.

Ao incluir o código adicional necessário para as extensões diretamente em nosso plugin, simplificamos o processo para os desenvolvedores.

Você planeja tornar seu plugin de WordPress extensível? Informe-nos na seção de comentários.

Leonardo Losoviz

Leo writes about innovative web development trends, mostly concerning PHP, WordPress and GraphQL. You can find him at leoloso.com and twitter.com/losoviz.