Ao desenvolver um plugin WordPress, uma etapa crucial é a pré-instalação dos dados essenciais, garantindo que o plugin funcione sem problemas desde o início. Veja, por exemplo, um plugin de gerenciador de eventos. Após a instalação, é extremamente útil que o plugin gere automaticamente uma página intitulada Próximos eventos, exibindo uma lista de eventos futuros.

Essa página pré-configurada, incorporada com um shortcode como [event_list number="10" scope="future" status="publish"], permite que os usuários aproveitem imediatamente as capacidades do plugin sem a necessidade de ler toda a sua documentação.

A instalação de dados é útil não apenas quando você instala o plugin pela primeira vez, mas também quando o atualiza posteriormente. Por exemplo, se uma atualização introduzir um recurso de visualização de calendário, o plugin poderá criar automaticamente uma nova página, Calendário de Eventos, mostrando essa adição por meio de um shortcode como [event_calendar status="publish"].

Em geral, o escopo da instalação de dados abrange várias necessidades:

  • Gerar novas páginas com títulos e conteúdos específicos
  • Adicionar entradas para tipos de artigos personalizados (CPTs) criados pelo plugin
  • Inserir configurações padrão na tabela wp_options
  • Atribuir novos recursos a funções de usuário
  • Atribuir metadados aos usuários, para recursos novos ou atualizados fornecidos pelo plugin (por exemplo, os usuários poderiam alterar o formato da data do evento, e um valor padrão é adicionado primeiro para todos os usuários)
  • Criar categorias comumente usadas no contexto do plugin, como “conferências” ou “esportes”

A instalação de dados deve ser um processo incremental, para evitar criar entradas duplicadas.

Por exemplo, se a versão 1.1 de um plugin introduzir uma página Próximos eventos e um usuário atualizar a partir da versão 1.0, somente os novos dados relevantes para a versão 1.1 deverão ser instalados. Essa atualização incremental garante que, quando a versão 1.2 for lançada com o recurso de calendário, somente a nova página Calendário de Eventos será adicionada, evitando qualquer duplicação da página Próximos eventos.

Portanto, quando atualizado, o plugin deve recuperar a versão anterior que estava instalada e instalar os dados que correspondem somente à(s) nova(s) versão(ões).

Este artigo explica como instalar os dados iniciais e continuar adicionando novos dados em atualizações posteriores de nossos plugins WordPress.

Fornecendo a versão atual

Para lidar com o processo incremental, o plugin deve rastrear sua versão atual, normalmente declarada no cabeçalho do arquivo principal do plugin. Mas, claro, não podemos referenciá-la diretamente de lá, pois está dentro de um comentário PHP. Então, também definimos esse valor em uma variável e o fornecemos a uma classe Plugin responsável pela inicialização e configuração:

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

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

A classe Plugin, aproveitando o recurso de promoção de propriedade do Constructor do PHP 8.0, armazena essa versão para que possamos referenciá-la posteriormente:

<?php

class Plugin {

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

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

  // ...
}

Observe como a lógica para inicializar e configurar o plugin é adicionada ao método setup, e não ao construtor. Isso ocorre porque o construtor deve evitar a produção de efeitos colaterais; caso contrário, poderíamos produzir bugs ao estender ou compor a classe Plugin.

Vejamos como isso poderia acontecer. Digamos que você adicione uma classe BetterPlugin que componha a funcionalidade da classe Plugin:

class BetterPlugin {

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

Sempre que você executa new Plugin() dentro de printSomething, uma nova instância de Plugin é criada. Se a lógica de configuração fosse adicionada ao construtor, ela seria executada toda vez que criássemos um novo objeto Plugin. No nosso caso, queremos criar a página Próximos eventos apenas uma vez, não várias. Ao adicionar a lógica ao método setup, evitamos esse problema.

Rastreamento da versão anterior

O WordPress não oferece uma maneira conveniente de recuperar a versão do plugin que está sendo substituído. Portanto, precisamos armazenar esse valor por nós mesmos na tabela wp_options do banco de dados.

Armazene a versão na entrada "myplugin_version", em que myplugin_ é o nome do plugin (por exemplo, eventsmanager_version). É importante sempre preceder todas as nossas configurações com myplugin_, para evitar possíveis conflitos, pois não podemos ter certeza de que outro plugin não adicionará uma opção version.

Ao carregar o plugin em cada solicitação, Plugin já saberá qual é a versão atual (da propriedade $pluginVersion anteriormente) e recuperará a última versão armazenada no banco de dados. Essa comparação determina o status do plugin:

  • Nova instalação: detecta se o banco de dados não tem uma entrada de versão para o plugin, indicando a primeira configuração (ou seja, $storedPluginVersion é null)
  • Atualização: identificada quando a versão atual excede a versão armazenada no banco de dados, indicando uma solicitação de atualização.
  • Caso contrário, não há alteração

Sempre que houver uma alteração, chamamos prepareAndInstallPluginSetupData para instalar os dados apropriados, seja para uma nova instalação (caso em que deve instalar todos os dados de todas as versões) ou uma atualização (instalar somente os dados para todas as novas versões). A variável $previousVersion, que pode ser anulada, indica em que situação você se encontra ($previousVersion é null => nova instalação).

Depois de chamar esse método, também devemos armazenar a versão atual do plugin no banco de dados, tornando-se a nova última versão armazenada (“last stored”). Isso deve ser feito após a chamada de prepareAndInstallPluginSetupData, para que, se esse método produzir um erro (por exemplo, lançando um RuntimeException) e os dados não forem instalados, a versão anterior ainda esteja armazenada no banco de dados e uma nova rodada de instalação de dados seja tentada na próxima solicitação.

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

Observe que prepareAndInstallPluginSetupData (e a subsequente atualização do banco de dados) é executada no hook de ação init. Isso serve para garantir que todos os dados do CMS estejam prontos para recuperação e manipulação.

Em particular, as taxonomias (tags e categorias) não podem ser acessadas antes do hook init. Se o processo de instalação do plugin precisasse criar uma entrada de CPT e atribuir uma categoria personalizada a ela, esse processo só poderia ser executado após o hook init.

Acessar a última versão armazenada do banco de dados em cada solicitação não é ideal do ponto de vista do desempenho. Para melhorar isso, combine todas as opções necessárias ao plugin em um array, armazene-as em uma única entrada e acesse todas elas com uma única chamada ao banco de dados.

Por exemplo, se o plugin também precisasse armazenar uma opção myplugin_date_format para exibir a data do evento, poderíamos criar uma única entrada myplugin_options com as propriedades version e date_format.

Para acessar a última versão armazenada, o código PHP deve ser adaptado da seguinte forma:

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

Evitando solicitações simultâneas que instalam dados duplicados

Existe a possibilidade de que o processo de instalação seja acionado mais de uma vez se dois ou mais usuários acessarem o wp-admin exatamente ao mesmo tempo. Para evitar que os mesmos dados sejam instalados duas ou mais vezes, usamos um transiente como sinalizador para permitir que apenas a primeira solicitação instale os dados:

  • Verifique se o transiente myplugin_installing_plugin_setup_data existe (mais uma vez, esse nome deve ser precedido de myplugin_); em caso afirmativo, não faça nada (pois algum outro processo está instalando os dados)
  • Caso contrário, armazene o transiente no banco de dados por um período máximo razoável de tempo para instalar os dados (por exemplo, 30 segundos)
  • Instale os dados
  • Exclua o transiente

Aqui está o 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...
  }
}

Instalando os dados para todas as versões

Conforme mencionamos, ao atualizar o plugin, devemos instalar apenas os dados das novas versões, não de todas elas. Isso significa que precisamos gerenciar quais dados instalar, versão por versão.

No código abaixo, o array $versionCallbacks indica a função a ser executada para cada versão, com a função executando a lógica para instalar os dados. Iteramos a lista de todas elas, comparamos cada uma com a versão anterior usando version_compare e, se for maior, executamos a função correspondente para instalar os dados correspondentes.

Observe que se $previousVersion for null (ou seja, é uma nova instalação), todas as funções serão executadas.

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

Instalação de dados para cada versão específica

Por fim, devemos instalar os dados reais (criar uma página, uma entrada CPT, adicionar uma opção, etc.) para cada versão.

Neste código, adicionamos a página Próximos eventos ao plugin do gerenciador 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"]',
    ]);
  }

  // ...
}

Em seguida, criamos a página Calendário de Eventos para v1.2 (nesse caso, usando blocos do Gutenberg na página, adicionando um bloco personalizado chamado 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 o código junto

Terminamos! Todo o código PHP da classe Plugin, contendo a lógica para rastrear a versão do plugin e instalar os dados apropriados, é o seguinte:

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

Resumo

Os plugins WordPress geralmente precisam instalar dados na instalação. Além disso, como as versões mais recentes do plugin fornecem novos recursos, o plugin também pode precisar instalar dados quando é atualizado.

Neste artigo, aprendemos como rastrear versões e instalar os dados apropriados para nossos plugins.

Você tem um plugin WordPress que pode se beneficiar da instalação de dados? Conte pra nós nos comentários.

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.