Bei der Entwicklung eines WordPress-Plugins ist ein wichtiger Schritt die Vorinstallation wichtiger Daten, damit das Plugin von Anfang an reibungslos funktioniert. Nehmen wir zum Beispiel ein Plugin für einen Veranstaltungsmanager. Bei der Installation ist es von großem Vorteil, wenn das Plugin automatisch eine Seite mit dem Titel Kommende Veranstaltungen erstellt, auf der eine Liste zukünftiger Veranstaltungen angezeigt wird.

Diese vorkonfigurierte Seite, die mit einem Shortcode wie [event_list number="10" scope="future" status="publish"] eingebettet ist, ermöglicht es den Nutzern, die Funktionen des Plugins sofort zu nutzen, ohne die Dokumentation lesen zu müssen.

Die Installation von Daten ist nicht nur bei der Erstinstallation des Plugins hilfreich, sondern auch bei späteren Aktualisierungen. Wenn z. B. mit einem Update eine neue Funktion für die Kalenderansicht eingeführt wird, kann das Plugin automatisch eine neue Seite Veranstaltungskalender erstellen, auf der diese Neuerung mit einem Shortcode wie [event_calendar status="publish"] vorgestellt wird.

Generell deckt der Umfang der Dateninstallation verschiedene Bedürfnisse ab:

  • Das Erstellen neuer Seiten mit bestimmten Titeln und Inhalten.
  • Hinzufügen von Einträgen für benutzerdefinierte Beitragstypen (CPTs), die mit dem Plugin erstellt wurden.
  • Einfügen von Standardeinstellungen in die Tabelle wp_options
  • Zuweisen von neuen Fähigkeiten zu Benutzerrollen
  • Zuweisen von Metadaten zu Nutzern für neue oder aktualisierte Funktionen des Plugins (z. B. können Nutzer das Format des Veranstaltungsdatums ändern, und es wird zunächst ein Standardwert für alle Nutzer hinzugefügt)
  • Erstellen von Kategorien, die im Kontext des Plugins häufig verwendet werden, wie z. B. „Konferenzen“ oder „Sport“

Die Installation der Daten muss schrittweise erfolgen, da wir sonst doppelte Einträge erstellen könnten.

Wenn z. B. ein Plugin der Version 1.1 eine Seite Aktuelle Veranstaltungen einführt und ein Nutzer ein Update von Version 1.0 vornimmt, sollten nur die neuen Daten installiert werden, die für Version 1.1 relevant sind. Diese schrittweise Aktualisierung stellt sicher, dass bei der Version 1.2 mit der Kalenderfunktion nur die neue Seite Veranstaltungskalender hinzugefügt wird und die Seite Bevorstehende Veranstaltungen nicht doppelt vorhanden ist.

Wenn das Plugin aktualisiert wird, muss es also herausfinden, welche Vorgängerversion installiert war, und nur die Daten installieren, die der neuen Version/den neuen Versionen entsprechen.

In diesem Artikel wird erklärt, wie wir in unseren WordPress-Plugins die ursprünglichen Daten installieren und bei weiteren Updates neue Daten hinzufügen.

Die aktuelle Version bereitstellen

Um den inkrementellen Prozess zu bewältigen, muss das Plugin seine aktuelle Version erkenneninstag, die normalerweise im Header der Hauptdatei des Plugins angegeben wird. Aber natürlich können wir sie dort nicht direkt referenzieren, da sie in einem PHP-Kommentar steht. Deshalb definieren wir diesen Wert auch in einer Variablen und übergeben ihn an eine Plugin Klasse, die für die Initialisierung und Konfiguration zuständig ist:

<?php
/*
Plugin Name: My plugin
Version: 1.6
*/

// Same version as in the header
$pluginVersion = '1.6';
new Plugin($pluginVersion)->setup();

Die Klasse Plugin nutzt die Eigenschaft Constructor Promotion von PHP 8.0 und speichert diese Version, damit wir sie später referenzieren können:

<?php

class Plugin {

  public function __construct(
    protected string $pluginVersion,
  ) {}

  public function setup(): void
  {
    // Initialization logic here...
  }

  // ...
}

Beachte, dass die Logik zur Initialisierung und Konfiguration des Plugins in der Methode setup und nicht im Konstruktor enthalten ist. Das liegt daran, dass der Konstruktor keine Seiteneffekte erzeugen darf; andernfalls könnte es zu Fehlern kommen, wenn wir die Klasse Plugin erweitern oder zusammenstellen.

Schauen wir uns an, wie das passieren könnte. Nehmen wir an, wir fügen irgendwann eine Klasse BetterPlugin hinzu, die die Funktionen der Klasse Plugin zusammenfasst:

class BetterPlugin {

  public function printSomething(): string
  {
    $pluginVersion = '1.0';
    $plugin = new Plugin($pluginVersion);
    return '<div class="wrapper">' . $plugin->printSomething() . '</div>';
  }
}

Jedes Mal, wenn new Plugin() innerhalb von printSomething ausgeführt wird, wird eine neue Instanz von Plugin erstellt. Wenn die Konfigurationslogik zum Konstruktor hinzugefügt würde, würde sie jedes Mal ausgeführt, wenn wir ein neues Plugin Objekt erstellen. In unserem Fall wollen wir die Seite mit den bevorstehenden Veranstaltungen nur einmal erstellen, nicht mehrmals. Wenn wir die Logik in die Methode setup einbauen, können wir dieses Problem vermeiden.

Rückverfolgung der vorherigen Version

WordPress bietet keine bequeme Möglichkeit, die Version des zu ersetzenden Plugins abzurufen. Daher müssen wir diesen Wert selbst in der Tabelle wp_options in der Datenbank speichern.

Speichere die Version unter dem Eintrag "myplugin_version", wobei myplugin_ der Name des Plugins ist (z.B. eventsmanager_version). Es ist wichtig, dass wir allen unseren Einstellungen immer myplugin_ voranstellen, um mögliche Konflikte zu vermeiden, da wir nicht sicher sein können, dass ein anderes Plugin nicht eine version Option hinzufügt.

Wenn das Plugin bei jeder Anfrage geladen wird, kennt Plugin bereits die aktuelle Version (aus der Eigenschaft $pluginVersion ) und ruft die zuletzt gespeicherte Version aus der Datenbank ab. Dieser Vergleich bestimmt den Status des Plugins:

  • Neuinstallation: Erkennt, ob in der Datenbank ein Versionseintrag für das Plugin fehlt, was auf eine erstmalige Einrichtung hinweist (d.h. $storedPluginVersion ist null)
  • Aktualisierung: Wird erkannt, wenn die aktuelle Version die in der Datenbank gespeicherte Version übersteigt, was auf einen Aktualisierungsbedarf hinweist.
  • Ansonsten gibt es keine Änderung

Bei jeder Änderung rufen wir prepareAndInstallPluginSetupData auf, um die entsprechenden Daten zu installieren, egal ob es sich um eine Neuinstallation (in diesem Fall müssen alle Daten für alle Versionen installiert werden) oder eine Aktualisierung (nur die Daten für alle neuen Versionen werden installiert) handelt. Die Variable $previousVersion, die gelöscht werden kann, zeigt an, um welche Situation es sich handelt ($previousVersion ist null => Neuinstallation).

Nach dem Aufruf dieser Methode müssen wir auch die aktuelle Plugin-Version in der Datenbank speichern, damit sie zur neuen „zuletzt gespeicherten“ Version wird. Dies muss nach dem Aufruf von prepareAndInstallPluginSetupData geschehen, damit im Falle eines Fehlers (z. B. RuntimeException) und einer nicht erfolgten Installation der Daten die vorherige Version in der Datenbank gespeichert bleibt und bei der nächsten Anfrage ein neuer Installationsversuch unternommen wird.

<?php

class Plugin {

  // ...

  public function setup(): void
  {
    if (!is_admin()) {
      return;
    }

    $this->managePluginDataVersioning();
  }

  /**
   * If the plugin has just been newly-installed + activated
   * or updated, install the appropriate data.
   */
  protected function managePluginDataVersioning(): void
  {
    $myPluginVersionOptionName = 'myplugin_version';
    $storedPluginVersion = get_option($myPluginVersionOptionName, null);

    // Check if the main plugin has been activated or updated
    $isPluginJustFirstTimeActivated = $storedPluginVersion === null;
    $isPluginJustUpdated = !$isPluginJustFirstTimeActivated && $storedPluginVersion !== $this->pluginVersion;

    // If there were no changes, nothing to do
    if (!$isPluginJustFirstTimeActivated && !$isPluginJustUpdated) {
      return;
    }

    \add_action(
      'init',
      function () use ($myPluginVersionOptionName, $storedPluginVersion): void {
        $this->prepareAndInstallPluginSetupData($storedPluginVersion);

        // Update on the DB
        update_option($myPluginVersionOptionName, $this->pluginVersion);
      }
    );
  }

  protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
  {
    // Installation logic...
  }
}

Beachte, dass prepareAndInstallPluginSetupData (und die anschließende DB-Aktualisierung) über den Aktionshaken init ausgeführt wird. Damit soll sichergestellt werden, dass alle Daten aus dem CMS abrufbar und bearbeitbar sind.

Vor allem auf Taxonomien (Tags und Kategorien) kann vor dem init Hook nicht zugegriffen werden. Wenn bei der Installation des Plugins ein CPT-Eintrag erstellt und ihm eine benutzerdefinierte Kategorie zugewiesen werden muss, kann dieser Vorgang erst ab dem init -Hook ausgeführt werden.

Bei jeder Anfrage auf die letzte gespeicherte Version in der DB zuzugreifen, ist aus Sicht der Leistung nicht ideal. Um dies zu verbessern, kannst du alle Optionen, die das Plugin benötigt, in einem Array zusammenfassen, sie in einem einzigen Eintrag speichern und dann mit einem einzigen Aufruf der DB darauf zugreifen.

Wenn das Plugin z. B. auch die Option myplugin_date_format zur Anzeige des Veranstaltungsdatums speichern muss, können wir einen einzigen Eintrag myplugin_options mit den Eigenschaften version und date_format erstellen.

Um auf die zuletzt gespeicherte Version zuzugreifen, muss der PHP-Code dann wie folgt angepasst werden:

<?php

class Plugin {

  // ...

  protected function managePluginDataVersioning(): void
  {
    $myPluginOptionsOptionName = 'myplugin_options';
    $myPluginOptions = get_option($myPluginOptionsOptionName, []);
    $storedPluginVersion = $myPluginOptions['version'] ?? null;

    // ...

    \add_action(
      'init',
      function () use ($myPluginOptionsOptionName, $myPluginOptions): void {
        // ...

        // Update on the DB
        $myPluginOptions['version'] = $this->pluginVersion;
        update_option($myPluginOptionsOptionName, $myPluginOptions);
      }
    );
  }
}

Vermeidung von gleichzeitigen Anfragen bei der Installation von doppelten Daten

Es besteht die Möglichkeit, dass der Installationsprozess mehr als einmal ausgelöst wird, wenn zwei oder mehr Nutzer/innen genau zur gleichen Zeit auf den wp-admin zugreifen. Um zu verhindern, dass dieselben Daten zweimal oder öfter installiert werden, verwenden wir einen Transient als Flag, um nur die erste Anfrage zur Installation von Daten zuzulassen:

  • Prüfe, ob der Transient myplugin_installing_plugin_setup_data existiert (auch hier muss dem Namen myplugin_ vorangestellt werden); wenn ja, tue nichts (da ein anderer Prozess die Daten installiert)
  • Andernfalls speichere den Transient in der Datenbank für eine angemessene Zeitspanne, um die Daten zu installieren (z. B. 30 Sekunden)
  • Installiere die Daten
  • Lösche die Transiente

Hier ist der Code:

<?php

class Plugin {

  // ...

  /**
   * Use a transient to make sure that only one instance
   * will install the data. Otherwise, two requests
   * happening simultaneously might execute the logic
   */
  protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
  {
    $transientName = 'myplugin_installing_plugin_setup_data';
    $transient = \get_transient($transientName);
    if ($transient !== false) {
      // Another instance is executing this code right now
      return;
    }

    \set_transient($transientName, true, 30);
    $this->installPluginSetupData($previousVersion);
    \delete_transient($transientName);
  }

  protected function installPluginSetupData(?string $previousVersion): void
  {
    // Do something...
  }
}

Installation der Daten für alle Versionen

Wie bereits erwähnt, müssen wir beim Aktualisieren des Plugins nur die Daten für die neuen Versionen installieren, nicht für alle. Das bedeutet, dass wir verwalten müssen, welche Daten wir Version für Version installieren.

Im folgenden Code gibt das Array $versionCallbacks an, welche Funktion für jede Version ausgeführt werden soll, wobei die Funktion die Logik zur Installation der Daten ausführt. Wir durchlaufen die Liste aller Versionen, vergleichen jede mit der vorherigen Version unter version_compare und führen, wenn sie größer ist, die entsprechende Funktion aus, um die entsprechenden Daten zu installieren.

Wenn $previousVersion gleich null ist (d.h. es handelt sich um eine neue Installation), werden alle Funktionen ausgeführt.

class Plugin {
  /**
   * Provide the installation in stages, version by version, to
   * be able to execute it both when installing/activating the plugin,
   * or updating it to a new version with setup data.
   *
   * The plugin's setup data will be installed if:
   *
   * - $previousVersion = null => Activating the plugin for first time
   * - $previousVersion < someVersion => Updating to a new version that has data to install
   */
  protected function installPluginSetupData(?string $previousVersion): void
  {
    $versionCallbacks = [
      '1.1' => $this->installPluginSetupDataForVersion1Dot1(...),
      '1.2' => $this->installPluginSetupDataForVersion1Dot2(...),
      // ... Add more versions
    ];
    foreach ($versionCallbacks as $version => $callback) {
      /**
       * If the previous version is provided, check if the corresponding update
       * has already been performed, then skip
       */
      if ($previousVersion !== null && version_compare($previousVersion, $version, '>=')) {
        continue;
      }
      $callback();
    }
  }

  protected function installPluginSetupDataForVersion1Dot1(): void
  {
    // Do something...
  }

  protected function installPluginSetupDataForVersion1Dot2(): void
  {
    // Do something...
  }
}

Installieren der Daten für jede einzelne Version

Zum Schluss müssen wir für jede Version die eigentlichen Daten installieren (eine Seite, einen CPT-Eintrag erstellen, eine Option hinzufügen usw.).

In diesem Code fügen wir die Seite bevorstehende Veranstaltungen für das Eventmanager-Plugin für v1.1 hinzu:

class Plugin {
  
  // ...

  protected function installPluginSetupDataForVersion1Dot1(): void
  {
    \wp_insert_post([
      'post_status' => 'publish',
      'post_type' => 'page',
      'post_title' => \__('Upcoming Events', 'myplugin'),
      'post_content' => '[event_list number="10" scope="future"]',
    ]);
  }

  // ...
}

Dann erstellen wir die Seite Veranstaltungskalender für v1.2 (in diesem Fall verwenden wir Gutenberg-Blöcke auf der Seite und fügen einen benutzerdefinierten Block namens event-calendar hinzu):

class Plugin {
  
  // ...

  protected function installPluginSetupDataForVersion1Dot2(): void
  {
    \wp_insert_post([
      'post_status' => 'publish',
      'post_type' => 'page',
      'post_title' => \__('Events Calendar', 'myplugin'),
      'post_content' => serialize_blocks([
        [
          'blockName' => 'myplugin/event-calendar',
          'attrs' => [
            'status' => 'publish',
          ],
          'innerContent' => [],
        ],
      ]),
    ]);
  }
}

Alle Codes zusammen

Wir sind fertig! Der gesamte PHP-Code für die Klasse Plugin, der die Logik enthält, um die Plugin-Version zu verfolgen und die entsprechenden Daten zu installieren, lautet wie folgt:

<?php

class Plugin {

  public function __construct(
    protected string $pluginVersion,
  ) {
  }

  public function setup(): void
  {
    if (!is_admin()) {
      return;
    }

    $this->managePluginDataVersioning();
  }

  /**
   * If the plugin has just been newly-installed + activated
   * or updated, install the appropriate data.
   */
  protected function managePluginDataVersioning(): void
  {
    $myPluginVersionOptionName = 'myplugin_version';
    $storedPluginVersion = get_option($myPluginVersionOptionName, null);

    // Check if the main plugin has been activated or updated
    $isPluginJustFirstTimeActivated = $storedPluginVersion === null;
    $isPluginJustUpdated = !$isPluginJustFirstTimeActivated && $storedPluginVersion !== $this->pluginVersion;

    // If there were no changes, nothing to do
    if (!$isPluginJustFirstTimeActivated && !$isPluginJustUpdated) {
      return;
    }

    \add_action(
      'init',
      function () use ($myPluginVersionOptionName, $storedPluginVersion): void {
        $this->prepareAndInstallPluginSetupData($storedPluginVersion);

        // Update on the DB
        update_option($myPluginVersionOptionName, $this->pluginVersion);
      }
    );
  }

  /**
   * Use a transient to make sure that only one instance
   * will install the data. Otherwise, two requests
   * happening simultaneously might both execute
   * this logic
   */
  protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
  {
    $transientName = 'myplugin_installing_plugin_setup_data';
    $transient = \get_transient($transientName);
    if ($transient !== false) {
      // Another instance is executing this code right now
      return;
    }

    \set_transient($transientName, true, 30);
    $this->installPluginSetupData($previousVersion);
    \delete_transient($transientName);
  }

  /**
   * Provide the installation in stages, version by version, to
   * be able to execute it both when installing/activating the plugin,
   * or updating it to a new version with setup data.
   *
   * The plugin's setup data will be installed if:
   *
   * - $previousVersion = null => Activating the plugin for first time
   * - $previousVersion < someVersion => Updating to a new version that has data to install
   */
  protected function installPluginSetupData(?string $previousVersion): void
  {
    $versionCallbacks = [
      '1.1' => $this->installPluginSetupDataForVersion1Dot1(...),
      '1.2' => $this->installPluginSetupDataForVersion1Dot2(...),
      // ... Add more versions
    ];
    foreach ($versionCallbacks as $version => $callback) {
      /**
       * If the previous version is provided, check if the corresponding update
       * has already been performed, then skip
       */
      if ($previousVersion !== null && version_compare($previousVersion, $version, '>=')) {
        continue;
      }
      $callback();
    }
  }

  protected function installPluginSetupDataForVersion1Dot1(): void
  {
    \wp_insert_post([
      'post_status' => 'publish',
      'post_type' => 'page',
      'post_title' => \__('Upcoming Events', 'myplugin'),
      'post_content' => '[event_list number="10" scope="future" status="publish"]',
    ]);
  }

  protected function installPluginSetupDataForVersion1Dot2(): void
  {
    \wp_insert_post([
      'post_status' => 'publish',
      'post_type' => 'page',
      'post_title' => \__('Events Calendar', 'myplugin'),
      'post_content' => serialize_blocks([
        [
          'blockName' => 'myplugin/event-calendar',
          'attrs' => [
            'status' => 'publish',
          ],
          'innerContent' => [],
        ],
      ]),
    ]);
  }
}

Zusammenfassung

WordPress-Plugins müssen bei der Installation oft Daten installieren. Wenn neuere Versionen des Plugins neue Funktionen bieten, muss das Plugin möglicherweise auch Daten installieren, wenn es aktualisiert wird.

In diesem Artikel haben wir gelernt, wie wir Versionen verfolgen und die entsprechenden Daten für unsere Plugins installieren können.

Hast du ein WordPress-Plugin, das von der Installation von Daten profitieren kann? 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.