Les extensions WordPress peuvent être étendues avec des fonctionnalités supplémentaires, comme le démontrent des extensions populaires telles que WooCommerce et Gravity Forms. Dans l’article « Architecturer une extension WordPress pour prendre en charge les extensions », nous apprenons qu’il existe deux façons principales de rendre une extension WordPress extensible :

  1. En mettant en place des hooks (actions et filtres) pour que les plugins d’extension puissent injecter leurs propres fonctionnalités.
  2. En fournissant des classes PHP dont les plugins d’extension peuvent hériter.

La première méthode repose davantage sur la documentation, détaillant les crochets disponibles et leur utilisation. La deuxième méthode, en revanche, propose un code prêt à l’emploi pour les extensions, ce qui réduit la nécessité d’une documentation approfondie. C’est un avantage car la création d’une documentation à côté du code peut compliquer la gestion et la publication de l’extension.

Le fait de fournir directement des classes PHP remplace efficacement la documentation par du code. Au lieu d’enseigner comment mettre en œuvre une fonctionnalité, l’extension fournit le code PHP nécessaire, ce qui simplifie la tâche des développeurs tiers.

Explorons quelques techniques pour y parvenir, dans le but ultime de favoriser un écosystème d’intégrations autour de notre extnsion WordPress.

Définir les classes PHP de base dans le plugin WordPress

L’extension WordPress comprendra des classes PHP destinées à être utilisées par les plugins d’extension. Ces classes PHP peuvent ne pas être utilisées par l’extension principale elle-même, mais sont fournies spécifiquement pour que d’autres puissent les utiliser.

Voyons comment cela est mis en œuvre dans l’extension open source Gato GraphQL.

Classe AbstractPlugin :

AbstractPlugin représente une extension, à la fois pour l’extension principale Gato GraphQL et pour ses extensions :

abstract class AbstractPlugin implements PluginInterface
{
  protected string $pluginBaseName;
  protected string $pluginSlug;
  protected string $pluginName;

  public function __construct(
    protected string $pluginFile,
    protected string $pluginVersion,
    ?string $pluginName,
  ) {
    $this->pluginBaseName = plugin_basename($pluginFile);
    $this->pluginSlug = dirname($this->pluginBaseName);
    $this->pluginName = $pluginName ?? $this->pluginBaseName;
  }

  public function getPluginName(): string
  {
    return $this->pluginName;
  }

  public function getPluginBaseName(): string
  {
    return $this->pluginBaseName;
  }

  public function getPluginSlug(): string
  {
    return $this->pluginSlug;
  }

  public function getPluginFile(): string
  {
    return $this->pluginFile;
  }

  public function getPluginVersion(): string
  {
    return $this->pluginVersion;
  }

  public function getPluginDir(): string
  {
    return dirname($this->pluginFile);
  }

  public function getPluginURL(): string
  {
    return plugin_dir_url($this->pluginFile);
  }

  // ...
}

Classe AbstractMainPlugin :

AbstractMainPlugin étend AbstractPlugin pour représenter l’extension principale :

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 :

De même, AbstractExtension étend AbstractPlugin pour représenter un plugin d’extension :

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

  // ...
}

Remarquez que AbstractExtension est inclus dans l’extension principale, fournissant la fonctionnalité d’enregistrement et d’initialisation d’une extension. Cependant, il n’est utilisé que par les extensions, et non par l’extension principale elle-même.

La classe AbstractPlugin contient un code d’initialisation partagé, invoqué à différents moments. Ces méthodes sont définies au niveau de l’ancêtre mais sont invoquées par les classes qui en héritent en fonction de leur cycle de vie.

L’extension principale et les autres extensions sont initialisées en exécutant la méthode setup sur la classe correspondante, invoquée depuis le fichier principal de l’extnsion WordPress.

Par exemple, dans Gato GraphQL, cela se fait dans gatographql.php:

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

Méthode setup :

Au niveau de l’ancêtre, setup contient la logique commune entre l’extension et ses extensions, comme la désinscription de ces dernières lorsque l’extension est désactivée. Cette méthode n’est pas finale ; elle peut être surchargée par les classes qui en héritent pour ajouter leurs fonctionnalités :

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

La méthode setup du plugin principal :

La méthode setup de l’extension principale initialise le cycle de vie de l’application. Elle exécute les fonctionnalités de l’extnsion principale par le biais de méthodes telles que initialize, configureComponents, configure, et boot, et déclenche les crochets d’action correspondants pour les extensions :

abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
  public function setup(): void
  {
    parent::setup();

    add_action('plugins_loaded', function (): void
    {
      // 1. Initialize main plugin
      $this->initialize();

      // 2. Initialize extensions
      do_action('gatographql:initializeExtension');

      // 3. Configure main plugin components
      $this->configureComponents();

      // 4. Configure extension components
      do_action('gatographql:configureExtensionComponents');

      // 5. Configure main plugin
      $this->configure();

      // 6. Configure extension
      do_action('gatographql:configureExtension');

      // 7. Boot main plugin
      $this->boot();

      // 8. Boot extension
      do_action('gatographql:bootExtension');
    }

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

Méthode setup de l’extension :

La classe AbstractExtension exécute sa logique sur les hooks correspondants :

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

Les méthodes initialize, configureComponents, configure, et boot sont communes à l’extension principale et aux autres extensions et peuvent partager leur logique. Cette logique partagée est stockée dans la classe AbstractPlugin.

Par exemple, la méthode configure configure l’extension ou les autres extensions, en appelant callPluginInitializationConfiguration, qui a des implémentations différentes pour l’extension principale et les autres extensions et qui est défini comme abstrait et getModuleClassConfiguration, qui fournit un comportement par défaut mais qui peut être surchargé si nécessaire :

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

L’extension principale fournit son implémentation pour callPluginInitializationConfiguration:

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

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

De même, la classe d’extension fournit son implémentation :

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

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

Les méthodes initialize, configureComponents et boot sont définies au niveau de l’ancêtre et peuvent être surchargées par les classes qui en héritent :

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

Toutes les méthodes peuvent être remplacées par AbstractMainPlugin ou AbstractExtension pour les étendre avec leur fonctionnalité personnalisée.

Pour l’extension principale, la méthode setup supprime également toute mise en cache de l’instance WordPress lorsque l’extension ou l’une de ses extensions est activée ou désactivée :

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
  }

  // ...
}

De même, la méthode deactivate supprime la mise en cache et boot exécute des hooks d’action supplémentaires pour l’extension principale uniquement :

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

D’après tout le code présenté ci-dessus, il est clair que lors de la conception et du codage d’une extension WordPress, nous devons prendre en compte les besoins de ses extensions et réutiliser le code entre elles autant que possible. La mise en œuvre de modèles de programmation orientée objet (tels que les principes SOLID) permet d’atteindre cet objectif et de rendre la base de code maintenable à long terme.

Déclarer et valider la dépendance de la version

Étant donné que l’extension hérite d’une classe PHP fournie par l’extension, il est crucial de valider que la version nécessaire de l’extension est présente. Si vous ne le faites pas, vous risquez de provoquer des conflits qui entraîneront l’arrêt du site.

Par exemple, si la classe AbstractExtension est mise à jour avec des changements de rupture et publie une version majeure 4.0.0 par rapport à la précédente 3.4.0, le chargement de l’extension sans vérification de la version pourrait entraîner une erreur PHP, empêchant le chargement de WordPress.

Pour éviter cela, l’extension doit valider que l’extension installée est la version 3.x.x. Lorsque la version 4.0.0 est installée, l’extension sera désactivée, ce qui évitera les erreurs.

L’extension peut effectuer cette validation en utilisant la logique suivante, exécutée sur le hook plugins_loaded (puisque l’extension principale sera chargée à ce moment-là) dans le fichier de plugin principal de l’extension. Cette logique accède à la classe ExtensionManager qui est incluse dans le plugin principal pour gérer les extensions :

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

Remarquez que l’extension déclare une dépendance sur la contrainte de version ^1.0 de l’extension principale (en utilisant les contraintes de version de Composer). Ainsi, lorsque la version 2.0.0 de Gato GraphQL est installée, l’extension ne sera pas activée.

La contrainte de version est validée par la méthode ExtensionManager::assertIsValid, qui appelle Semver::satisfies (fournie par le paquetage 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;
    });
  }
}

Exécution de tests d’intégration sur un serveur WordPress

Pour que les développeurs tiers puissent créer plus facilement des extensions pour vous extnsions, mettez à leur disposition des outils de développement et de test, notamment des flux de travail pour leurs processus d’intégration et de livraison continues (CI/CD).

Pendant le développement, n’importe qui peut facilement faire tourner un serveur web à l’aide de DevKinsta, installer l’extension pour lequel il code l’extension, et valider immédiatement que l’extension est compatible avec l’extension.

Pour automatiser les tests pendant le processus CI/CD, nous avons besoin que le serveur web soit accessible via un réseau au service CI/CD. Des services tels que InstaWP peuvent créer un site bac à sable avec WordPress installé à cette fin.

Si la base de code de l’extension est hébergée sur GitHub, les développeurs peuvent utiliser les Actions GitHub pour exécuter des tests d’intégration contre le service InstaWP. Le flux de travail suivant installe l’extension sur un site sandbox InstaWP (aux côtés de la dernière version stable du plugin principal) et exécute ensuite les tests d’intégration:

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

Ce workflow accède au fichier .zip via Nightly Link, un service qui permet d’accéder à un artefact de GitHub sans se connecter, ce qui simplifie la configuration d’InstaWP.

Publication du plugin d’extension

Nous pouvons fournir des outils pour aider à libérer les extensions, en automatisant les procédures autant que possible.

Monorepo Builder est une bibliothèque permettant de gérer n’importe quel projet PHP, y compris une extension WordPress. Elle fournit la commande monorepo-builder release pour publier une version du projet, en incrémentant le composant majeur, mineur ou correctif de la version conformément au versionnement sémantique.

Cette commande exécute une série d’agents de publication, qui sont des classes PHP qui exécutent une certaine logique. Les agents par défaut comprennent un agent qui crée une page git tag avec la nouvelle version et un autre qui pousse la balise vers le dépôt distant. Des agents personnalisés peuvent être injectés avant, après ou entre ces étapes.

Les agents de validation sont configurés par le biais d’un fichier de configuration :

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

Nous pouvons fournir des workers de libération personnalisés pour augmenter le processus de libération adapté aux besoins d’une extension WordPress. Par exemple, le fichier de configuration InjectStableTagVersionInPluginReadmeFileReleaseWorker définit la nouvelle version comme l’entrée « Stable tag » dans le fichier readme.txt de l’extension:

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

En ajoutant InjectStableTagVersionInPluginReadmeFileReleaseWorker à la liste de configuration, chaque fois que l’on exécute la commande monorepo-builder release pour publier une nouvelle version de l’extension, « Stable tag » dans le fichier readme.txt de l’extension sera automatiquement mise à jour.

Publication du plugin d’extension dans le répertoire WP.org

Nous pouvons également distribuer un flux de travail pour aider à publier l’extension dans le répertoire des extensions WordPress. Lors du marquage du projet sur le dépôt distant, le flux de travail suivant publiera le plugin d’extension WordPress dans le dépôt :

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

Ce flux de travail utilise l’action 10up/action-wordpress-plugin-deploy qui récupère le code d’un dépôt Git et le pousse vers le dépôt SVN de WordPress.org, ce qui simplifie l’opération.

Résumé

Lorsque l’on crée une extension extensible pour WordPress, notre objectif est de faire en sorte qu’il soit aussi facile que possible pour les développeurs tiers de l’étendre, maximisant ainsi les chances de favoriser un écosystème dynamique autour de nos extensions.

Bien qu’une documentation complète puisse guider les développeurs sur la façon d’étendre l’extension, une approche encore plus efficace consiste à fournir le code PHP et les outils nécessaires au développement, aux tests et à la publication de leurs extensions.

En incluant le code supplémentaire nécessaire aux extensions directement dans notre extension, nous simplifions le processus pour les développeurs.

Avez-vous l’intention de rendre votre extension WordPress extensible ? Faites-le nous savoir dans la section des commentaires.

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.