Bij het ontwikkelen van een WordPress plugin is een cruciale stap het vooraf installeren van essentiële gegevens, zodat de plugin vanaf het begin soepel werkt. Neem bijvoorbeeld een plugin voor het managen van evenementen. Bij de installatie is het enorm handig als de plugin automatisch een pagina genereert met de titel Aankomende evenementen, waarop een lijst met toekomstige evenementen wordt weergegeven.

Deze voorgeconfigureerde pagina, ingesloten met een shortcode zoals [event_list number="10" scope="future" status="publish"], stelt gebruikers in staat om direct gebruik te maken van de mogelijkheden van de plugin zonder de documentatie door te lezen.

Het installeren van gegevens is niet alleen handig wanneer je de plugin voor het eerst installeert, maar ook wanneer je de plugin later bijwerkt. Bijvoorbeeld, als een update een feature introduceert om een kalender weer te geven, kan de plugin automatisch een nieuwe pagina maken, Evenementenkalender, die deze toevoeging laat zien door middel van een shortcode zoals [event_calendar status="publish"].

In het algemeen moet je bij het installeren van gegevens aan het volgende denken:

  • Het genereren van nieuwe pagina’s met specifieke titels en inhoud.
  • Toevoegen van items voor custom posttypes (CPT’s) die door de plugin zijn gemaakt.
  • Standaardinstellingen invoegen in de tabel wp_options
  • Nieuwe mogelijkheden toewijzen aan gebruikersrollen
  • Metadata toewijzen aan gebruikers, voor nieuwe of bijgewerkte functies die door de plugin worden aangeboden (gebruikers kunnen bijvoorbeeld het datumformat van een evenement wijzigen, en een standaardwaarde wordt eerst toegevoegd voor alle gebruikers)
  • Categorieën aanmaken die vaak gebruikt worden binnen de context van de plugin, zoals “conferenties” of “sport”

Het installeren van gegevens moet een incrementeel proces zijn, anders zouden we dubbele vermeldingen kunnen maken.

Bijvoorbeeld, als een plugin versie 1.1 een Aankomende Evenementen pagina introduceert en een gebruiker update vanaf versie 1.0, dan moeten alleen de nieuwe gegevens die relevant zijn voor versie 1.1 worden geïnstalleerd. Deze incrementele update zorgt ervoor dat wanneer versie 1.2 uitrolt met de kalenderfunctie, alleen de nieuwe pagina Evenementenkalender wordt toegevoegd, waardoor duplicatie van de pagina Aankomende evenementen wordt voorkomen.

Daarom moet de plugin bij het updaten achterhalen welke vorige versie was geïnstalleerd en alleen de gegevens installeren die overeenkomen met de nieuwe versie(s).

Dit artikel legt uit hoe je initiële gegevens installeert en nieuwe gegevens blijft toevoegen bij verdere updates in onze WordPress plugins.

De huidige versie leveren

Om het incrementele proces aan te kunnen, moet de plugin zijn huidige versie bijhouden, die meestal wordt aangegeven in de header van het hoofdbestand van de plugin. Maar daar kunnen we natuurlijk niet direct naar verwijzen, omdat het in een PHP comment staat. Dus definiëren we deze waarde ook in een variabele en geven die door aan een Plugin klasse die verantwoordelijk is voor initialisatie en configuratie:

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

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

De klasse Plugin, die gebruik maakt van PHP 8.0’s Constructor property promotion, slaat deze versie op, zodat we er later naar kunnen verwijzen:

<?php

class Plugin {

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

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

  // ...
}

Let op hoe de logica om de plugin te initialiseren en configureren is toegevoegd aan de methode setup, niet aan de constructor. Dat is omdat de constructor bijwerkingen moet vermijden; anders zouden we bugs kunnen veroorzaken bij het uitbreiden of samenstellen van de klasse Plugin.

Laten we eens kijken hoe dat zou kunnen gebeuren. Stel dat we uiteindelijk een klasse BetterPlugin toevoegen die functionaliteit uit de klasse Plugin samenstelt:

class BetterPlugin {

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

Telkens wanneer new Plugin() wordt uitgevoerd binnen printSomething, wordt een nieuwe instantie van Plugin aangemaakt. Als de configuratielogica zou worden toegevoegd aan de constructor, zou deze elke keer worden uitgevoerd als we een nieuw Plugin object maken. In ons geval willen we de pagina Aankomende evenementen maar één keer maken, niet meerdere keren. Door de logica toe te voegen aan de methode setup kunnen we dit probleem vermijden.

De vorige versie bijhouden

WordPress biedt geen handige manier om de versie op te halen van de plugin die wordt vervangen. Daarom moeten we deze waarde zelf opslaan in de wp_options tabel van de database.

Sla de versie op onder entry "myplugin_version", waarbij myplugin_ de naam van de plugin is (bijvoorbeeld eventsmanager_version). Het is belangrijk om al onze instellingen altijd vooraf te laten gaan door myplugin_, om mogelijke conflicten te voorkomen, omdat we er niet zeker van kunnen zijn dat een andere plugin geen version optie zal toevoegen.

Bij het laden van de plugin bij elke aanvraag, zal Plugin al weten wat de huidige versie is (van propery $pluginVersion eerder), en zal de laatst opgeslagen versie ophalen uit de database. Deze vergelijking bepaalt de status van de plugin:

  • Nieuwe installatie: detecteert of de database een versievermelding voor de plugin mist, wat duidt op een eerste installatie (d.w.z. $storedPluginVersion is null)
  • Update: Wordt geïdentificeerd als de huidige versie hoger is dan de in de database opgeslagen versie, wat aangeeft dat er een upgrade nodig is.
  • Anders is er geen verandering

Wanneer er een verandering is, callen we prepareAndInstallPluginSetupData om de juiste gegevens te installeren, of het nu gaat om een nieuwe installatie (in dat geval moet het alle gegevens voor alle versies installeren) of een update (installeer alleen gegevens voor alle nieuwe versies). De nulvariabele $previousVersion geeft aan om welke situatie het gaat ($previousVersion is null => nieuwe installatie).

Na het callen van deze methode moeten we ook de huidige plugin versie opslaan in de database, wat de nieuwe “laatst opgeslagen” versie wordt. Dit moet gedaan worden na het callen van prepareAndInstallPluginSetupData, zodat als deze methode een fout oplevert (bijvoorbeeld het gooien van een RuntimeException) en de gegevens niet geïnstalleerd worden, de vorige versie nog steeds opgeslagen is in de database en een nieuwe ronde van het installeren van gegevens zal worden geprobeerd bij de volgende aanvraag.

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

Merk op dat prepareAndInstallPluginSetupData (en de daaropvolgende DB update) wordt uitgevoerd op de init action hook. Dit is om er zeker van te zijn dat alle gegevens van het CMS klaar zijn om opgevraagd en gemanipuleerd te worden.

Met name taxonomieën (tags en categorieën) kunnen niet worden benaderd vóór de init hook. Als het installatieproces van de plugin een CPT entry moest aanmaken en er een aangepaste categorie aan moest toewijzen, dan kon dit proces pas worden uitgevoerd vanaf de init hook.

Het opvragen van de laatst opgeslagen versie uit de DB bij elke aanvraag is niet ideaal vanuit het oogpunt van prestaties. Om dit te verbeteren, combineer je alle opties die de plugin nodig heeft in een array, sla je ze op in een enkel item en call je ze allemaal met een enkele oproep naar de DB.

Bijvoorbeeld, als de plugin ook een myplugin_date_format optie nodig heeft om de datum van de gebeurtenis weer te geven, dan kunnen we een enkel item myplugin_options maken met properties version en date_format.

Om toegang te krijgen tot de laatst opgeslagen versie, moet de PHP code dan als volgt worden aangepast:

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

Het vermijden van gelijktijdige verzoeken die dubbele gegevens installeren

De mogelijkheid bestaat dat het installatieproces meer dan eens wordt gestart als twee of meer gebruikers op precies hetzelfde moment toegang krijgen tot de wp-admin. Om te voorkomen dat dezelfde gegevens twee of meer keer worden geïnstalleerd, gebruiken we een transient als flag om alleen het eerste verzoek toe te staan om gegevens te installeren:

  • Controleer of transient myplugin_installing_plugin_setup_data bestaat (nogmaals, deze naam moet worden voorafgegaan door myplugin_); zo ja, doe dan niets (omdat een ander proces de gegevens installeert)
  • Anders de transient in de database opslaan voor een redelijke maximumtijd om de gegevens te installeren (bijvoorbeeld 30 seconden)
  • Installeer de gegevens
  • Verwijder het transient event

Hier is de 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...
  }
}

Gegevens installeren voor alle versies

Zoals eerder vermeld, moeten we bij het bijwerken van de plugin alleen de gegevens voor de nieuwe versies installeren, niet voor alle versies. Dat betekent dat we per versie moeten beheren welke gegevens we installeren.

In de onderstaande code geeft de array $versionCallbacks aan welke functie moet worden uitgevoerd voor elke versie, waarbij de functie de logica uitvoert om de gegevens te installeren. We itereren de lijst van alle versies, vergelijken elke versie met de vorige versie met version_compare en als deze groter is, voeren we de bijbehorende functie uit om de bijbehorende gegevens te installeren.

Merk op dat als $previousVersion null is (het is dus een nieuwe installatie), alle functies worden uitgevoerd.

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

Gegevens installeren voor elke specifieke versie

Tot slot moeten we de eigenlijke gegevens installeren (een pagina maken, een CPT vermelding, een optie toevoegen, enzovoort) voor elke versie.

In deze code voegen we de pagina Upcoming Events toe voor de plugin voor evenementenbeheer, voor v1.1:

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

  // ...
}

Vervolgens maken we de pagina Event Calendar voor v1.2 (in dit geval gebruiken we Gutenberg blokken op de pagina en voegen we een custom blok toe met de naam event-calendar):

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 code bij elkaar

We zijn klaar! De hele PHP code voor de klasse Plugin, die de logica bevat om de pluginversie bij te houden en de juiste gegevens te installeren, is de volgende:

<?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' => [],
        ],
      ]),
    ]);
  }
}

Samenvatting

WordPress plugins moeten vaak gegevens installeren bij de installatie. Als nieuwere versies van de plugin nieuwe features bieden, kan het bovendien nodig zijn dat de plugin gegevens installeert bij updates.

In dit artikel hebben we geleerd hoe we versies kunnen bijhouden en de juiste gegevens voor onze plugins kunnen installeren.

Heb jij een WordPress plugin die baat kan hebben bij het installeren van gegevens? 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.