Lors du développement d’une extension WordPress, une étape cruciale consiste à préinstaller les données essentielles, afin de garantir le bon fonctionnement de l’extension dès le départ. Prenons l’exemple d’une extension de gestion d’évènements. Lors de l’installation, il est extrêmement utile que l’extension génère automatiquement une page intitulée « Évènements à venir » qui affiche une liste des évènements futurs.

Cette page préconfigurée, incorporée avec un code court comme [event_list number="10" scope="future" status="publish"], permet aux utilisateurs d’exploiter immédiatement les capacités de l’extension sans avoir à lire sa documentation.

L’installation des données est utile non seulement lors de la première installation de l’extension, mais aussi lors des mises à jour ultérieures. Par exemple, si une mise à jour introduit une fonction d’affichage du calendrier, l’extension peut créer automatiquement une nouvelle page, Calendrier des évènements, présentant cet ajout au moyen d’un code court tel que [event_calendar status="publish"].

En général, le champ d’application de l’installation des données couvre différents besoins :

  • Générer de nouvelles pages avec des titres et des contenus spécifiques.
  • Ajouter des entrées pour les types de publication personnalisés (CPT) créés par l’extension.
  • Insérer des réglages par défaut dans la table wp_options
  • Attribuer de nouvelles capacités aux rôles des utilisateurs
  • Assigner des métadonnées aux utilisateurs, pour des fonctionnalités nouvelles ou mises à jour fournies par l’extension (par exemple, les utilisateurs peuvent changer le format de la date de l’évènement, et une valeur par défaut est d’abord ajoutée pour tous les utilisateurs)
  • Créer des catégories couramment utilisées dans le contexte de l’extension, telles que « conférences » ou « sports »

L’installation des données doit être un processus incrémental, sinon nous pourrions créer des entrées en double.

Par exemple, si la version 1.1 d’une extension introduit une page Évènements à venir et qu’un utilisateur effectue une mise à jour à partir de la version 1.0, seules les nouvelles données relatives à la version 1.1 doivent être installées. Cette mise à jour incrémentale garantit que, lorsque la version 1.2 sera lancée avec la fonction de calendrier, seule la nouvelle page Calendrier des évènements sera ajoutée, évitant ainsi toute duplication de la page Évènements à venir.

Par conséquent, lors de la mise à jour, l’extension doit récupérer la version précédente installée et installer les données correspondant uniquement à la ou aux nouvelles versions.

Cet article explique comment installer les données initiales, et continuer à ajouter de nouvelles données lors des mises à jour ultérieures, dans nos extensions WordPress.

Fournir la version actuelle

Pour gérer le processus incrémental, l’extension doit suivre sa version actuelle, typiquement déclarée dans l’en-tête du fichier principal de m’extension. Mais bien sûr, nous ne pouvons pas y faire référence directement, car elle se trouve à l’intérieur d’un commentaire PHP. Nous définissons donc également cette valeur dans une variable et la fournissons à une classe Plugin responsable de l’initialisation et de la configuration :

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

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

La classe Plugin, tirant parti de la promotion de la propriété Constructor de PHP 8.0, stocke cette version, de sorte que nous puissions la référencer plus tard :

<?php

class Plugin {

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

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

  // ...
}

Remarquez que la logique d’initialisation et de configuration de l’extension est ajoutée à la méthode setup, et non au constructeur. C’est parce que le constructeur doit éviter de produire des effets de bord ; sinon, nous pourrions produire des bogues lors de l’extension ou de la composition de la classe Plugin.

Voyons comment cela pourrait se produire. Supposons que nous ajoutions une classe BetterPlugin qui compose des fonctionnalités de la classe Plugin:

class BetterPlugin {

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

Chaque fois que l’on exécute new Plugin() à l’intérieur de printSomething, une nouvelle instance de Plugin est créée. Si la logique de configuration était ajoutée au constructeur, elle serait exécutée chaque fois que nous créons un nouvel objet Plugin. Dans notre cas, nous voulons créer la page Évènements à venir une seule fois, et non plusieurs fois. En ajoutant la logique à la méthode setup, nous pouvons éviter ce problème.

Suivi de la version précédente

WordPress ne fournit pas de moyen pratique pour récupérer la version de l’extension remplacée. Nous devons donc stocker cette valeur nous-mêmes dans la table wp_options de la base de données.

Enregistrez la version sous l’entrée "myplugin_version", où myplugin_ est le nom de l’extension (par exemple eventsmanager_version). Il est important de toujours faire précéder tous nos paramètres de myplugin_, afin d’éviter les conflits potentiels, car nous ne pouvons pas être surs qu’une autre extension n’ajoutera pas une option version.

Lors du chargement de l’extension à chaque requête, Plugin connaitra déjà la version actuelle (à partir de la propriété $pluginVersion) et récupèrera la dernière version stockée dans la base de données. Cette comparaison détermine l’état de l’extension :

  • Nouvelle installation : détecte si la base de données ne contient pas d’entrée de version pour l’extension, ce qui indique une première installation (par exemple, $storedPluginVersion est null)
  • Mise à jour : identifiée lorsque la version actuelle dépasse la version stockée dans la base de données, ce qui indique la nécessité d’une mise à jour.
  • Sinon, il n’y a pas de changement

Chaque fois qu’il y a un changement, nous appelons prepareAndInstallPluginSetupData pour qu’il installe les données appropriées, qu’il s’agisse d’une nouvelle installation (auquel cas il doit installer toutes les données pour toutes les versions) ou d’une mise à jour (installer les données uniquement pour toutes les nouvelles versions). La variable nullable $previousVersion indique de quelle situation il s’agit ($previousVersion est null => nouvelle installation).

Après avoir appelé cette méthode, nous devons également stocker la version actuelle de l’extension dans la base de données, devenant ainsi la nouvelle version « last stored ». Cela doit être fait après l’appel à prepareAndInstallPluginSetupData, de sorte que si cette méthode produit une erreur (par exemple, en lançant un RuntimeException) et que les données ne sont pas installées, la version précédente est toujours stockée dans la base de données, et un nouveau cycle d’installation des données sera tenté lors de la prochaine requête.

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

Notez que prepareAndInstallPluginSetupData (et la mise à jour ultérieure de la base de données) est exécuté sur le crochet d’action init. Cela permet de s’assurer que toutes les données du CMS sont prêtes à être récupérées et manipulées.

En particulier, les taxonomies (étiquettes et catégories) ne sont pas accessibles avant le crochet init. Si le processus d’installation de l’extension devait créer une entrée CPT et lui attribuer une catégorie personnalisée, ce processus ne pourrait être exécuté qu’à partir du crochet init.

Accéder à la dernière version stockée dans la base de données à chaque requête n’est pas idéal du point de vue des performances. Pour améliorer cela, combinez toutes les options nécessaires à l’extension dans un tableau, stockez-les dans une seule entrée, puis accédez-y avec un seul appel à la base de données.

Par exemple, si l’extension a également besoin de stocker une option myplugin_date_format pour afficher la date de l’évènement, nous pouvons créer une entrée unique myplugin_options avec les propriétés version et date_format.

Pour accéder à la dernière version stockée, le code PHP doit être adapté comme suit :

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

Éviter les requêtes concurrentes d’installation de données dupliquées

Il est possible que le processus d’installation soit déclenché plus d’une fois si deux utilisateurs ou plus accèdent au wp-admin exactement au même moment. Pour éviter que les mêmes données soient installées deux fois ou plus, nous utilisons un transient comme drapeau pour n’autoriser que la première demande d’installation des données :

  • Vérifiez si le transient myplugin_installing_plugin_setup_data existe (une fois de plus, ce nom doit être précédé de myplugin_) ; si c’est le cas, ne faites rien (car un autre processus est en train d’installer les données)
  • Dans le cas contraire, stockez le transient dans la base de données pendant une durée maximale raisonnable pour l’installation des données (par exemple, 30 secondes)
  • Installez les données
  • Supprimez le transient

Voici le 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 des données pour toutes les versions

Comme nous l’avons mentionné précédemment, si nous mettons à jour l’extension, nous ne devons installer que les données des nouvelles versions, et non pas toutes les versions. Cela signifie que nous devons gérer les données à installer version par version.

Dans le code ci-dessous, le tableau $versionCallbacks indique la fonction à exécuter pour chaque version, la fonction exécutant la logique d’installation des données. Nous itérons la liste de toutes les versions, comparons chacune d’elles à la version précédente à l’aide de version_compare et, si elle est supérieure, exécutons la fonction correspondante pour installer les données correspondantes.

Notez que si $previousVersion est null (c’est-à-dire qu’il s’agit d’une nouvelle installation), toutes les fonctions sont exécutées.

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

Installation des données pour chaque version spécifique

Enfin, nous devons installer les données réelles (créer une page, une entrée CPT, ajouter une option, etc) pour chaque version.

Dans ce code, nous ajoutons la page Évènements à venir pour l’extension de gestion des évènements, pour 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"]',
    ]);
  }

  // ...
}

Ensuite, nous créons la page Events Calendar pour v1.2 (dans ce cas, en utilisant les blocs Gutenberg sur la page, en ajoutant un bloc personnalisé appelé 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' => [],
        ],
      ]),
    ]);
  }
}

Tout le code ensemble

Nous avons terminé ! L’ensemble du code PHP pour la classe Plugin, contenant la logique pour suivre la version de l’extension et installer les données appropriées, est le suivant :

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

Résumé

Les extensions WordPress ont souvent besoin d’installer des données lors de l’installation. En outre, comme les nouvelles versions de l’extension fournissent de nouvelles fonctionnalités, l’extension peut également avoir besoin d’installer des données lorsqu’elle est mise à jour.

Dans cet article, nous avons appris comment suivre les versions et installer les données appropriées pour nos extensions.

Vous avez une extension WordPress qui pourrait bénéficier de l’installation de données ? Faites-le nous savoir dans les commentaires.

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.