Los plugins de WordPress pueden ampliarse con funciones adicionales, como demuestran plugins populares como WooCommerce y Gravity Forms. En el artículo «Diseñar un plugin de WordPress para que admita extensiones«, aprendemos que hay dos formas principales de hacer que un plugin de WordPress sea extensible:

  1. Configurando hooks (acciones y filtros) para que los plugins de extensión inyecten su propia funcionalidad.
  2. Proporcionando clases PHP que los plugins de extensión puedan heredar

El primer método se basa más en la documentación, detallando los hooks disponibles y su uso. El segundo método, por el contrario, ofrece código listo para usar por las extensiones, reduciendo la necesidad de una extensa documentación. Esto es ventajoso porque crear documentación junto al código puede complicar la gestión y liberación del plugin.

Proporcionar directamente clases PHP sustituye eficazmente la documentación por código. En lugar de enseñar cómo implementar una función, el plugin proporciona el código PHP necesario, simplificando la tarea a los desarrolladores externos.

Vamos a explorar algunas técnicas para conseguirlo, con el objetivo final de fomentar un ecosistema de integraciones en torno a nuestro plugin de WordPress.

Definición de clases base en PHP en el plugin de WordPress

El plugin de WordPress incluirá clases PHP destinadas a ser utilizadas por plugins de extensión. Estas clases PHP podrían no ser utilizadas por el propio plugin principal, sino que se proporcionan específicamente para que otros las utilicen.

Veamos cómo se implementa esto en el plugin de código abierto Gato GraphQL.

Clase AbstractPlugin:

AbstractPlugin representa un plugin, tanto para el plugin principal Gato GraphQL como para sus extensiones:

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

  // ...
}

Clase AbstractMainPlugin:

AbstractMainPlugin extiende AbstractPlugin para representar el 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,
    );
  }

  // ...
}

Clase AbstractExtension:

Del mismo modo AbstractExtension extiende AbstractPlugin para representar un plugin de extensión:

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

  // ...
}

Observa que AbstractExtension se incluye dentro del plugin principal, proporcionando funcionalidad para registrar e inicializar una extensión. Sin embargo, sólo lo utilizan las extensiones, no el propio plugin principal.

La clase AbstractPlugin contiene código de inicialización compartido que se invoca en distintos momentos. Estos métodos se definen en el nivel de la clase base, pero son invocados por las clases herederas según sus ciclos de vida.

El plugin principal y las extensiones se inicializan ejecutando el método setup en la clase correspondiente, invocado desde dentro del archivo principal del plugin de WordPress.

Por ejemplo, en Gato GraphQL, esto se hace en gatographql.php:

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

Método setup:

En el nivel de la clase base, setup contiene la lógica común entre el plugin y sus extensiones, como anular su registro cuando se desactiva el plugin. Este método no es final; puede ser sobrescrito por las clases herederas para añadir su funcionalidad:

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 del plugin principal:

El método setup del plugin principal inicializa el ciclo de vida de la aplicación. Ejecuta la funcionalidad del plugin principal a través de métodos como initialize, configureComponents, configure, y boot, y activa los hooks de acción correspondientes para las extensiones:

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 de configuración de extensiones:

La clase AbstractExtension ejecuta su lógica en los hooks correspondientes:

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

Los métodos initialize, configureComponents, configure, y boot son comunes tanto al plugin principal como a las extensiones y pueden compartir lógica. Esta lógica compartida se almacena en la clase AbstractPlugin.

Por ejemplo, el método configure configura el plugin o las extensiones, llamando a callPluginInitializationConfiguration, que tiene implementaciones diferentes para el plugin principal y las extensiones y se define como abstracto, y a getModuleClassConfiguration, que proporciona un comportamiento por defecto pero que puede sobrescribirse si es necesario:

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

El plugin principal proporciona su implementación para callPluginInitializationConfiguration:

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

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

Del mismo modo, la clase de extensión proporciona su implementación:

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

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

Los métodos initialize, configureComponents y boot se definen en el nivel de la clase base y pueden ser sobrescritos por las clases herederas:

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 los métodos pueden ser sobrescritos por AbstractMainPlugin o AbstractExtension para ampliarlos con su funcionalidad personalizada.

Para el plugin principal, el método setup también elimina cualquier almacenamiento en caché de la instancia de WordPress cuando se activa o desactiva el plugin o cualquiera de sus extensiones:

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
  }

  // ...
}

Del mismo modo, el método deactivate elimina el caché y boot ejecuta hooks de acción adicionales sólo para el 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;
      }
    );
  }
}

De todo el código presentado anteriormente, queda claro que cuando diseñamos y programamos un plugin de WordPress, debemos tener en cuenta las necesidades de sus extensiones y reutilizar el código en ellas tanto como sea posible. Implementar patrones sólidos de Programación Orientada a Objetos (como los principios SOLID) ayuda a conseguirlo, haciendo que la base de código sea mantenible a largo plazo.

Declarar y validar la dependencia de la versión

Puesto que la extensión hereda de una clase PHP proporcionada por el plugin, es crucial validar que la versión requerida del plugin está presente. No hacerlo podría causar conflictos que hagan caer el sitio.

Por ejemplo, si la clase AbstractExtension se actualiza con cambios importantes y libera una versión principal 4.0.0 desde la  versión anterior 3.4.0, cargar la extensión sin comprobar la versión podría provocar un error PHP, impidiendo la carga de WordPress.

Para evitarlo, la extensión debe validar que el plugin instalado es la versión 3.x.x. Cuando se instale la versión 4.0.0, la extensión se desactivará, evitando así errores.

La extensión puede realizar esta validación utilizando la siguiente lógica, que se ejecuta en el hook plugins_loaded (ya que el plugin principal estará cargado para entonces) en el archivo del plugin principal de la extensión. Esta lógica accede a la clase ExtensionManager que se incluye en el plugin principal para gestionar las extensiones:

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

Observa cómo la extensión declara una dependencia de la restricción de versión ^1.0 del plugin principal (utilizando las restricciones de versión de Composer). Así, cuando se instale la versión 2.0.0 de Gato GraphQL, la extensión no se activará.

La restricción de versión se valida mediante el método ExtensionManager::assertIsValid, que llama a Semver::satisfies (proporcionado por el paquete 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;
    });
  }
}

Ejecutar pruebas de integración contra un servidor WordPress

Para facilitar a los desarrolladores externos la creación de extensiones para tus plugins, proporciónales herramientas para el desarrollo y las pruebas, incluidos flujos de trabajo para sus procesos de integración continua y entrega continua (CI/CD).

Durante el desarrollo, cualquiera puede poner en marcha fácilmente un servidor web con DevKinsta, instalar el plugin para el que está programando la extensión y validar inmediatamente que la extensión es compatible con el plugin.

Para automatizar las pruebas durante el CI/CD, necesitamos que el servidor web sea accesible a través de una red al servicio CI/CD. Servicios como InstaWP pueden crear un sitio sandbox con WordPress instalado para este fin.

Si el código base de la extensión está alojado en GitHub, los desarrolladores pueden utilizar las Acciones de GitHub para ejecutar pruebas de integración contra el servicio InstaWP. El siguiente flujo de trabajo instala la extensión en un sitio sandbox de InstaWP (junto con la última versión estable del plugin principal) y, a continuación, ejecuta las pruebas de integración:

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

Este flujo de trabajo accede al archivo .zip a través de Nightly Link, un servicio que permite acceder a un artefacto desde GitHub sin iniciar sesión, simplificando la configuración de InstaWP.

Liberar el plugin de extensión

Podemos proporcionar herramientas para ayudar a liberar las extensiones, automatizando los procedimientos tanto como sea posible.

Monorepo Builder es una biblioteca para gestionar cualquier proyecto PHP, incluido un plugin de WordPress. Proporciona el comando monorepo-builder release para liberar una versión del proyecto, incrementando el componente mayor, menor o parche de la versión según el versionado semántico.

Este comando ejecuta una serie de release workers, que son clases PHP que ejecutan cierta lógica. Los workers por defecto incluyen uno que crea un git tag con la nueva versión y otro que envía la etiqueta al repositorio remoto. Se pueden inyectar workers personalizados antes, después o entre estos pasos.

Los workers de publicación se configuran mediante un archivo de configuración:

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 proporcionar release workers personalizados para aumentar el proceso de liberación adaptado a las necesidades de un plugin de WordPress. Por ejemplo, el InjectStableTagVersionInPluginReadmeFileReleaseWorker establece la nueva versión como la entrada «Stable tag» en el archivo readme.txt de la extensión:

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

Añadiendo InjectStableTagVersionInPluginReadmeFileReleaseWorker a la lista de configuración, cada vez que ejecutes el comando monorepo-builder release para liberar una nueva versión del plugin, se actualizará automáticamente «Stable tag» del archivo readme.txt de la extensión.

Publicar el plugin de extensión en el directorio WP.org

También podemos distribuir un flujo de trabajo para ayudar a publicar la extensión en el Directorio de Plugins de WordPress. Al etiquetar el proyecto en el repositorio remoto, el siguiente flujo de trabajo publicará el plugin de extensión de WordPress en el directorio:

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

Este flujo de trabajo utiliza la acción 10up/action-wordpress-plugin-deploy que recupera el código de un repositorio Git y lo envía al repositorio SVN de WordPress.org, simplificando la operación.

Resumen

Al crear un plugin extensible para WordPress, nuestro objetivo es facilitar al máximo su ampliación por parte de desarrolladores externos, maximizando así las posibilidades de fomentar un ecosistema vibrante en torno a nuestros plugins.

Aunque proporcionar una amplia documentación puede guiar a los desarrolladores sobre cómo ampliar el plugin, un enfoque aún más eficaz es proporcionar el código PHP y las herramientas necesarias para desarrollar, probar y publicar sus extensiones.

Al incluir el código adicional que necesitan las extensiones directamente en nuestro plugin, simplificamos el proceso para los desarrolladores.

¿Planeas hacer extensible tu plugin de WordPress? Háznoslo saber en la sección de comentarios.

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.