I plugin di WordPress possono essere estesi con funzionalità aggiuntive, come dimostrano plugin popolari come WooCommerce e Gravity Forms. Nell’articolo “Progettare un plugin WordPress che supporti le estensioni“, abbiamo visto che esistono due modi principali per rendere estensibile un plugin WordPress:

  1. Creando degli hook (azioni e filtri) che permettano ai plugin di estendere le proprie funzionalità.
  2. Fornendo delle classi PHP che i plugin di estensione possono ereditare.

Il primo metodo si basa maggiormente sulla documentazione, che illustra in dettaglio gli hook disponibili e il loro utilizzo. Il secondo metodo, invece, offre codice pronto all’uso per le estensioni, riducendo la necessità di una documentazione esaustiva. Questo è un vantaggio perché la creazione di documentazione accanto al codice può complicare la gestione e il rilascio del plugin.

La fornitura diretta di classi PHP sostituisce efficacemente la documentazione con il codice. Invece di insegnare come implementare una funzione, il plugin fornisce il codice PHP necessario, semplificando il compito agli sviluppatori di terze parti.

Esploriamo alcune tecniche per raggiungere questo obiettivo, con l’obiettivo finale di promuovere un ecosistema di integrazioni intorno al nostro plugin WordPress.

Definire le classi PHP di base nel plugin WordPress

Il plugin di WordPress includerà classi PHP destinate all’utilizzo da parte dei plugin di estensione. Queste classi PHP potrebbero non essere utilizzate dal plugin principale, ma sono fornite appositamente per essere utilizzate da altri.

Vediamo come questo viene implementato nel plugin open-source Gato GraphQL.

Classe AbstractPlugin:

AbstractPlugin rappresenta un plugin, sia per il plugin principale di Gato GraphQL che per le sue estensioni:

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 per rappresentare il plugin 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,
    );
  }

  // ...
}

AbstractExtension:

Allo stesso modo, AbstractExtension estende AbstractPlugin per rappresentare un plugin di estensione:

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

  // ...
}

Da notare che AbstractExtension è incluso nel plugin principale e fornisce le funzionalità per registrare e inizializzare un’estensione. Tuttavia, viene utilizzato solo dalle estensioni e non dal plugin principale.

La classe AbstractPlugin contiene codice di inizializzazione condiviso invocato in momenti diversi. Questi metodi sono definiti a livello di antenato ma vengono invocati dalle classi ereditarie in base al loro ciclo di vita.

Il plugin principale e le estensioni vengono inizializzati eseguendo il metodo setup sulla classe corrispondente, invocato dal file principale del plugin WordPress.

Ad esempio, in Gato GraphQL, questo avviene in gatographql.php:

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

Metodo setup:

A livello di antenati, setup contiene la logica comune tra il plugin e le sue estensioni, come ad esempio la loro disiscrizione quando il plugin viene disattivato. Questo metodo non è definitivo; può essere sovrascritto dalle classi ereditarie per aggiungere le loro funzionalità:

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

Metodo setup del plugin principale:

Il metodo setup del plugin principale inizializza il ciclo di vita dell’applicazione. Esegue le funzionalità del plugin principale attraverso metodi come initialize, configureComponents, configure e boot e attiva gli hook di azione corrispondenti per le estensioni:

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

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

Metodo di configurazione delle estensioni:

La classe AbstractExtension esegue la sua logica sugli hook corrispondenti:

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

I metodi initialize, configureComponents, configure e boot sono comuni sia al plugin principale che alle estensioni e possono condividere la logica. Questa logica condivisa è memorizzata nella classe AbstractPlugin.

Ad esempio, il metodo configure configura il plugin o le estensioni, chiamando callPluginInitializationConfiguration, che ha implementazioni diverse per il plugin principale e le estensioni ed è definito come astratto e getModuleClassConfiguration, che fornisce un comportamento predefinito ma può essere sovrascritto se necessario:

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

Il plugin principale fornisce la sua implementazione per callPluginInitializationConfiguration:

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

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

Allo stesso modo, la classe di estensione fornisce la sua implementazione:

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

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

I metodi initialize, configureComponents e boot sono definiti a livello di antenato e possono essere sovrascritti dalle classi ereditarie:

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

Tutti i metodi possono essere sovrascritti da AbstractMainPlugin o AbstractExtension per estenderli con le loro funzionalità personalizzate.

Per il plugin principale, il metodo setup rimuove anche la cache dall’istanza di WordPress quando il plugin o una delle sue estensioni viene attivata o disattivata:

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
  }

  // ...
}

Allo stesso modo, il metodo deactivate rimuove la cache e boot esegue hook di azione aggiuntivi solo per il plugin principale:

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

Da tutto il codice presentato sopra, è chiaro che quando si progetta e si codifica un plugin di WordPress, è necessario considerare le esigenze delle sue estensioni e riutilizzare il codice il più possibile. L’implementazione di validi modelli di programmazione orientata agli oggetti (come i principi SOLID ) aiuta a raggiungere questo obiettivo, rendendo la base di codice manutenibile a lungo termine.

Dichiarare e convalidare la dipendenza dalla versione

Poiché l’estensione eredita da una classe PHP fornita dal plugin, è fondamentale convalidare la presenza della versione richiesta del plugin. In caso contrario, potrebbero verificarsi conflitti che potrebbero causare il blocco del sito.

Ad esempio, se la classe AbstractExtension viene aggiornata con modifiche di rottura e rilascia una versione maggiore 4.0.0 rispetto alla precedente 3.4.0, caricare l’estensione senza verificare la versione potrebbe causare un errore PHP, impedendo il caricamento di WordPress.

Per evitare questo problema, l’estensione deve convalidare che il plugin installato sia la versione 3.x.x. Quando viene installata la versione 4.0.0, l’estensione viene disabilitata, evitando così gli errori.

L’estensione può effettuare questa convalida utilizzando la seguente logica, eseguita sull’hook plugins_loaded (poiché il plugin principale sarà già stato caricato) nel file del plugin principale dell’estensione. Questa logica accede alla classe ExtensionManager che è inclusa nel plugin principale per gestire le estensioni:

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

Da notare come l’estensione dichiari una dipendenza dal vincolo di versione ^1.0 del plugin principale (utilizzando i vincoli di versione di Composer). Pertanto, quando viene installata la versione 2.0.0 di Gato GraphQL, l’estensione non verrà attivata.

Il vincolo di versione viene convalidato tramite il metodo ExtensionManager::assertIsValid, che chiama Semver::satisfies (fornito dal pacchetto 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;
    });
  }
}

Eseguire test di integrazione su un server WordPress

Per facilitare gli sviluppatori di terze parti nella creazione di estensioni per i vostri plugin, fornite loro strumenti per lo sviluppo e il test, compresi i flussi di lavoro per i loro processi di continuous integration e continuous delivery (CI/CD).

Durante lo sviluppo, chiunque può facilmente avviare un server web utilizzando DevKinsta, installare il plugin per il quale si sta progettando l’estensione e convalidare immediatamente la compatibilità dell’estensione con il plugin.

Per automatizzare i test durante il CI/CD, è necessario che il server web sia accessibile in rete al servizio CI/CD. Servizi come InstaWP possono creare un sito sandbox con WordPress installato a questo scopo.

Se la base di codice dell’estensione è ospitata su GitHub, gli sviluppatori possono utilizzare GitHub Actions per eseguire test di integrazione contro il servizio InstaWP. Il seguente flusso di lavoro installa l’estensione su un sito sandbox InstaWP (insieme all’ultima versione stabile del plugin principale) e poi esegue i test di integrazione:

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

Questo flusso di lavoro accede al file .zip tramite Nightly Link, un servizio che consente di accedere a un artefatto da GitHub senza effettuare il login, semplificando la configurazione di InstaWP.

Rilasciare il plugin di estensione

Possiamo fornire strumenti per aiutare a rilasciare le estensioni, automatizzando il più possibile le procedure.

Monorepo Builder è una libreria per la gestione di qualsiasi progetto PHP, compreso un plugin WordPress. Fornisce il comando monorepo-builder release per rilasciare una versione del progetto, incrementando la componente maggiore, minore o patch della versione in base al versioning semantico.

Questo comando esegue una serie di release worker, ovvero classi PHP che eseguono una determinata logica. I worker predefiniti includono uno che crea un git tag con la nuova versione e un altro che invia il tag al repository remoto. I worker personalizzati possono essere iniettati prima, dopo o tra questi passaggi.

I worker di rilascio sono configurati tramite un file di configurazione:

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

Possiamo fornire dei release worker personalizzati per aumentare il processo di rilascio in base alle esigenze di un plugin WordPress. Ad esempio, il file InjectStableTagVersionInPluginReadmeFileReleaseWorker imposta la nuova versione come voce “Stable tag” nel file readme.txt dell’estensione:

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

Aggiungendo InjectStableTagVersionInPluginReadmeFileReleaseWorker all’elenco di configurazione, ogni volta che si esegue il comando monorepo-builder release per rilasciare una nuova versione del plugin, il “tag Stable” nel file readme.txt dell’estensione verrà aggiornato automaticamente.

Pubblicare il plugin di estensione nella directory di WP.org

Possiamo anche distribuire un workflow per aiutare a rilasciare l’estensione nella directory dei plugin di WordPress. Quando si assegna il tag al progetto sul repository remoto, il seguente flusso di lavoro pubblicherà il plugin di estensione WordPress nella directory:

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

Questo flusso di lavoro utilizza l’azione 10up/action-wordpress-plugin-deploy che recupera il codice da un repository Git e lo invia al repository SVN di WordPress.org, semplificando l’operazione.

Riepilogo

Quando creiamo un plugin estensibile per WordPress, il nostro obiettivo è quello di rendere il più semplice possibile l’estensione da parte di sviluppatori terzi, massimizzando così le possibilità di promuovere un ecosistema vivace intorno ai nostri plugin.

Sebbene fornire un’ampia documentazione possa guidare gli sviluppatori su come estendere il plugin, un approccio ancora più efficace è quello di fornire il codice PHP e gli strumenti necessari per sviluppare, testare e rilasciare le loro estensioni.

Includendo il codice aggiuntivo necessario alle estensioni direttamente nel nostro plugin, semplifichiamo il processo per gli sviluppatori.

Avete intenzione di rendere estensibile il vostro plugin per WordPress? Fatecelo sapere nella sezione commenti.

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.