WordPress-Plugins können mit zusätzlichen Funktionen erweitert werden, wie beliebte Plugins wie WooCommerce und Gravity Forms zeigen. In dem Artikel Ein WordPress-Plugin so gestalten, dass es Erweiterungen unterstützt erfahren wir, dass es zwei Möglichkeiten gibt, ein WordPress-Plugin erweiterbar zu machen:

  1. Durch das Einrichten von Hooks (Aktionen und Filter), mit denen Erweiterungs-Plugins ihre eigenen Funktionen einfügen können
  2. Durch die Bereitstellung von PHP-Klassen, die Erweiterungs-Plugins erben können

Die erste Methode stützt sich eher auf die Dokumentation, in der die verfügbaren Hooks und ihre Verwendung detailliert beschrieben werden. Die zweite Methode hingegen bietet gebrauchsfertigen Code für Erweiterungen und macht eine umfangreiche Dokumentation überflüssig. Das ist von Vorteil, da die Erstellung von Dokumentation neben dem Code die Verwaltung und Veröffentlichung des Plugins erschweren kann.

Die direkte Bereitstellung von PHP-Klassen ersetzt die Dokumentation effektiv durch Code. Anstatt zu lehren, wie eine Funktion zu implementieren ist, liefert das Plugin den notwendigen PHP-Code und vereinfacht damit die Aufgabe für Drittentwickler.

Wir wollen uns einige Techniken ansehen, um dies zu erreichen, mit dem Ziel, ein Ökosystem von Integrationen rund um unser WordPress-Plugin zu schaffen.

Definition von PHP-Basisklassen im WordPress-Plugin

Das WordPress-Plugin enthält PHP-Klassen, die von Erweiterungs-Plugins verwendet werden können. Diese PHP-Klassen werden möglicherweise nicht vom Haupt-Plugin selbst verwendet, sondern sind speziell für die Nutzung durch andere Plugins vorgesehen.

Schauen wir uns an, wie dies im Open-Source-Plugin Gato GraphQL umgesetzt wird.

AbstractPlugin-Klasse:

AbstractPlugin stellt ein Plugin dar, sowohl für das Haupt-Plugin von Gato GraphQL als auch für seine Erweiterungen:

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 erweitert AbstractPlugin, um das Hauptplugin zu repräsentieren:

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:

Ähnlich, AbstractExtension erweitert AbstractPlugin, um ein Erweiterungs-Plugin darzustellen:

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

  // ...
}

Beachte, dass AbstractExtension im Hauptplugin enthalten ist und Funktionen zur Registrierung und Initialisierung einer Erweiterung bereitstellt. Es wird jedoch nur von Erweiterungen verwendet, nicht vom Hauptplugin selbst.

Die Klasse AbstractPlugin enthält einen gemeinsamen Initialisierungscode, der zu verschiedenen Zeitpunkten aufgerufen wird. Diese Methoden sind auf der Ebene der Vorfahren definiert, werden aber von den vererbenden Klassen entsprechend ihrer Lebenszyklen aufgerufen.

Das Hauptplugin und die Erweiterungen werden initialisiert, indem die Methode setup der entsprechenden Klasse ausgeführt wird, die in der Hauptdatei des WordPress-Plugins aufgerufen wird.

In Gato GraphQL geschieht dies zum Beispiel in gatographql.php:

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

setup-Methode:

Auf der Vorgängerebene enthält setup die gemeinsame Logik zwischen dem Plugin und seinen Erweiterungen, z. B. die Aufhebung der Registrierung, wenn das Plugin deaktiviert wird. Diese Methode ist nicht endgültig; sie kann von den vererbenden Klassen überschrieben werden, um ihre Funktionalität zu erweitern:

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

Die Setup-Methode des Haupt-Plugins:

Die setup Methode des Hauptplugins initialisiert den Lebenszyklus der Anwendung. Sie führt die Funktionen des Hauptplugins über Methoden wie initialize, configureComponents, configure und boot aus und löst die entsprechenden Aktionshaken für Erweiterungen aus:

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

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

Methode „Extension setup“:

Die Klasse AbstractExtension führt ihre Logik über die entsprechenden Hooks aus:

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

Die Methoden initialize, configureComponents, configure und boot sind sowohl für das Hauptplugin als auch für die Erweiterungen gleich und können eine gemeinsame Logik haben. Diese gemeinsame Logik wird in der Klasse AbstractPlugin gespeichert.

Die Methode configure beispielsweise konfiguriert das Plugin oder die Erweiterungen, indem sie callPluginInitializationConfiguration aufruft, die für das Hauptplugin und die Erweiterungen unterschiedliche Implementierungen hat und als abstrakt definiert ist, und getModuleClassConfiguration, das ein Standardverhalten bietet, aber bei Bedarf überschrieben werden kann:

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

Das Haupt-Plugin stellt seine Implementierung für callPluginInitializationConfiguration zur Verfügung:

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

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

Ebenso stellt die Erweiterungsklasse ihre Implementierung zur Verfügung:

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

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

Die Methoden initialize, configureComponents und boot sind auf der Vorfahrenebene definiert und können von den erbenden Klassen überschrieben werden:

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 können von AbstractMainPlugin oder AbstractExtension überschrieben werden, um sie mit ihren eigenen Funktionen zu erweitern.

Für das Hauptplugin entfernt die Methode setup auch jegliches Caching aus der WordPress-Instanz, wenn das Plugin oder eine seiner Erweiterungen aktiviert oder deaktiviert wird:

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
  }

  // ...
}

In ähnlicher Weise entfernt die Methode deactivate das Caching und boot führt zusätzliche Aktionshaken nur für das Hauptplugin aus:

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

Aus dem oben dargestellten Code wird deutlich, dass wir bei der Entwicklung und Programmierung eines WordPress-Plugins die Bedürfnisse der Erweiterungen berücksichtigen und den Code so oft wie möglich wiederverwenden müssen. Die Umsetzung solider objektorientierter Programmiermuster (wie die SOLID-Prinzipien) hilft dabei und macht die Codebasis langfristig wartbar.

Deklaration und Validierung der Versionsabhängigkeit

Da die Erweiterung von einer PHP-Klasse erbt, die vom Plugin bereitgestellt wird, ist es wichtig zu überprüfen, ob die erforderliche Version des Plugins vorhanden ist. Wenn du das nicht tust, kann es zu Konflikten kommen, die die Website zum Absturz bringen.

Wenn z. B. die Klasse AbstractExtension aktualisiert wird und eine Hauptversion 4.0.0 von der vorherigen Version 3.4.0 veröffentlicht wird, kann das Laden der Erweiterung ohne Überprüfung der Version zu einem PHP-Fehler führen und WordPress am Laden hindern.

Um dies zu vermeiden, muss die Erweiterung überprüfen, ob das installierte Plugin die Version 3.x.x hat. Wenn die Version 4.0.0 installiert ist, wird die Erweiterung deaktiviert und verhindert so Fehler.

Die Erweiterung kann diese Überprüfung mit der folgenden Logik durchführen, die am plugins_loaded -Hook (da das Haupt-Plugin zu diesem Zeitpunkt bereits geladen ist) in der Haupt-Plugin-Datei der Erweiterung ausgeführt wird. Diese Logik greift auf die ExtensionManager Klasse zu, die im Hauptplugin enthalten ist, um Erweiterungen zu verwalten:

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

Beachte, dass die Erweiterung eine Abhängigkeit von der Versionsbeschränkung ^1.0 des Hauptplugins deklariert (unter Verwendung der Versionsbeschränkungen des Composers). Wenn also die Version 2.0.0 von Gato GraphQL installiert ist, wird die Erweiterung nicht aktiviert.

Die Versionsbeschränkung wird über die Methode ExtensionManager::assertIsValid überprüft, die Semver::satisfies aufruft (bereitgestellt durch das Paket 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;
    });
  }
}

Integrationstests gegen einen WordPress-Server durchführen

Um es Entwicklern von Drittanbietern zu erleichtern, Erweiterungen für deine Plugins zu erstellen, solltest du ihnen Werkzeuge für die Entwicklung und das Testen zur Verfügung stellen, einschließlich Workflows für ihre kontinuierlichen Integrations- und Lieferprozesse (CI/CD).

Während der Entwicklung kann jeder mit DevKinsta ganz einfach einen Webserver aufsetzen, das Plugin installieren, für das er die Erweiterung programmiert, und sofort überprüfen, ob die Erweiterung mit dem Plugin kompatibel ist.

Um die Tests während des CI/CD zu automatisieren, muss der Webserver über ein Netzwerk für den CI/CD-Dienst zugänglich sein. Dienste wie InstaWP können zu diesem Zweck eine Sandbox-Website mit installiertem WordPress erstellen.

Wenn die Codebasis der Erweiterung auf GitHub gehostet wird, können Entwickler GitHub Actions nutzen, um Integrationstests gegen den InstaWP-Dienst durchzuführen. Der folgende Arbeitsablauf installiert die Erweiterung auf einer InstaWP-Sandbox-Website (zusammen mit der neuesten stabilen Version des Haupt-Plugins) und führt dann die Integrationstests aus:

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

Dieser Workflow greift auf die .zip-Datei über Nightly Link zu, einen Dienst, der es ermöglicht, auf ein Artefakt von GitHub zuzugreifen, ohne sich anzumelden, was die Konfiguration von InstaWP vereinfacht.

Freigabe des Erweiterungs-Plugins

Wir können Werkzeuge bereitstellen, die bei der Veröffentlichung der Erweiterungen helfen und die Abläufe so weit wie möglich automatisieren.

Der Monorepo Builder ist eine Bibliothek zur Verwaltung beliebiger PHP-Projekte, einschließlich eines WordPress-Plugins. Er bietet den Befehl monorepo-builder release, um eine Version des Projekts freizugeben, wobei entweder die Major-, Minor- oder Patch-Komponente der Version gemäß der semantischen Versionierung erhöht wird.

Dieser Befehl führt eine Reihe von Release Workern aus, d. h. PHP-Klassen, die eine bestimmte Logik ausführen. Zu den Standardworkern gehört einer, der eine git tag mit der neuen Version erstellt, und ein anderer, der das Tag in das entfernte Repository überträgt. Benutzerdefinierte Worker können vor, nach oder zwischen diesen Schritten eingefügt werden.

Die Release Worker werden über eine Konfigurationsdatei konfiguriert:

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

Wir können benutzerdefinierte Release Worker bereitstellen, um den Release-Prozess auf die Bedürfnisse eines WordPress-Plugins zuzuschneiden. Zum Beispiel kann der InjectStableTagVersionInPluginReadmeFileReleaseWorker setzt die neue Version als „Stable tag“-Eintrag in der readme.txt-Datei der Erweiterung:

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

Wenn du InjectStableTagVersionInPluginReadmeFileReleaseWorker zur Konfigurationsliste hinzufügst, wird bei der Ausführung des Befehls monorepo-builder release zur Veröffentlichung einer neuen Version des Plugins der „Stable-Tag“ in der readme.txt-Datei der Erweiterung automatisch aktualisiert.

Veröffentlichung des Erweiterungs-Plugins im WP.org-Verzeichnis

Wir können auch einen Arbeitsablauf bereitstellen, der die Veröffentlichung der Erweiterung im WordPress Plugin-Verzeichnis unterstützt. Wenn du das Projekt auf dem remoten Repository taggst, veröffentlicht der folgende Workflow das WordPress-Erweiterungs-Plugin im Verzeichnis:

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

Dieser Arbeitsablauf verwendet die 10up/action-wordpress-plugin-deploy Aktion, die den Code aus einem Git-Repository abruft und ihn in das SVN-Repository von WordPress.org pusht, was den Vorgang vereinfacht.

Zusammenfassung

Wenn wir ein erweiterbares Plugin für WordPress entwickeln, wollen wir es Entwicklern von Drittanbietern so einfach wie möglich machen, das Plugin zu erweitern, um so die Chancen auf ein lebendiges Ökosystem rund um unsere Plugins zu erhöhen.

Eine ausführliche Dokumentation kann Entwicklern zeigen, wie sie das Plugin erweitern können. Noch effektiver ist es jedoch, den notwendigen PHP-Code und die Werkzeuge für die Entwicklung, das Testen und die Veröffentlichung ihrer Erweiterungen bereitzustellen.

Indem wir den zusätzlichen Code, den die Erweiterungen benötigen, direkt in unser Plugin integrieren, vereinfachen wir den Prozess für Entwickler.

Hast du vor, dein WordPress-Plugin erweiterbar zu machen? Lass es uns in den Kommentaren wissen.

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.