Al desarrollar un plugin de WordPress, un paso crucial es preinstalar los datos esenciales, para garantizar que el plugin funcione sin problemas desde el principio. Tomemos, por ejemplo, un plugin de gestión de eventos. Una vez instalado, es enormemente beneficioso si el plugin genera automáticamente una página titulada Próximos eventos, mostrando una lista de eventos futuros.

Esta página preconfigurada, incrustada con un shortcode como [event_list number="10" scope="future" status="publish"], permite a los usuarios aprovechar inmediatamente las capacidades del plugin sin necesidad de leer su documentación.

La instalación de datos no sólo es útil cuando se instala el plugin por primera vez, sino también cuando se actualiza posteriormente. Por ejemplo, si una actualización introduce una función de vista de calendario, el plugin puede crear automáticamente una nueva página, Calendario de eventos, que muestre esta incorporación mediante un shortcode como [event_calendar status="publish"].

En general, el alcance de la instalación de los datos abarca varias necesidades:

  • Generar nuevas páginas con títulos y contenidos específicos.
  • Añadir entradas para tipos de entrada personalizados (CPTs, custom post types) creados por el plugin.
  • Insertar configuraciones predeterminadas en la tabla wp_options
  • Asignar nuevas capacidades a los roles de usuario
  • Asignar metadatos a los usuarios, para funciones nuevas o actualizadas proporcionadas por el plugin (por ejemplo, los usuarios podrían cambiar el formato de la fecha del evento, y se añade primero un valor por defecto para todos los usuarios)
  • Crear categorías de uso común en el contexto del plugin, como «conferencias» o «deportes»

La instalación de datos debe ser un proceso incremental, de lo contrario podríamos crear entradas duplicadas.

Por ejemplo, si la versión 1.1 de un plugin introduce una página de Próximos Eventos y un usuario actualiza desde la versión 1.0, sólo deben instalarse los nuevos datos relevantes para la versión 1.1. Esta actualización incremental garantiza que cuando salga la versión 1.2 con la función de calendario, sólo se añada la nueva página Calendario de Eventos, evitando cualquier duplicación de la página Próximos Eventos.

Por lo tanto, cuando se actualiza, el plugin debe recuperar qué versión anterior estaba instalada, e instalar los datos que corresponden sólo a la(s) nueva(s) versión(es).

Este artículo explica cómo instalar los datos iniciales, y seguir añadiendo nuevos datos con las actualizaciones posteriores, en nuestros plugins de WordPress.

Proporcionando la versión actual

Para manejar el proceso incremental, el plugin debe hacer un seguimiento de su versión actual, normalmente declarada en la cabecera del archivo principal del plugin. Pero, por supuesto, no podemos referenciarlo directamente desde ahí, ya que está dentro de un comentario PHP. Así que también definimos este valor en una variable y se lo proporcionamos a una clase de Plugin responsable de la inicialización y la configuración:

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

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

La clase Plugin, aprovechando la función de promoción de propiedades del Constructor de PHP 8.0, almacena esta versión, para que podamos hacer referencia a ella más adelante:

<?php

class Plugin {

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

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

  // ...
}

Observa cómo la lógica para inicializar y configurar el plugin se añade al método setup, no al constructor. Esto se debe a que el constructor debe evitar producir efectos secundarios; de lo contrario, podríamos producir errores al extender o componer la clase Plugin.

Veamos cómo podría ocurrir. Supongamos que añadimos una clase BetterPlugin que compone funciones de la clase Plugin:

class BetterPlugin {

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

Cada vez que se ejecuta new Plugin() dentro de printSomething, se crea una nueva instancia de Plugin. Si la lógica de configuración se añadiera al constructor, se ejecutaría cada vez que creáramos un nuevo objeto Plugin. En nuestro caso, queremos crear la página Próximos Eventos una sola vez, no varias. Añadiendo la lógica al método setup, podemos evitar este problema.

Seguimiento de la versión anterior

WordPress no proporciona una forma sencilla de recuperar la versión del plugin que se va a sustituir. Por lo tanto, debemos almacenar este valor por nosotros mismos en la tabla wp_options de la base de datos.

Almacena la versión en la entrada "myplugin_version", donde myplugin_ es el nombre del plugin (por ejemplo eventsmanager_version). Es importante anteponer siempre a todos nuestros ajustes myplugin_, para evitar posibles conflictos, ya que no podemos estar seguros de que otro plugin no añada una opción version.

Al cargar el plugin en cada solicitud, Plugin ya sabrá cuál es la versión actual (de la propiedad $pluginVersion anterior), y recuperará la última versión almacenada en la base de datos. Esta comparación determina el estado del plugin:

  • Nueva instalación: detecta si la base de datos carece de una entrada de versión para el plugin, lo que indica que es la primera vez que se instala (es decir, $storedPluginVersion es null)
  • Actualización: se identifica cuando la versión actual supera la versión almacenada en la base de datos, lo que indica una necesidad de actualización.
  • De lo contrario, no hay cambios

Siempre que haya un cambio, llamamos a prepareAndInstallPluginSetupData para que instale los datos adecuados, ya sea para una nueva instalación (en cuyo caso debe instalar todos los datos de todas las versiones) o para una actualización (instala sólo los datos de todas las versiones nuevas). La variable anulable $previousVersion indica de qué situación se trata ($previousVersion es null => nueva instalación).

Después de llamar a este método, también debemos almacenar la versión actual del plugin en la base de datos, convirtiéndose en la nueva versión «última almacenada». Esto debe hacerse después de llamar a prepareAndInstallPluginSetupData para que, si este método produce un error (por ejemplo, lanzando un RuntimeException) y los datos no se instalan, la versión anterior siga almacenada en la base de datos, y se intente una nueva ronda de instalación de datos en la siguiente solicitud.

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

Observa que prepareAndInstallPluginSetupData (y la posterior actualización de la BD) se ejecuta en el hook de acción init. Esto es para asegurarse de que todos los datos del CMS están listos para su recuperación y manipulación.

En particular, no se puede acceder a las taxonomías (etiquetas y categorías) antes del hook init . Si el proceso de instalación del plugin necesitara crear una entrada CPT y asignarle una categoría personalizada, este proceso sólo podría ejecutarse a partir del hook init.

Acceder a la última versión almacenada desde la BD en cada petición no es lo ideal desde el punto de vista del rendimiento. Para mejorar esto, combina todas las opciones que necesita el plugin en un array, almacénalas en una única entrada y accede a todas ellas con una única llamada a la BD.

Por ejemplo, si el plugin también necesitara almacenar una opción myplugin_date_format para mostrar la fecha del evento, podemos crear una única entrada myplugin_options con las propiedades version y date_format.

Para acceder a la última versión almacenada, hay que adaptar entonces el código PHP de la siguiente manera:

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

Evitar peticiones concurrentes instalando datos duplicados

Existe la posibilidad de que el proceso de instalación se active más de una vez si dos o más usuarios acceden al wp-admin exactamente al mismo tiempo. Para evitar que los mismos datos se instalen dos o más veces, utilizamos un transient como bandera para permitir que sólo la primera petición instale los datos:

  • Comprueba si existe el transitorio myplugin_installing_plugin_setup_data (una vez más, este nombre debe ir precedido de myplugin_); si es así, no hagas nada (ya que algún otro proceso está instalando los datos)
  • En caso contrario, almacena el transitorio en la base de datos durante un tiempo máximo razonable para instalar los datos (por ejemplo, 30 segundos)
  • Instala los datos
  • Elimina el transitorio

Aquí tienes el código:

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

Instalar los datos para todas las versiones

Como ya hemos dicho, si actualizamos el plugin, sólo debemos instalar los datos de las nuevas versiones, no de todas. Eso significa que tenemos que gestionar qué datos instalar versión por versión.

En el siguiente código, el array $versionCallbacks indica qué función ejecutar para cada versión con la función que ejecuta la lógica para instalar los datos. Iteramos la lista de todas ellas, comparamos cada una con la versión anterior utilizando version_compare y, si es mayor, ejecutamos la función correspondiente para instalar los datos correspondientes.

Observa que si $previousVersion es null (es decir, es una nueva instalación), entonces se ejecutan todas las funciones.

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

Instalar los datos para cada versión específica

Por último, debemos instalar los datos reales (crear una página, una entrada CPT, añadir una opción, etc.) para cada versión.

En este código, añadimos la página Próximos Eventos para el plugin del gestor de eventos, para 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"]',
    ]);
  }

  // ...
}

A continuación, creamos la página Calendario de Eventos para v1.2 (en este caso, utilizando bloques Gutenberg en la página, añadiendo un bloque personalizado llamado 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' => [],
        ],
      ]),
    ]);
  }
}

Todo el código junto

¡Ya hemos terminado! Todo el código PHP de la clase Plugin, que contiene la lógica para rastrear la versión del plugin e instalar los datos apropiados, es este:

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

Resumen

Los plugins de WordPress a menudo necesitan instalar datos al instalarse. Además, a medida que las nuevas versiones del plugin proporcionan nuevas funciones, el plugin también puede necesitar instalar datos cuando se actualiza.

En este artículo, hemos aprendido a realizar un seguimiento de las versiones y a instalar los datos adecuados para nuestros plugins.

¿Tienes un plugin de WordPress que pueda beneficiarse de la instalación de datos? Háznoslo saber en los comentarios.

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.