O WordPress é um CMS antigo, mas também o mais usado. Graças ao seu histórico de suporte a versões PHP desatualizadas e códigos legados, ainda falta implementar práticas modernas de codificação – a abstração do WordPress é um exemplo.

Por exemplo, seria muito melhor dividir o código do núcleo do WordPress em pacotes gerenciados pelo Composer. Ou talvez, para carregar automaticamente classes WordPress a partir de caminhos de arquivos.

Este artigo irá ensiná-lo a abstrair o código WordPress manualmente e a usar os recursos de plugins de abstração WordPress.

Problemas com a integração de ferramentas WordPress e PHP

Devido a sua arquitetura antiga, ocasionalmente encontramos problemas ao integrar o WordPress com ferramentas para bases de código PHP, como o analisador estático PHPStan, a biblioteca de testes de unidade PHPUnit e a biblioteca de namespace-scoping PHP-Scoper. Por exemplo, considere os seguintes casos:

O código WordPress dentro de nossos projetos será apenas uma fração do total; o projeto também conterá código de negócios agnóstico do CMS subjacente. No entanto, apenas por ter algum código WordPress, o projeto pode não se integrar corretamente com as ferramentas.

Por causa disso, poderia fazer sentido dividir o projeto em pacotes, alguns deles contendo código WordPress e outros tendo apenas código de negócios usando PHP “vanilla” e sem código WordPress. Desta forma, estes últimos pacotes não serão afetados pelos problemas descritos acima, mas podem ser perfeitamente integrados com o tooling.

O que é abstração de código?

A abstração de código remove dependências fixas do código, produzindo pacotes que interagem uns com os outros através de contratos. Estes pacotes podem então ser adicionados a diferentes aplicativos com diferentes pilhas, maximizando a sua usabilidade. O resultado da abstração de código é uma base de código claramente desacoplada, baseada nos seguintes pilares:

  1. Código contra interfaces, não implementações.
  2. Crie pacotes e distribua-os através do Composer.
  3. Cole todas as partes através de injeção de dependência.

Código contra interfaces, não contra implementações

A codificação contra interfaces é a prática de usar contratos para que peças de código interajam umas com as outras. Um contrato é simplesmente uma interface PHP (ou qualquer linguagem diferente) que define que funções estão disponíveis e suas assinaturas, ou seja, que entradas recebem e suas saídas.

Uma interface declara a intenção da funcionalidade sem explicar como a funcionalidade será implementada. Ao acessar funcionalidades através de interfaces, nosso aplicativo pode contar com peças de código autônomas que atingem um objetivo específico sem saber, ou se preocupar com a forma como o fazem. Desta forma, o aplicativo não precisa ser adaptada para mudar para outro pedaço de código que atinja o mesmo objetivo – por exemplo, de um provedor diferente.

Exemplo de contratos

O código a seguir usa o contrato da Symfony CacheInterface e o contrato da CacheItemInterface da Recomendação Padrão do PHP (PSR) para implementar a funcionalidade de cache:

use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;

$value = $cache->get('my_cache_key', function (CacheItemInterface $item) {
    $item->expiresAfter(3600);
    return 'foobar';
});

$cache implementa a CacheInterface, que define o método get para recuperar um objeto do cache. Ao aceder a esta funcionalidade através do contrato, o aplicativo pode ficar alheia ao local onde se encontra a cache. Seja na memória, disco, banco de dados, rede ou em qualquer outro lugar. Mesmo assim, ele tem que executar a função. A CacheItemInterface define o método que expiresAfter de declarar por quanto tempo o item deve ser mantido no cache. O aplicativo pode invocar este método sem se preocupar com o que é o objeto em cache; ela só se preocupa com quanto tempo ele deve ser mantido em cache.

Codificação contra interfaces no WordPress

Como estamos abstraindo o código WordPress, o resultado será que o aplicativo não fará referência direta ao código WordPress, mas sempre através de uma interface. Por exemplo, a função get_posts do WordPress tem esta assinatura:

/**
 * @param array $args
 * @return WP_Post[]|int[] Array of post objects or post IDs.
 */
function get_posts( $args = null )

Em vez de invocar este método diretamente, podemos acessá-lo através do contrato Owner\MyApp\Contracts\PostsAPIInterface:

namespace Owner\MyApp\Contracts;

interface PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[];
}

Observe que a função get_posts do WordPress pode retornar objetos da classe WP_Post, que é específica para o WordPress. Ao abstrair o código, é necessário remover esse tipo de dependência fixa. O método get_posts no contrato retorna objetos do tipo PostInterface, permitindo que você faça referência à classe WP_Post sem ser explícito sobre ela. A classe PostInterface precisará fornecer acesso a todos os métodos e atributos do WP_Post:

namespace Owner\MyApp\Contracts;

interface PostInterface
{
  public function get_ID(): int;
  public function get_post_author(): string;
  public function get_post_date(): string;
  // ...
}

A execução desta estratégia pode mudar o nosso entendimento de onde o WordPress cabe na nossa pilha. Em vez de pensarmos no WordPress como o próprio aplicativo (sobre o qual instalamos temas e plugins), podemos pensar nele simplesmente como uma outra dependência dentro do aplicativo, substituível como qualquer outro componente. (Mesmo que não substituamos o WordPress na prática, ele é substituível de um ponto de vista conceitual).

Criação e distribuição de pacotes

O Composer é um gerenciador de pacotes para PHP. Ele permite que aplicativos PHP recuperem pacotes (ou seja, pedaços de código) de um repositório e os instalem como dependências. Para dissociar o aplicativo do WordPress, devemos distribuir seu código em pacotes de dois tipos diferentes: aqueles contendo código WordPress e os outros contendo lógica de negócios (ou seja, sem código WordPress).

Finalmente, adicionamos todos os pacotes como dependências no aplicativo, e os instalamos através do Composer. Como as ferramentas serão aplicadas aos pacotes de código de negócios, estes devem conter a maior parte do código do aplicativo; quanto maior a porcentagem, melhor. Fazer com que eles gerenciem cerca de 90% do código geral é um bom objetivo.

Extraindo o código WordPress para dentro dos pacotes

Seguindo o exemplo anterior, os contratos PostAPIInterface e PostInterface serão adicionados ao pacote contendo o código de negócios, e outro pacote incluirá a implementação desses contratos no WordPress. Para satisfazer a PostInterface, criamos uma classe PostWrapper que irá recuperar todos os atributos de um objeto WP_Post:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostInterface;
use WP_Post;

class PostWrapper implements PostInterface
{
  private WP_Post $post;
  
  public function __construct(WP_Post $post)
  {
    $this->post = $post;
  }

  public function get_ID(): int
  {
    return $this->post->ID;
  }

  public function get_post_author(): string
  {
    return $this->post->post_author;
  }

  public function get_post_date(): string
  {
    return $this->post->post_date;
  }

  // ...
}

Ao implementar PostAPI, uma vez que o método get_posts retorna PostInterface[], devemos converter objetos de WP_Post para PostWrapper:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;

class PostAPI implements PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Convert WP_Post[] to PostWrapper[]
    return array_map(
      function (WP_Post|int $post) {
        if ($post instanceof WP_Post) {
          return new PostWrapper($post);
        }
        return $post
      },
      $wpPosts
    );
  }
}

Usando a injeção de dependência

A injeção de dependência é um padrão de design que permite a colagem de todas as peças do aplicativo de uma forma solta. Com a injeção de dependência, o aplicativo acessa serviços através de seus contratos, e as implementações de contrato são “injetadas” no aplicativo através da configuração.

Simplesmente mudando a configuração, podemos mudar facilmente de um fornecedor de contrato para outro. Existem várias bibliotecas de injeção de dependência que podemos escolher. Aconselhamos escolher uma que siga as Recomendações Padrão do PHP (frequentemente referidas como “PSR”), para que possamos facilmente substituir a biblioteca por outra, se a necessidade surgir. Em relação à injeção de dependência, a biblioteca deve satisfazer a PSR-11, que fornece a especificação para uma “interface de recipiente”. Entre outras, as seguintes bibliotecas estão em conformidade com o PSR-11:

Acesso aos serviços através do contentor de Serviços

A biblioteca de injeção de dependência disponibilizará um “recipiente de serviço”, que resolve um contrato em sua classe de implementação correspondente. O aplicativo deve contar com o contentor de serviço para aceder a todas as funcionalidades. Por exemplo, enquanto nós normalmente invocaríamos diretamente as funções do WordPress:

$posts = get_posts();

… com o contentor de serviço, devemos primeiro obter o serviço que satisfaz o  PostAPIInetrface e executar a funcionalidade através dele:

use Owner\MyApp\Contracts\PostAPIInterface;

// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();

// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);

// Now we can invoke the WordPress functionality
$posts = $postAPI->get_posts();

Usando a injeção de dependência da Symfony

O componente Symfony’sDependency Injection é atualmente a mais popular biblioteca de injeção de dependência. Ele permite que você configure o container de serviço via PHP, YAML, ou código XML. Por exemplo, para definir que o contrato PostAPIInterface é satisfeito através da classe PostAPI é configurado no YAML desta forma:

services:
  Owner\MyApp\Contracts\PostAPIInterface:
    class: \Owner\MyAppForWP\ContractImplementations\PostAPI

A Symfony DependencyInjection também permite que instâncias de um serviço sejam automaticamente injetadas (ou “autowired”) em qualquer outro serviço que dependa dele. Além disso, torna fácil definir que uma classe é uma implementação de seu próprio serviço. Por exemplo, considere a seguinte configuração de YAML:

services:
  _defaults:
    public: true
    autowire: true

  GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
    class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'

  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
    class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
    
  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
    resource: '../src/Security/UserAuthorizationSchemes/*'

Esta configuração define o seguinte:

  • Contrato UserAuthorizationSchemeRegistryInterface é satisfeito através da classe  UserAuthorizationSchemeRegistry
  • Contrato UserAuthorizationInterface é satisfeita através da classe UserAuthorization
  • Todas as classes sob a pasta UserAuthorizationSchemes/ são uma implementação de si mesmas
  • Os serviços devem ser injectados automaticamente uns nos outros (autowire: true)

Vamos ver como funciona a cablagem automática. A classe UserAuthorization depende do serviço com contrato UserAuthorizationSchemeRegistryInterface:

class UserAuthorization implements UserAuthorizationInterface
{
  public function __construct(
      protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
  ) {
  }

  // ...
}

Graças ao autowire: true, o componente DependencyInjection terá automaticamente o serviço UserAuthorization receberá sua dependência requerida, que é uma instância do UserAuthorizationSchemeRegistry.

Quando abstrair

Abstrair o código pode consumir tempo e esforço consideráveis, por isso só devemos assumi-lo quando os seus benefícios superarem os seus custos. As sugestões a seguir são sugestões de quando a abstração do código pode valer a pena. Você pode fazer isso usando trechos de código neste artigo ou os plugins WordPress abstract sugeridos abaixo.

Obtendo acesso a ferramentas

Como mencionado anteriormente, rodar o PHP-Scoperno WordPress é difícil. Desacoplando o código do WordPress em pacotes distintos, torna-se exequível escanear um plugin do WordPress diretamente.

Redução de tempo e custo de ferramentas

Executar um conjunto de testes PHPUnit leva mais tempo quando ele precisa inicializar e executar o WordPress do que quando não o faz. Menos tempo também pode se traduzir em menos dinheiro gasto rodando os testes – por exemplo, o GitHub Actions cobra pelos corredores hospedados no GitHub com base no tempo gasto usando-os.

Uma grande refatoração não é necessária

Um projeto existente pode requerer uma refatoração pesada para introduzir a arquitetura necessária (injeção de dependência, divisão do código em pacotes, etc.), dificultando a retirada. Abstrair o código ao criar um projeto do zero o torna muito mais gerenciável.

Código de produção para múltiplas plataformas

Ao extrair 90% do código para um pacote CMS-agnóstico, podemos produzir uma versão de biblioteca que funciona para um CMS ou framework diferente, substituindo apenas 10% da base de código global.

Migrando para uma plataforma diferente

Se precisarmos migrar um projeto de Drupal para WordPress, WordPress para Laravel, ou qualquer outra combinação, então apenas 10% do código deve ser reescrito – uma economia significativa.

Melhores práticas

Enquanto desenhamos os contratos para abstrair nosso código, há várias melhorias que podemos aplicar à base de código.

Aderir ao PSR-12

Ao definir a interface para acessar os métodos do WordPress, devemos aderir ao PSR-12. Esta especificação recente visa reduzir o atrito cognitivo ao digitalizar código de diferentes autores. Aderir à PSR-12 implica renomear as funções do WordPress.

O WordPress funciona com o uso de snake_case, enquanto o PSR-12 usa o camelCase. Portanto, a função get_posts se tornará getPosts:

interface PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[];
}

…e:

class PostAPI implements PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Rest of the code
    // ...
  }
}

Métodos de divisão

Os métodos na interface não precisam ser uma réplica do WordPress. Podemos transformá-los sempre que isso fizer sentido. Por exemplo, a função do WordPress get_user_by($field, $value) sabe como recuperar o usuário do banco de dados através do parâmetro $field, que aceita os valores "id", "ID", "slug“, "email" ou "login“. Este design tem alguns problemas:

  • Não falhará na hora da compilação se passarmos uma corda errada.
  • O parâmetro $value precisa aceitar todos os tipos diferentes para todas as opções, mesmo que ao passar “ID" ele espera uma int, ao passar "e-mail" ele só pode receber uma string

Podemos melhorar esta situação, dividindo a função em várias:

namespace Owner\MyApp\Contracts;

interface UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface;
  public function getUserByEmail(string $email): ?UserInterface;
  public function getUserBySlug(string $slug): ?UserInterface;
  public function getUserByLogin(string $login): ?UserInterface;
}

O contrato é resolvido para o WordPress desta forma (assumindo que criamos o UserWrappe e a UserInterface, como explicado anteriormente):

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\UserAPIInterface;

class UserAPI implements UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface
  {
    return $this->getUserByProp('id', $id);
  }

  public function getUserByEmail(string $email): ?UserInterface
  {
    return $this->getUserByProp('email', $email);
  }

  public function getUserBySlug(string $slug): ?UserInterface
  {
    return $this->getUserByProp('slug', $slug);
  }

  public function getUserByLogin(string $login): ?UserInterface
  {
    return $this->getUserByProp('login', $login);
  }

  private function getUserByProp(string $prop, int|string $value): ?UserInterface
  {
    if ($user = \get_user_by($prop, $value)) {
      return new UserWrapper($user);
    }
    return null;
  }
}

Remover detalhes de implementação da assinatura da função

As funções no WordPress podem fornecer informações sobre como elas são implementadas em sua própria assinatura. Esta informação pode ser removida ao avaliar a função a partir de uma perspectiva abstrata. Por exemplo, a obtenção do sobrenome do usuário no WordPress é feita chamando get_the_author_meta, tornando explícito que o sobrenome de um usuário é armazenado como um valor “meta” (na tabela wp_usermeta):

$userLastname = get_the_author_meta("user_lastname", $user_id);

Você não tem que transmitir esta informação ao contrato. As interfaces só se preocupam com o quê, não com o como. Portanto, o contrato pode ter um método getUserLastname, que não fornece qualquer informação sobre como é implementado:

interface UserAPIInterface
{
  public function getUserLastname(UserWrapper $userWrapper): string;
  ...
}

Adicionar tipos mais estritos

Algumas funções do WordPress podem receber parâmetros de diferentes maneiras, levando à ambigüidade. Por exemplo, a função add_query_arg pode tanto receber uma única chave e valor:

$url = add_query_arg('id', 5, $url);

… ou um array de key => value:

$url = add_query_arg(['id' => 5], $url);

Nossa interface pode definir uma intenção mais compreensível, dividindo tais funções em várias separadas, cada uma delas aceitando uma combinação única de entradas:

public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $keyValues, string $url);

Eliminação da dívida técnica

A função get_post do WordPress retorna não só “artigos” mas também “páginas” ou qualquer entidade do tipo “artigoss personalizados”, e essas entidades não são intercambiáveis. Tanto artigos como páginas são artigos personalizados, mas uma página não é um artigo e não é uma página. Portanto, a execução de get_posts pode retornar páginas. Este comportamento é uma discrepância conceitual.

Para fazer isso corretamente, get_posts deve ser chamado de get_customposts, mas nunca foi renomeado no núcleo do WordPress. É um problema comum na maioria dos softwares de longa duração e é chamado de “dívida técnica” – código que tem problemas, mas nunca é corrigido porque introduz mudanças de quebra.

Ao criar os nossos contratos, no entanto, temos a oportunidade de evitar este tipo de dívida técnica. Neste caso, podemos criar uma nova interface ModelAPIInterface que pode lidar com entidades de diferentes tipos, e fazemos vários métodos, cada um para lidar com um tipo diferente:

interface ModelAPIInterface
{
  public function getPosts(array $args): array;
  public function getPages(array $args): array;
  public function getCustomPosts(array $args): array;
}

Desta forma, a discrepância não ocorrerá mais, e você verá estes resultados:

  • getPosts retorna apenas mensagens
  • getPages devolve apenas páginas
  • getCustomPosts devolve tanto artigos como páginas

Benefícios de abstrair o código

As principais vantagens de abstrair o código de um aplicativo são:

  • A execução de ferramentas em pacotes contendo apenas código de negócios é mais fácil de configurar e levará menos tempo (e menos dinheiro) para ser executada.
  • Podemos usar ferramentas que não funcionam com o WordPress, como o scoping de um plugin com o PHP-Scoper.
  • Você pode acabar produzindo dezenas de pacotes que depois têm de ser gerenciados e mantidos.
  • A migração de um aplicativo para outras plataformas torna-se mais fácil.
  • Podemos mudar a nossa mentalidade do pensamento no WordPress para pensar em termos da nossa lógica empresarial.
  • Os contratos descrevem a intenção do pedido, tornando-o mais compreensível.
  • O aplicativo é organizado através de pacotes, criando um aplicativo enxuto contendo o mínimo necessário e melhorando-o progressivamente conforme a necessidade.
  • Nós podemos saldar a dívida técnica.

Questões com o código de abstração

As desvantagens de abstrair o código de um aplicativo são:

  • Inicialmente envolve uma quantidade considerável de trabalho.
  • O código torna-se mais verboso; adiciona camadas extras de código para alcançar o mesmo resultado.
  • Você pode acabar produzindo dezenas de pacotes que devem então ser gerenciados e mantidos.
  • Você pode precisar de um monorepo para gerenciar todos os pacotes juntos.
  • A injeção de dependência pode ser exagerada para aplicativos simples (diminuindo os retornos).
  • A abstração do código nunca será totalmente realizada, uma vez que existe normalmente uma preferência geral implícita na arquitectura do CMS.

Opções de plugin de abstração WordPress

Embora seja geralmente mais sensato extrair seu código para um ambiente local antes de trabalhar nele, alguns plugins do WordPress podem ajudá-lo a atingir seus objetivos de abstração. Estas são as nossas melhores escolhas.

1. WPide

Produzido pela WebFactory Ltd, o popular plugin WPide amplia drasticamente a funcionalidade padrão do editor de código do WordPress. Ele serve como um plugin de abstração WordPress, permitindo que você visualize seu código in situ para visualizar melhor o que precisa de atenção.

O plugin WPide.
O plugin WPide.

WPide também tem uma função de busca e substituição para localizar rapidamente código desatualizado ou expirado e substituí-lo por uma rendição refatorada.

Além disso, WPide fornece muitas características extras, incluindo:

  • Sintaxe e destaque de blocos
  • Backups automáticas
  • Criação de arquivos e pastas
  • Abrangente navegador de árvore de arquivos
  • Acesso à API do sistema de arquivos WordPress

2. Ultimate DB Manager

O plugin Ultimate WP DB Manager do WPHobby dá uma forma rápida de baixar suas bases de dados na totalidade para extração e refatoração.

O plug-in do Ultimate DB Manager.
O plug-in do Ultimate DB Manager.

Naturalmente, plugins deste tipo não são necessários para os usuários Kinsta, pois Kinsta oferece acesso direto à base de dados a todos os clientes. No entanto, se você não tiver acesso suficiente à base de dados através do seu provedor de hospedagem, o Ultimate DB Manager pode vir a ser útil como um plugin de abstração WordPress.

3. Seu próprio plugin personalizado de Abstração WordPress

No final, a melhor escolha para abstração será sempre criar o seu plugin. Pode parecer um grande empreendimento, mas se você tem habilidade limitada para gerenciar seus arquivos principais do WordPress diretamente, isso oferece uma solução de abstração amigável.

Fazê-lo tem benefícios claros:

  • Abstrai as suas funções dos seus arquivos de temas
  • Preserva o seu código através de alterações de temas e atualizações da bases de dados

Você pode aprender como criar seu plugin de abstração WordPress através do Plugin Developer Handbook do WordPress.

Resumo

Devemos abstrair o código dos nossos aplicativos? As vezes, não existe uma “resposta certa ou predefinida”, porque depende de cada projeto. Os projetos que requerem um tempo tremendo para analisar com PHPUnit ou PHPStan podem ser os que mais se beneficiam, mas o esforço necessário para conseguir isso pode nem sempre valer a pena.

Você aprendeu tudo o que precisa saber para começar a abstrair o código WordPress.

Você planeja implementar esta estratégia em seu projeto? Em caso afirmativo, você vai usar um plugin de abstração WordPress? Informe-nos na secção de 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.