WordPress plugins kunnen worden uitgebreid met extra functionaliteit, zoals populaire plugins als WooCommerce en Gravity Forms laten zien. In het artikel  “Een WordPress plugin maken om extensies te ondersteunen” leren we dat er twee manieren zijn om een WordPress plugin uitbreidbaar te maken:

  1. Door hooks  (acties en filters) in te stellen waarmee uitbreidingsplugins hun eigen functionaliteit kunnen injecteren
  2. Door PHP klassen aan te bieden die uitbreidingsplugins kunnen erven (inheriten)

De eerste methode is meer gebaseerd op documentatie, waarin de beschikbare hooks en hun gebruik gedetailleerd worden beschreven. De tweede methode biedt daarentegen kant-en-klare code voor extensies, waardoor er minder uitgebreide documentatie nodig is. Dit is voordelig omdat het maken van documentatie naast code het beheer en de uitgave van de plugin kan bemoeilijken.

Het direct aanbieden van PHP klassen vervangt documentatie effectief door code. In plaats van te leren hoe een functie geïmplementeerd moet worden, levert de plugin de benodigde PHP code, wat de taak voor externe ontwikkelaars vereenvoudigt.

Laten we een aantal technieken verkennen om dit te bereiken, met als uiteindelijke doel het bevorderen van een ecosysteem van integraties rondom onze WordPress plugin.

PHP basisklassen definiëren in de WordPress plugin

De WordPress plugin zal PHP klassen bevatten die bedoeld zijn voor gebruik door uitbreidingsplugins. Deze PHP klassen worden wellicht niet gebruikt door de hoofdplugin zelf, maar worden specifiek geleverd voor gebruik door anderen.

Laten we eens kijken hoe dit is geïmplementeerd in de open-source Gato GraphQL plugin.

De klasse AbstractPlugin:

AbstractPlugin staat voor een plugin, zowel voor de hoofd-Gato GraphQL plugin als voor zijn uitbreidingen:

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

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

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

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

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

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

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

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

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

  // ...
}

AbstractMainPlugin klasse:

AbstractMainPlugin breidt AbstractPlugin uit om de hoofdplugin te representeren:

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 klasse:

Evenzo, AbstractExtensionAbstractPlugin om een uitbreidingsplugin weer te geven:

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

  // ...
}

Merk op dat AbstractExtension is opgenomen in de hoofdplugin en functionaliteit biedt om een extensie te registreren en te initialiseren. Het wordt echter alleen gebruikt door extensies, niet door de hoofdplugin zelf.

De klasse AbstractPlugin bevat gedeelde initialisatiecode die op verschillende momenten wordt gecalld. Deze methoden zijn gedefinieerd op het niveau van de ancestor, maar worden gecalld door de inheriting klassen volgens hun levenscycli.

De hoofdplugin en de uitbreidingen worden geïnitialiseerd door de methode setup uit te voeren op de corresponderende klasse, gecalld vanuit het hoofdbestand van de WordPress plugin.

In Gato GraphQL wordt dit bijvoorbeeld gedaan in gatographql.php:

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

setup methode:

Op het niveau van de ancestor bevat setup de gemeenschappelijke logica tussen de plugin en zijn uitbreidingen, zoals het uitschrijven van de plugin wanneer deze wordt gedeactiveerd. Deze methode is niet definitief; het kan overschreven worden door de inheriting klassen om hun functionaliteit toe te voegen:

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

De setup methode van de hoofdplugin:

De methode setup van de hoofdplugin initialiseert de levenscyclus van de applicatie. Het voert de functionaliteit van de hoofdplugin uit via methodes als initialize, configureComponents, configure, en boot, en activeert overeenkomstige action hooks voor uitbreidingen:

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

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

Extension setup methode:

De klasse AbstractExtension voert zijn logica uit op de overeenkomstige hooks:

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

Methoden initialize, configureComponents, configure, en boot zijn gemeenschappelijk voor zowel de hoofdplugin als de uitbreidingen en kunnen logica delen. Deze gedeelde logica wordt opgeslagen in de klasse AbstractPlugin.

De methode configure configureert bijvoorbeeld de plugin of uitbreidingen, door callPluginInitializationConfiguration te callen, die verschillende implementaties heeft voor de hoofdplugin en uitbreidingen en gedefinieerd is als abstract en getModuleClassConfiguration, die een standaard gedrag geeft maar overschreven kan worden als dat nodig is:

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

De hoofdplugin levert zijn implementatie voor callPluginInitializationConfiguration:

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

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

Op dezelfde manier levert de uitbreidingsklasse zijn implementatie:

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

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

Methoden initialize, configureComponents en boot zijn gedefinieerd op het niveau van de ancestor en kunnen worden overschreven door overervende klassen:

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

Alle methoden kunnen overschreven worden door AbstractMainPlugin of AbstractExtension om ze uit te breiden met hun eigen functionaliteit.

Voor de hoofdplugin verwijdert de methode setup ook alle caching van de WordPress instantie wanneer de plugin of een van zijn uitbreidingen wordt geactiveerd of gedeactiveerd:

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
  }

  // ...
}

Op dezelfde manier verwijdert de methode deactivate caching en voert boot alleen voor de hoofdplugin extra action hooks uit:

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

Uit alle hierboven gepresenteerde code is het duidelijk dat we bij het ontwerpen en coderen van een WordPress plugin rekening moeten houden met de behoeften van de uitbreidingen en dat we code zoveel mogelijk moeten hergebruiken. Het implementeren van goede Object-georiënteerde programmeerpatronen (zoals de SOLID principes) helpt om dit te bereiken, waardoor de codebase onderhoudbaar wordt voor de lange termijn.

De versieafhankelijkheid declareren en valideren

Omdat de extensie erft van een PHP klasse die wordt geleverd door de plugin, is het cruciaal om te valideren dat de vereiste versie van de plugin aanwezig is. Als je dat niet doet, kan dat conflicten veroorzaken die de site platleggen.

Als bijvoorbeeld de klasse AbstractExtension wordt bijgewerkt met cruciale wijzigingen en een major versie 4.0.0 uitbrengt ten opzichte van de vorige 3.4.0, kan het laden van de extensie zonder de versie te controleren resulteren in een PHP fout, waardoor WordPress niet kan worden geladen.

Om dit te voorkomen moet de extensie valideren dat de geïnstalleerde plugin versie 3.x.x is. Als versie 4.0.0 is geïnstalleerd, wordt de extensie uitgeschakeld, waardoor fouten worden voorkomen.

De extensie kan deze validatie uitvoeren met de volgende logica, uitgevoerd op de plugins_loaded hook (omdat de hoofdplugin dan al geladen is) in het hoofdpluginbestand van de extensie. Deze logica heeft toegang tot de ExtensionManager klasse, die is opgenomen in de hoofdplugin om extensies te beheren:

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

Let op hoe de uitbreiding een dependency verklaart van versiebeperking ^1.0 van de hoofdplugin (met behulp van Composer’s versiebeperkingen). Dus als versie 2.0.0 van Gato GraphQL is geïnstalleerd, zal de extensie niet worden geactiveerd.

De versiebeperking wordt gevalideerd via de methode ExtensionManager::assertIsValid, die Semver::satisfies callt (geleverd door het pakket 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;
    });
  }
}

Integratietests uitvoeren tegen een WordPress server

Om het gemakkelijker te maken voor externe ontwikkelaars om uitbreidingen voor je plugins te maken, moet je ze voorzien van tools voor ontwikkeling en testen, inclusief workflows voor hun continue integratie en continue levering (CI/CD) processen.

Tijdens de ontwikkeling kan iedereen eenvoudig een webserver opstarten met DevKinsta, de plugin installeren waarvoor ze de extensie coderen en meteen valideren dat de extensie compatibel is met de plugin.

Om het testen tijdens CI/CD te automatiseren, moeten we de webserver via een netwerk toegankelijk maken voor de CI/CD service. Diensten zoals InstaWP kunnen voor dit doel een sandbox site maken waarop WordPress is geïnstalleerd.

Als de codebase van de extensie wordt gehost op GitHub, dan kunnen ontwikkelaars GitHub Actions gebruiken om integratietests uit te voeren tegen de InstaWP service. De volgende workflow installeert de extensie op een InstaWP sandbox site (naast de laatste stabiele versie van de hoofdplugin) en voert dan de integratietesten uit:

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

Deze workflow opent het .zip bestand via Nightly Link, een service die toegang geeft tot een artefact van GitHub zonder in te loggen, wat de configuratie van InstaWP vereenvoudigt.

De extensie-plugin vrijgeven

We kunnen tools leveren om te helpen bij het uitbrengen van de extensies, waarbij we de procedures zoveel mogelijk automatiseren.

De Monorepo Builder is een bibliotheek voor het beheren van elk PHP project, inclusief een WordPress plugin. Het biedt het commando monorepo-builder release om een versie van het project vrij te geven, waarbij de major, minor of patch component van de versie wordt verhoogd volgens semantisch versiebeheer.

Dit commando voert een reeks release workers uit, wat PHP klassen zijn die bepaalde logica uitvoeren. De standaard workers zijn er een die een git tag aanmaakt met de nieuwe versie en een andere die de tag naar de remote repository pusht. Custom workers kunnen voor, na of tussen deze stappen worden geïnjecteerd.

De release workers worden geconfigureerd via een configuratiebestand:

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

We kunnen aangepaste release workers leveren om het release proces uit te breiden, aangepast aan de behoeften van een WordPress plugin. Bijvoorbeeld, de InjectStableTagVersionInPluginReadmeFileReleaseWorker stelt de nieuwe versie in als de “Stable tag” vermelding in het readme.txt bestand van de extensie:

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

Door InjectStableTagVersionInPluginReadmeFileReleaseWorker toe te voegen aan de configuratielijst, wordt bij het uitvoeren van het monorepo-builder release commando om een nieuwe versie van de plugin vrij te geven, de “Stable tag” in het readme.txt bestand van de extensie automatisch bijgewerkt.

De extensie-plugin publiceren naar de WP.org directory

We kunnen ook een workflow verspreiden om de extensie naar de WordPress Plugin Directory te publiceren. Bij het taggen van het project op de externe repository zal de volgende workflow de WordPress extensie plugin publiceren naar de 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 }}

Deze workflow gebruikt de 10up/action-wordpress-plugin-deploy actie, die de code ophaalt van een Git repository en het naar de WordPress.org SVN repository pusht, wat de operatie vereenvoudigt.

Samenvatting

Bij het maken van een uitbreidbare plugin voor WordPress, is het ons doel om het zo gemakkelijk mogelijk te maken voor externe ontwikkelaars om deze uit te breiden, waardoor de kans op een levendig ecosysteem rondom onze plugins wordt gemaximaliseerd.

Hoewel het verstrekken van uitgebreide documentatie ontwikkelaars kan helpen bij het uitbreiden van de plugin, is een nog effectievere aanpak het leveren van de benodigde PHP code en tools voor het ontwikkelen, testen en uitbrengen van hun uitbreidingen.

Door de extra code die uitbreidingen nodig hebben direct in onze plugin op te nemen, vereenvoudigen we het proces voor ontwikkelaars.

Ben jij van plan om je WordPress plugin uitbreidbaar te maken? Laat het ons weten in de comments.

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.