Quando si sviluppa un plugin per WordPress, un passo fondamentale è quello di preinstallare i dati essenziali, per garantire che il plugin funzioni senza problemi fin dall’inizio. Prendiamo ad esempio un plugin per la gestione degli eventi. Al momento dell’installazione, è estremamente utile che il plugin generi automaticamente una pagina intitolata Prossimi eventi, che mostra un elenco di eventi futuri.

Questa pagina preconfigurata, incorporata con uno shortcode come [event_list number="10" scope="future" status="publish"], permette agli utenti di sfruttare immediatamente le funzionalità del plugin senza dover leggere la sua documentazione.

L’installazione dei dati è utile non solo quando si installa per la prima volta il plugin, ma anche quando lo si aggiorna successivamente. Ad esempio, se un aggiornamento introduce una funzione di visualizzazione del calendario, il plugin può creare automaticamente una nuova pagina, Calendario degli eventi, che mostra questa aggiunta attraverso uno shortcode come [event_calendar status="publish"].

In generale, la portata dell’installazione dei dati abbraccia diverse esigenze:

  • Generare nuove pagine con titoli e contenuti specifici.
  • Aggiungere voci per i tipi di post personalizzati (CPT) creati dal plugin.
  • Inserire impostazioni predefinite nella tabella wp_options
  • Assegnare nuove funzionalità ai ruoli degli utenti
  • Assegnare metadati agli utenti per le funzionalità nuove o aggiornate fornite dal plugin (ad esempio, gli utenti possono modificare il formato della data dell’evento e viene aggiunto un valore predefinito per tutti gli utenti)
  • Creare categorie comunemente utilizzate nel contesto del plugin, come “conferenze” o “sport”

L’installazione dei dati deve essere un processo incrementale, altrimenti si potrebbero creare voci duplicate.

Ad esempio, se la versione 1.1 di un plugin introduce la pagina dei prossimi eventi e un utente esegue l’aggiornamento dalla versione 1.0, dovranno essere installati solo i nuovi dati rilevanti per la versione 1.1. Questo aggiornamento incrementale assicura che quando la versione 1.2 verrà rilasciata con la funzione calendario, verrà aggiunta solo la nuova pagina Calendario eventi, evitando la duplicazione della pagina Prossimi eventi.

Pertanto, al momento dell’aggiornamento, il plugin deve recuperare la versione precedente installata e installare solo i dati corrispondenti alla nuova versione.

Questo articolo spiega come installare i dati iniziali e continuare ad aggiungerne di nuovi con i successivi aggiornamenti nei nostri plugin WordPress.

Fornire la versione corrente

Per gestire il processo incrementale, il plugin deve tenere traccia della sua versione corrente, in genere dichiarata nell’intestazione del file principale del plugin. Ma ovviamente non possiamo fare riferimento direttamente a questo valore, poiché si trova all’interno di un commento PHP. Quindi definiamo questo valore in una variabile e lo forniamo a una classe Plugin responsabile dell’inizializzazione e della configurazione:

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

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

La classe Plugin, sfruttando la Constructor Property Promotion di PHP 8.0, memorizza questa versione, in modo da poterla consultare in seguito:

<?php

class Plugin {

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

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

  // ...
}

Notiamo che la logica per inizializzare e configurare il plugin viene aggiunta al metodo setup e non al constructor. Questo perché il constructor deve evitare di produrre effetti collaterali; in caso contrario, potremmo generare dei bug quando estendiamo o componiamo la classe Plugin.

Vediamo come potrebbe accadere. Supponiamo di aggiungere una classe BetterPlugin che compone le funzionalità della classe Plugin:

class BetterPlugin {

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

Ogni volta che si esegue new Plugin() all’interno di printSomething, viene creata una nuova istanza di Plugin. Se la logica di configurazione fosse aggiunta al constructor, verrebbe eseguita ogni volta che si crea un nuovo oggetto Plugin. Nel nostro caso, vogliamo creare la pagina dei prossimi eventi solo una volta, non più volte. Aggiungendo la logica al metodo setup, possiamo evitare questo problema.

Tracciare la versione precedente

WordPress non fornisce un modo pratico per recuperare la versione del plugin che viene sostituito. Pertanto, dobbiamo memorizzare questo valore nella tabella wp_options del database.

Memorizziamo la versione alla voce "myplugin_version", dove myplugin_ è il nome del plugin (ad esempio eventsmanager_version). È importante anteporre sempre a tutte le nostre impostazioni la voce myplugin_, per evitare potenziali conflitti, poiché non possiamo essere sicuri che un altro plugin non aggiunga un’opzione version.

Quando il plugin viene caricato a ogni richiesta, Plugin conosce già la versione corrente (dalla proprietà $pluginVersion precedente) e recupera l’ultima versione memorizzata dal database. Questo confronto determina lo stato del plugin:

  • Nuova installazione: rileva se nel database manca una voce relativa alla versione del plugin, indicando la prima installazione (ad esempio, $storedPluginVersion è null)
  • Aggiornamento: viene identificato quando la versione corrente supera quella memorizzata nel database, segnalando la necessità di un aggiornamento.
  • Altrimenti, non c’è alcun cambiamento

Ogni volta che c’è un cambiamento, chiamiamo prepareAndInstallPluginSetupData per installare i dati appropriati, sia per una nuova installazione (nel qual caso deve installare tutti i dati per tutte le versioni) sia per un aggiornamento (installa i dati solo per tutte le nuove versioni). La variabile nullable $previousVersion indica di quale situazione si tratta ($previousVersion è null => nuova installazione).

Dopo aver richiamato questo metodo, dobbiamo anche memorizzare la versione corrente del plugin nel database, facendo diventare la nuova versione “ultima memorizzata”. Questo deve essere fatto dopo aver chiamato prepareAndInstallPluginSetupData in modo che se questo metodo produce un errore (ad esempio lanciando un RuntimeException) e i dati non vengono installati, la versione precedente è ancora memorizzata nel database e si tenterà una nuova installazione dei dati alla prossima richiesta.

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

Notate che prepareAndInstallPluginSetupData (e il successivo aggiornamento del database) viene eseguito sull’action hook init. Questo per assicurarsi che tutti i dati del CMS siano pronti per essere recuperati e manipolati.

In particolare, non è possibile accedere alle tassonomie (tag e categorie) prima dell’hook init. Se il processo di installazione del plugin richiede la creazione di una voce CPT e l’assegnazione di una categoria personalizzata, questo processo può essere eseguito solo a partire dall’hook init.

Accedere all’ultima versione memorizzata dal DB a ogni richiesta non è l’ideale dal punto di vista delle prestazioni. Per migliorare questo aspetto, è possibile combinare tutte le opzioni necessarie al plugin in un array, memorizzarle in un’unica voce e quindi accedervi con un’unica chiamata al DB.

Ad esempio, se il plugin avesse bisogno di memorizzare anche l’opzione myplugin_date_format per visualizzare la data dell’evento, potremmo creare una singola voce myplugin_options con le proprietà version e date_format.

Per accedere all’ultima versione memorizzata, il codice PHP deve essere adattato in questo modo:

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

Evitare le richieste simultanee che installano dati duplicati

Esiste la possibilità che il processo di installazione venga attivato più di una volta se due o più utenti accedono al wp-admin esattamente nello stesso momento. Per evitare che gli stessi dati vengano installati due o più volte, utilizziamo un transitorio come flag per consentire solo la prima richiesta di installazione dei dati:

  • Controlliamo se il transitorio myplugin_installing_plugin_setup_data esiste (ancora una volta, questo nome deve essere preceduto da myplugin_); in caso affermativo, non facciamo nulla (poiché qualche altro processo sta installando i dati)
  • Altrimenti, memorizziamo il transitorio nel database per un tempo massimo ragionevole per l’installazione dei dati (ad esempio, 30 secondi)
  • Installiamo i dati
  • Eliminiamo il transitorio

Ecco il codice:

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

Installare i dati per tutte le versioni

Come abbiamo già detto, se aggiorniamo il plugin, dobbiamo installare solo i dati per le nuove versioni, non per tutte. Ciò significa che dobbiamo gestire quali dati installare versione per versione.

Nel codice sottostante, l’array $versionCallbacks indica la funzione da eseguire per ogni versione e la funzione che esegue la logica per installare i dati. Eseguiamo un’iterazione dell’elenco di tutte le versioni, confrontiamo ciascuna di esse con la versione precedente utilizzando version_compare e, se è maggiore, eseguiamo la funzione corrispondente per installare i dati corrispondenti.

Notate che se $previousVersion è null (cioè è una nuova installazione), tutte le funzioni vengono eseguite.

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

Installazione dei dati per ogni versione specifica

Infine, dobbiamo installare i dati effettivi (creare una pagina, una voce CPT, aggiungere un’opzione, ecc.) per ogni versione.

In questo codice, aggiungiamo la pagina Prossimi eventi per il plugin Gestione eventi, per 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"]',
    ]);
  }

  // ...
}

Poi, creiamo la pagina Calendario eventi per v1.2 (in questo caso, utilizzando i blocchi di Gutenberg sulla pagina, aggiungendo un blocco personalizzato chiamato 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' => [],
        ],
      ]),
    ]);
  }
}

Tutto il codice insieme

Abbiamo finito! L’intero codice PHP della classe Plugin, che contiene la logica per tracciare la versione del plugin e installare i dati appropriati, è il seguente:

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

Riepilogo

I plugin di WordPress hanno spesso bisogno di installare dei dati al momento dell’installazione. Inoltre, man mano che le nuove versioni del plugin offrono nuove funzionalità, è possibile che il plugin debba installare dei dati al momento dell’aggiornamento.

In questo articolo abbiamo imparato come tenere traccia delle versioni e installare i dati appropriati per i nostri plugin.

Avete un plugin di WordPress che può trarre vantaggio dall’installazione di dati? Fatecelo sapere nei commenti.

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.