Parte 8 – WordPress e Programação Orientada a Objetos: Um Exemplo WordPress – Implementação: Opções

Publicados: 2022-02-04

Até agora só precisávamos armazenar opções definidas pelo usuário, então utilizamos a API de Configurações. No entanto, nosso plugin deve ser capaz de ler/gravar opções para “lembrar” quantas vezes um endereço IP tentou fazer login sem sucesso, se está bloqueado no momento, etc.

Precisamos de uma maneira orientada a objetos para armazenar e recuperar opções. Durante a fase de “Design”, discutimos isso brevemente, mas abstraímos alguns dos detalhes da implementação, focando apenas nas ações que gostaríamos de poder realizar — obter , configurar e remover uma opção.

Também agruparemos as opções com base em sua seção para mantê-las organizadas. Isso é puramente baseado na preferência pessoal.

Vamos transformar isso em uma interface:

 interface Options { /** * Return the option value based on the given option name. * * @param string $name Option name. * @return mixed */ public function get( $name ); /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section ID. * @return bool Whether the option was added. */ public function set( $name, $value, $section_id ); /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section ID. */ public function remove( $name, $section_id ); }

Idealmente, poderíamos interagir com a API de opções do WordPress, fazendo algo assim:

 $options = new WP_Options(); $options->get( 'retries' );

Neste ponto, você deve estar se perguntando por que não usamos apenas a get_option() do WordPress, em vez de nos darmos ao trabalho de criar nossa própria interface e classe. Embora usar as funções do WordPress diretamente seja uma maneira perfeitamente aceitável de desenvolver nosso plug-in, dando um passo adiante e criando uma interface para depender, permanecemos flexíveis.

Nossa classe WP_Options vai implementar nossa interface Options . Dessa forma, estaremos prontos se nossas necessidades mudarem no futuro. Por exemplo, podemos precisar armazenar nossas opções em uma tabela personalizada, em um banco de dados externo, na memória (por exemplo, Redis), você escolhe. Ao depender de uma abstração (ou seja, interface), alterar algo na implementação é tão simples quanto criar uma nova classe implementando a mesma interface.

WP_Options

Vamos começar a escrever nossa classe WP_Options , recuperando todas as opções usando a get_option() do WordPress em seu construtor.

 class WP_Options { /** * @var array Stored options. */ private $options; /** * WP_Options constructor. */ public function __construct() { $this->options = get_option( Plugin::PREFIX ); } }

Como a propriedade $options será usada internamente, vamos declará-la private para que possa ser acessada apenas pela classe que a definiu, a classe WP_Options .

Agora, vamos implementar nossa interface Options usando o operador implements .

 class WP_Options implements Options { // ...

Nosso IDE está gritando conosco para declarar nossa classe abstrata ou implementar os métodos get() , set() e remove() , definidos na interface.

Então, vamos começar a implementar esses métodos!

Obtendo uma opção

Começaremos com o método get() , que procurará o nome da opção especificada em nossa propriedade $options e retornará seu valor ou false se não existir.

 class WP_Options implements Options { private $options; public function __construct() { $this->options = get_option( Plugin::PREFIX ); } /** * Return the option value based on the given option name. * * @return mixed */ public function get( $option_name ) { if ( ! isset( $this->options[ $option_name ] ) ) { return false; } return $this->options[ $option_name ]; } }

Agora é um bom momento para pensar nas opções padrão.

Opções padrão

Conforme mencionado anteriormente, gostaríamos de agrupar as opções, com base em sua seção. Então, provavelmente dividiremos as opções em algumas seções. A seção “Opções Gerais” e outra para os dados que precisamos acompanhar. Bloqueios, novas tentativas, logs de bloqueio e número total de bloqueios — chamaremos esse estado arbitrariamente.

Usaremos uma constante para armazenar nossas opções padrão. O valor de uma constante não pode ser alterado enquanto nosso código está em execução, o que o torna ideal para algo como nossas opções padrão. As constantes de classe são alocadas uma vez por classe e não para cada instância de classe.

NOTA: O nome de uma constante está em letras maiúsculas por convenção.

 const DEFAULT_OPTIONS = array( 'general_options' => array( 'allowed_retries' => 4, 'normal_lockout_time' => 1200, // 20 minutes 'max_lockouts' => 4, 'long_lockout_time' => 86400, // 24 hours 'hours_until_retries_reset' => 43200, // 12 hours 'site_connection' => 'direct', 'handle_cookie_login' => 'yes', 'notify_on_lockout_log_ip' => true, 'notify_on_lockout_email_to_admin' => false, 'notify_after_lockouts' => 4 ), 'state' => array( 'lockouts' => array(), 'retries' => array(), 'lockout_logs' => array(), 'total_lockouts' => 0 ) );

Na matriz aninhada DEFAULT_OPTIONS , definimos um valor padrão para todas as nossas opções.

O que gostaríamos de fazer a seguir é armazenar os valores de opção padrão no banco de dados assim que o plug-in for inicializado, usando a add_option() do WordPress.

 class WP_Options { public function __construct() { $all_options = array(); foreach ( self::DEFAULT_OPTIONS as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; } $all_options = array_merge( $all_options, $section_options ); } $this->options = $all_options; } }

Vamos dar uma olhada mais de perto neste trecho. Primeiro, iteramos o array de opções padrão e recuperamos as opções usando a get_option() do WordPress.

 foreach ( self::default_options as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); // ...

Em seguida, verificamos se cada opção já existe no banco de dados e, caso não exista, armazenamos sua opção padrão.

 if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; }

Por fim, coletamos as opções de todas as seções.

 $all_options = array_merge( $all_options, $section_options );

E armazene-os na propriedade $options para que possamos acessá-los mais tarde.

 $this->options = $all_options;

A tabela de opções do WordPress no banco de dados terá algumas linhas, onde o option_name consiste no prefixo do plug-in concatenado ao nome da seção.

Vamos passar agora para o resto dos métodos que precisamos implementar.

Armazenando uma opção

Da mesma forma, gostaríamos de armazenar facilmente uma nova opção no banco de dados e substituir qualquer valor anterior, assim:

 $options = new Options(); $options->set( 'retries', 4 );

Então, vamos implementar o método set() , que usará a função update_option() do WordPress.

 /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was added. */ public function set( $name, $value, $section_ ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $stored_option = get_option( $db_option_name ); $stored_option[ $name ] = $value; return update_option( $db_option_name, $stored_option ); }

Removendo uma opção

Por fim, implementaremos o método remove() , que definirá a opção para seu valor inicial:

 /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was removed. */ public function remove( $name, $section_ ) { $initial_value = array(); if ( isset( self::DEFAULT_OPTIONS[ $section_id ][ $name ] ) ) { $initial_value = self::DEFAULT_OPTIONS[ $section_id ][ $name ]; } return $this->set( $name, $initial_value, $section_id ); }

Reunimos tudo em uma única classe. Todos os dados relacionados a opções (ou seja, nossas propriedades) e os detalhes de implementação (ou seja, os métodos que acabamos de implementar) são encapsulados na classe WP_Options .

Encapsulamento/Abstração

Envolver tudo em uma única classe, encerrar os internos (como se estivesse em uma cápsula), essencialmente “escondê-los” do mundo exterior, é o que chamamos de encapsulamento . O encapsulamento é outro conceito central da programação orientada a objetos.

Hospede seu site com a Pressidium

GARANTIA DE DEVOLUÇÃO DO DINHEIRO DE 60 DIAS

VEJA NOSSOS PLANOS

Usando a interface Options , focamos no que fazemos com nossas opções em vez de como fazemos, abstraindo a ideia de opções, simplificando as coisas conceitualmente. Isso é o que chamamos de abstração , outro conceito central da programação orientada a objetos.

Encapsulamento e abstração são conceitos completamente diferentes , mas claramente, como você pode ver, altamente relacionados. Sua principal diferença é que o encapsulamento existe no nível de implementação, enquanto a abstração existe no nível de design.

Dependências

Vamos considerar o seguinte cenário:

Existe uma classe Lockouts , responsável por determinar se um endereço IP deve ser bloqueado, qual deve ser a duração desse bloqueio, se um bloqueio ativo ainda é válido ou expirou etc. Essa classe contém um método should_get_locked_out() , responsável por determinar se um endereço IP deve ser bloqueado. Esse método precisaria ler o número máximo de tentativas permitidas antes que um endereço IP seja bloqueado, que é um valor configurável, o que significa que é armazenado como uma opção .

Portanto, o código que acabamos de descrever seria semelhante a este:

 class Lockouts { // ... /** * @var WP_Options An instance of `WP_Options`. */ private $options; /** * Lockouts constructor */ public function __construct() { $this->options = new WP_Options(); } /** * Return the number of retries. * * @return int */ private function get_number_of_retries() { // ... } /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; } // ... }

Basicamente, estamos criando uma nova instância de WP_Options no construtor e, em seguida, usamos essa instância para recuperar o valor da opção allowed_retries .

Isso é absolutamente bom, mas temos que ter em mente que nossa classe Lockouts agora depende de WP_Options . Chamamos WP_Options de dependência .

Se nossas necessidades mudarem no futuro, por exemplo, precisarmos ler/gravar opções em um banco de dados externo, precisaremos substituir WP_Options por uma classe DB_Options . Isso não parece tão ruim, se precisarmos recuperar opções em apenas uma classe. No entanto, pode ficar um pouco complicado quando há muitas classes com várias dependências. Quaisquer alterações em uma única dependência provavelmente se espalharão pela base de código, forçando-nos a modificar uma classe se uma de suas dependências for alterada.

Podemos eliminar esse problema reescrevendo nosso código para seguir o Princípio de Inversão de Dependência .

Dissociação

O Princípio de Inversão de Dependência (DIP), o “D” em SOLID, afirma:

  • Módulos de alto nível não devem importar nada de módulos de baixo nível. Ambos devem depender de abstrações.
  • As abstrações não devem depender de detalhes. Detalhes (implementações concretas) devem depender de abstrações.

No nosso caso, a classe Lockouts é o “módulo de alto nível” e depende de um “módulo de baixo nível”, a classe WP_Options .

Vamos mudar isso, usando Dependency Injection , que é mais fácil do que parece. Nossa classe Lockouts receberá os objetos dos quais depende, em vez de criá-los.

 class Lockouts { // ... /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; } // ... }

Então, injetamos uma dependência:

 $options = new WP_Options(); $lockouts = new Lockouts( $options );

Acabamos de tornar nossa classe Lockouts mais fácil de manter, já que agora ela está fracamente acoplada à sua dependência WP_Options . Além disso, poderemos simular as dependências, tornando nosso código mais fácil de testar. Substituir o WP_Options por um objeto que imite seu comportamento nos permitirá testar nosso código sem realmente executar nenhuma consulta em um banco de dados.

 /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; }

Mesmo que tenhamos dado o controle das dependências de Lockouts para outra classe (ao contrário de Lockouts controlando as dependências em si), Lockouts ainda espera um objeto WP_Options . Ou seja, ainda depende da classe WP_Options concreta, em vez de uma abstração. Como mencionado anteriormente, ambos os módulos devem depender de abstrações .

Vamos consertar isso!

 /** * Lockouts constructor. * * @param Options $options */ public function __construct( Options $options ) { $this->options = $options; }

E simplesmente alterando o tipo do argumento $options da classe WP_Options para a interface Options , nossa classe Lockouts depende de uma abstração e estamos livres para passar um objeto DB_Options , ou uma instância de qualquer classe que implemente a mesma interface, ao seu construtor.

Responsabilidade única

Vale a pena notar que usamos um método chamado should_get_locked_out() para verificar se o endereço IP deve ser bloqueado ou não.

 /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; }

Poderíamos facilmente escrever um one-liner como este:

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {

No entanto, mover esse pedaço de lógica para seu próprio pequeno método tem muitos benefícios.

  • Se a condição para determinar se um endereço IP deve ser bloqueado mudar, só teremos que modificar esse método (em vez de procurar todas as ocorrências de nossa instrução if)
  • Escrever testes de unidade se torna mais fácil quando cada “unidade” é menor
  • Melhora muito a legibilidade do nosso código

Lendo isso:

 if ( $this->should_get_locked_out() ) { // ...

parece-nos muito mais fácil do que ler isso:

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) { // ...

Fizemos isso para praticamente todos os métodos do nosso plugin. Extraindo métodos de métodos mais longos até que não haja mais nada para extrair. O mesmo vale para as classes, cada classe e método deve ter uma única responsabilidade.

O Princípio da Responsabilidade Única (SRP) , o “S” em SOLID, afirma:

“Cada módulo, classe ou função em um programa de computador deve ter responsabilidade sobre uma única parte da funcionalidade desse programa e deve encapsular essa parte.”

Ou, como Robert C. Martin (“Tio Bob”) diz:

“Uma classe deve ter um, e apenas um, motivo para mudar.”

Revisitando o arquivo de plugin principal

No momento, nosso arquivo de plugin principal contém apenas isso:

 /** * Plugin Name: PRSDM Limit Login Attempts * Plugin URI: https://pressidium.com * Description: Limit rate of login attempts, including by way of cookies, for each IP. * Author: Pressidium * Author URI: https://pressidium.com * Text Domain: prsdm-limit-login-attempts * License: GPL-2.0+ * Version: 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; }

Mais uma vez, agruparemos tudo em uma classe Plugin, desta vez apenas para evitar colisões de nomes.

 namespace Pressidium\Limit_Login_Attempts; if ( ! defined( 'ABSPATH' ) ) { exit; } class Plugin { /** * Plugin constructor. */ public function __construct() { // ... } }

Iremos instanciar esta classe Plugin no final do arquivo, que irá executar o código em seu construtor.

 new Plugin();

No construtor, vamos ligar para a ação plugins_loaded, que é acionada quando os plugins ativados são carregados.

 public function __construct() { add_action( 'plugins_loaded', array( $this, 'init' ) ); } public function init() { // Initialization }

Também chamaremos um método require_files() para carregar todos os nossos arquivos PHP.

 public function __construct() { $this->require_files(); add_action( 'plugins_loaded', array( $this, 'init' ) ); } private function require_files() { require_once __DIR__ . '/includes/Sections/Section.php'; require_once __DIR__ . '/includes/Pages/Admin_Page.php'; require_once __DIR__ . '/includes/Pages/Settings_Page.php'; // ... }

Finalmente, inicializaremos nosso plugin criando alguns objetos em nosso método init() .

NOTA: O trecho a seguir contém apenas uma pequena parte do arquivo principal do plug-in. Você pode ler o arquivo real no repositório GitHub do plug-in.

 public function init() { $options = new Options(); $hooks_manager = new Hooks_Manager(); $settings_page = new Settings_Page( $options ); $hooks_manager->register( $settings_page ); // ... }

Organizando os arquivos

Manter seus arquivos organizados é vital, especialmente ao trabalhar em grandes plugins com muito código. Sua estrutura de pastas deve agrupar arquivos semelhantes, ajudando você e seus colegas de equipe a se manterem organizados.

Já definimos um namespace ( Pressidium\Limit_Login_Attempts ), contendo vários sub-namespaces para Pages , Sections , Fields , Elements , etc. Seguindo essa hierarquia para organizar nossos diretórios e arquivos, acabamos com uma estrutura semelhante a esta:

 . ├── includes │ ├── Hooks │ │ ├── Actions.php │ │ ├── Filters.php │ │ └── Hooks_Manager.php │ ├── Pages │ │ ├── Admin_Page.php │ │ └── Settings_Page.php │ ├── Sections │ │ ├── Fields │ │ │ ├── Elements │ │ │ │ ├── Checkbox_Element.php │ │ │ │ ├── Custom_Element.php │ │ │ │ ├── Element.php │ │ │ │ ├── Number_Element.php │ │ │ │ └── Radio_Element.php │ │ │ └── Field.php │ │ └── Section.php │ └── WP_Options.php ├── prsdm-limit-login-attempts.php └── uninstall.php

Cada arquivo contém uma única classe. Os arquivos são nomeados de acordo com as classes que eles contêm, e os diretórios e subdiretórios são nomeados de acordo com os (sub) namespaces.

Existem vários padrões de arquitetura e esquemas de nomenclatura que você pode usar. Cabe a você escolher um que faça sentido para você e se adapte às necessidades do seu projeto. Na hora de estruturar seu projeto, o importante é ser consistente .

Conclusão

Parabéns! Você concluiu nossa série de artigos sobre WordPress e programação orientada a objetos.

Espero que você tenha aprendido algumas coisas e esteja animado para começar a aplicar o que aprendeu em seus próprios projetos!

Aqui está uma rápida recapitulação do que abordamos nesta série:

  • Levantamento de requisitos: Decidimos o que o plugin deve fazer.
  • Design: Pensamos em como o plugin será estruturado, os relacionamentos entre nossas classes potenciais e uma visão geral de alto nível de nossas abstrações.
  • Implementação: Escrevemos o código real de algumas partes-chave do plugin. Ao fazer isso, apresentamos vários conceitos e princípios.

No entanto, mal arranhamos a superfície do que OOP é e tem a oferecer. Ficar bom em uma nova habilidade requer prática, então vá em frente e comece a construir seus próprios plugins WordPress orientados a objetos. Boa codificação!

Veja também

  • WordPress e programação orientada a objetos – uma visão geral
  • Parte 2 – WordPress e Programação Orientada a Objetos: Um Exemplo do Mundo Real
  • Parte 3 – WordPress e Programação Orientada a Objetos: Α Exemplo WordPress – Definindo o Escopo
  • Parte 4 – WordPress e Programação Orientada a Objetos: Um Exemplo WordPress – Design
  • Parte 5 – WordPress e Programação Orientada a Objetos: Um Exemplo WordPress – Implementação: O Menu de Administração
  • Parte 6 – WordPress e Programação Orientada a Objetos: Um Exemplo WordPress – Implementação: Registrando as Seções
  • Parte 7 – WordPress e Programação Orientada a Objetos: Um Exemplo WordPress – Implementação: Gerenciando Ganchos do WordPress