Parte 6 – WordPress e Programação Orientada a Objetos: Um Exemplo WordPress – Implementação: Registrando as Seções

Publicados: 2022-02-04

Bem-vindo de volta à nossa série sobre Programação Orientada a Objetos.

Conforme explicamos na parte de Design da série, uma página de administração consiste em seções . Cada seção contém um ou mais campos e cada um desses campos contém um ou mais elementos .

Como isso ficaria no código?

 public function register_sections() { $my_section = $this->register_section( /* ... */ ); $my_field = $my_section->add_field( /* ... */ ); $my_element = $my_field->add_element( /* ... */ ); }

Tudo bem, isso parece fácil de usar e já podemos dizer que provavelmente precisaremos criar três novas classes: Section , Field e Element .

 class Section {}
 class Field {}
 class Element {}

Vamos parar um momento e nos perguntar o que sabemos até agora sobre essas aulas.

  • $my_section->add_field() → A classe Section deve ser capaz de adicionar (e armazenar) um novo objeto Field
  • $my_field->add_element() → A classe Field deve ser capaz de adicionar (e armazenar) um novo objeto Element .

Começamos armazenando nossos objetos Field em um array, como normalmente faríamos:

 class Section { /** * @var Field[] Section field objects. */ protected $fields = array();

Essa variável $fields é um membro de classe e é o que chamamos de propriedade . Propriedades são variáveis ​​PHP, vivendo em uma classe, e podem ser de qualquer tipo de dado ( string , integer , object , etc.).

Também escreveremos o método add_field() para criar e adicionar um novo campo.

 public function add_field() { $field = new Field( /* ... */ ); $this->fields[] = $field; return $field; }

Esse método cria um novo objeto Field , adiciona-o à propriedade fields e retorna esse objeto recém-criado. Bem direto.

Vamos repetir o mesmo processo para a classe Field também.

 class Field { /** * @var Element[] Field elements. */ private $elements = array(); /** * Create a new element object. * * @return Element */ private function create_element() { return new Element( /* ... */ ); } /** * Add a new element object to this field. */ public function add_element() { $element = $this->create_element(); $this->elements[] = $element; } }

Isso é um começo! Qual é o próximo?

A Classe da Seção

Precisamos chamar add_settings_section(), quando uma nova seção é criada. Mais uma vez, o método construtor é uma ótima maneira de realizar nossa inicialização. Vamos adicionar na classe:

 class Section { // ... public function __construct() { add_settings_section( $this->id, $this->title, array( $this, 'print_description' ), $this->page ); } }

Parece que uma Seção precisa de um nome de slug para identificá-la (usado no atributo id das tags). Também pode ter um título, uma descrição e pertencer a uma página específica.

 class Section { /** * @var Field[] Section field objects. */ protected $fields = array(); /** * @var string Section title. */ public $title; /** * @var string Section id. */ public $id; /** * @var string Slug-name of the settings page this section belongs to. */ public $page; /** * @var string Section description. */ public $description;

Poderíamos definir o título da seção, fazendo algo assim:

 $section = new Section(); $section->title = __( 'Hello world', 'prsdm-limit-login-attempts' );

Bem, isso não está certo. Mesmo que o código acima seja perfeitamente válido, na verdade ele não faz o que esperamos que faça.

O método construtor é executado quando um novo objeto Section é criado. Então add_settings_section() será chamado antes mesmo de termos a chance de definir o título. Como resultado, a seção não terá um título.

Hospede seu site com a Pressidium

GARANTIA DE DEVOLUÇÃO DO DINHEIRO DE 60 DIAS

VEJA NOSSOS PLANOS

O título precisa estar disponível durante a inicialização do nosso objeto, então precisamos fazer isso no construtor.

 class Section { /** * @var string Section title. */ private $title; public function __construct( $title ) { $this->title = $title; // ... } // ..

Observe que $this->title se refere à propriedade da classe title, onde $title se refere ao argumento do construtor.

Aqui também aproveitamos a visibilidade . Como nossa propriedade $title só será acessada pela classe que a definiu, podemos declará-la private . Portanto, evitamos que ele seja acessado fora da classe.

Ah, e também temos que adicionar um método print_description() que vai, bem, imprimir a descrição da seção.

 /** * Print the section description. */ public function print_description() { echo esc_html( $this->description ); }

Juntando tudo, nossa classe Section fica assim.

 class Section { /** * @var Field[] Section field objects. */ protected $fields = array(); /** * @var string Section title. */ private $title; /** * @var string Section id. */ private $id; /** * @var string Slug-name of the settings page this section belongs to. */ private $page; /** * @var string Section description. */ private $description; /** * Section constructor. * * @param string $id Section id. * @param string $title Section title. * @param string $page Slug-name of the settings page. * @param string $description Section description. */ public function __construct( $id, $title, $page, $description ) { $this->id = $id; $this->title = $title; $this->page = $page; $this->description = $description; add_settings_section( $this->id, $this->title, array( $this, 'print_description' ), $this->page ); } /** * Print the section description. */ public function print_description() { echo esc_html( $this->description ); } /** * Create and add a new field object to this section. */ public function add_field() { $field = new Field( /* ... */ ); $this->fields[] = $field; return $field; } }

A classe de campo

De maneira semelhante à Section , agora podemos prosseguir e construir a classe Field , que utilizará a add_settings_field() do WordPress.

 class Field { /** * @var Element[] Field elements. */ private $elements = array(); /** * @var string ID of the section this field belongs to. */ private $section_id; /** * @var string Field description. */ private $description; /** * Field constructor. * * @param string $id Field ID. * @param string $label Field label. * @param string $page Slug-name of the settings page. * @param string $section_id ID of the section this field belongs to. * @param string $description Field description. */ public function __construct( $id, $label, $page, $section_id, $description ) { $this->section_id = $section_id; $this->description = $description; add_settings_field( $id, $label, array( $this, 'render' ), $page, $this->section_id ); } }

Aqui, também gostaríamos de fornecer valores padrão para o ID, rótulo e descrição do campo. Podemos fazer isso passando um array de opções para o construtor e usando a função wp_parse_args() do WordPress para analisar essas opções.

 class Field { /** * @var int Number of fields instantiated. */ private static $number_of_fields = 0; // ... /** * Field constructor. * * @param string $section_id ID of the section this field belongs to. * @param string $page Slug-name of the settings page. * @param array $options Options. */ public function __construct( $section_id, $page, $options = array() ) { self::$number_of_fields++; $options = wp_parse_args( $options, array( 'label' => sprintf( __( 'Field #%s', 'prsdm-limit-login-attempts' ), self::$number_of_fields 'id' => 'field_' . self::$number_of_fields, 'description' => '' ) ); $this->section_id = $section_id; $this->description = $options['description']; add_settings_field( $options['id'], $options['label'], array( $this, 'render' ), $page, $this->section_id ); } }

A função wp_parse_args() nos permitirá mesclar os valores definidos pelo usuário (o array $options ) com os valores padrão.

 array( 'label' => sprintf( __( 'Field #%s', 'prsdm-limit-login-attempts' ), self::$number_of_fields 'id' => 'field_' . self::$number_of_fields, 'description' => '' )

Também temos que definir rótulos exclusivos para cada campo. Podemos lidar com isso definindo o rótulo como um prefixo ( 'field_' ) seguido por um número, que será aumentado toda vez que um novo objeto Field for criado. Armazenaremos esse número na propriedade estática $number_of_fields .

 /** * @var int Number of fields instantiated. */ private static $number_of_fields = 0;

Uma propriedade estática pode ser acessada diretamente sem ter que criar uma instância de uma classe primeiro.

 'id' => 'field_' . self::$number_of_fields

A palavra-chave self é usada para se referir à classe atual e, com a ajuda do operador de resolução de escopo :: (comumente chamado de “dois dois pontos”), podemos acessar nossa propriedade estática.

Dessa forma, no construtor, sempre acessamos a mesma propriedade $number_of_fields , aumentando seu valor cada vez que um objeto é criado, o que resulta em um rótulo único anexado a cada campo.

Daqui para frente, o método render() , após imprimir a descrição (se houver), itera por todos os elementos e renderiza cada um deles.

 public function render() { if ( ! empty( $this->description ) ) { printf( '<p class="description">%s</p>', esc_html( $this->description ) ); } foreach ( $this->elements as $key => $element ) { $element->render(); } }

Juntando tudo…

 class Field { /** * @var int Number of fields instantiated. */ private static $number_of_fields = 0; /** * @var Element[] Field elements. */ private $elements = array(); /** * @var string ID of the section this field belongs to. */ private $section_id; /** * @var string Field description. */ private $description; /** * Field constructor. * * @param string $section_id ID of the section this field belongs to. * @param string $page Slug-name of the settings page. * @param array $options Options. */ public function __construct( $section_id, $page, $options = array() ) { self::$number_of_fields++; $options = wp_parse_args( $options, array( 'label' => sprintf( /* translators: %s is the unique s/n of the field. */ __( 'Field #%s', 'prsdm-limit-login-attempts' ), self::$number_of_fields 'id' => 'field_' . self::$number_of_fields, 'description' => '' ) ); $this->section_id = $section_id; $this->description = $options['description']; add_settings_field( $options['id'], $options['label'], array( $this, 'render' ), $page, $this->section_id ); } /** * Create a new element object. * * @return Element */ private function create_element() { return new Element( /* ... */ ); } /** * Add a new element object to this field. */ public function add_element() { $element = $this->create_element(); $this->elements[] = $element; } /** * Render the field. */ public function render() { if ( ! empty( $this->description ) ) { printf( '<p class="description">%s</p>', esc_html( $this->description ) ); } foreach ( $this->elements as $key => $element ) { $element->render(); } } }

A classe do elemento

Daqui para frente, construiremos a classe Element de maneira semelhante!

Vamos começar a escrever a classe assim:

 class Element { /** * @var int Number of elements instantiated. */ private static $number_of_elements = 0; /** * @var string Element label. */ private $label; /** * @var string Element name. */ private $name; /** * @var mixed Element value. */ private $value; /** * Element constructor. * * @param string $section_id Section ID. * @param array $options Options. */ public function __construct( $section_id, $options = array() ) { self::$number_of_elements++; $options = wp_parse_args( $options, array( 'label' => sprintf( /* translators: %s is the unique s/n of the element. */ __( 'Element #%s', 'prsdm-limit-login-attempts' ), self::$number_of_elements ), 'name' => 'element_' . self::$number_of_elements ) ); $this->label = $options['label']; $this->name = $options['name']; $this->value = ''; } /** * Render the element. */ public function render() { ?> <fieldset> <label> <input type="number" name="<?php echo esc_attr( $this->name ); ?>" value="<?php echo esc_attr( $this->value ); ?>" /> <?php echo esc_html(); ?> </label> </fieldset> <?php } }

Certifique-se de estar escapando de sua saída – como estamos fazendo aqui, usando as funções esc_attr() e esc_html() do WordPress – para evitar ataques de script entre sites. Mesmo que estejamos renderizando nossos elementos apenas em páginas de administração, ainda é uma boa ideia sempre escapar de quaisquer dados de saída.

NOTA: O script entre sites (ou XSS) é um tipo de vulnerabilidade de segurança normalmente encontrada em aplicativos da web. O XSS permite que invasores injetem código do lado do cliente em páginas da Web visualizadas por outros usuários. Uma vulnerabilidade de script entre sites pode ser usada por invasores para contornar controles de acesso, como a política de mesma origem.

Quando estávamos reunindo os requisitos do plug-in, notamos que existem vários tipos de elementos - caixas de seleção, botões de opção, campos numéricos etc. Quando criamos nosso design, tomamos a decisão de construir uma classe Element que deveria ser estendida. Então, sabemos que vamos acabar com uma classe filha para cada tipo de elemento.

A saída deve ser diferente dependendo do tipo de elemento, então vamos transformar render() em um método abstrato. Isso significa, é claro, que a própria classe também deve ser abstrata.

 abstract class Element { /** * @var int Number of elements instantiated. */ private static $number_of_elements = 0; /** * @var string Element label. */ protected $label; /** * @var string Element name. */ protected $name; /** * @var mixed Element value. */ protected $value; /** * Element constructor. * * @param string $section_id Section ID. * @param array $options Options. */ public function __construct( $section_id, $options = array() ) { self::$number_of_elements++; $options = wp_parse_args( $options, array( 'label' => sprintf( /* translators: %s is the unique s/n of the element. */ __( 'Element #%s', 'prsdm-limit-login-attempts' ), self::$number_of_elements ), 'name' => 'element_' . self::$number_of_elements ) ); $this->label = $options['label']; $this->name = $options['name']; $this->value = ''; } /** * Render the element. */ abstract public function render(); }

Por exemplo, uma classe Number_Element ficaria assim:

 class Number_Element extends Element { /** * Render the element. */ public function render() { ?> <fieldset> <label> <input type="number" name="<?php echo esc_attr( $this->name ); ?>" value="<?php echo esc_attr( $this->value ); ?>" /> <?php echo esc_html(); ?> </label> </fieldset> <?php } }

Da mesma forma, podemos construir um Checkbox_Element , um Radio_Element e até uma classe Custom_Element para o resto de nossos elementos.

Observe que estamos construindo nossas classes para que todas possam ser usadas da mesma maneira . Chamar o método render() em qualquer filho de Element produzirá algum HTML.

Esse é um exemplo de polimorfismo , um dos conceitos centrais da programação orientada a objetos.

Polimorfismo

“Polimorfismo” significa literalmente “muitas formas” (das palavras gregas “poli” que significa “muitos” e “morphe” que significa “forma”). Uma classe filha Element pode ter muitos formulários , pois pode assumir qualquer forma de uma classe em sua hierarquia pai.

Podemos usar um Number_Element , um Checkbox_Element ou qualquer outro subtipo em qualquer lugar em que um objeto Element seja esperado, pois todos os objetos filhos podem ser usados ​​exatamente da mesma maneira (ou seja, chamando seu método render() ), enquanto ainda podem se comportar diferentemente (a saída será diferente para cada tipo de elemento).

Como você provavelmente pode dizer, polimorfismo e herança são conceitos intimamente relacionados.

Substituibilidade

O Princípio de Substituição de Liskov (ou LSP) , o “L” em SOLID, afirma:

“Em um programa de computador, se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S (ou seja, um objeto do tipo T pode ser substituído por qualquer objeto do subtipo S) sem alterar nenhum dos as propriedades desejáveis ​​do programa”.

Em termos leigos, você deve poder usar qualquer classe filha no lugar de sua classe pai sem nenhum comportamento inesperado.

Fábricas

Vamos voltar para nossa classe Field , onde atualmente temos um método create_element() criando um novo Element .

 /** * Create a new element object. * * @return Element */ private function create_element() { return new Element( /* ... */ ); } /** * Add a new element object to this field. */ public function add_element() { $element = $this->create_element(); $this->elements[] = $element; }

Um método que retorna um novo objeto é geralmente chamado de fábrica simples (não confundir com “método de fábrica”, que é um padrão de projeto).

Sabendo que qualquer subtipo pode ser usado no lugar da classe pai Element , iremos em frente e modificaremos esta fábrica, para que ela possa criar objetos de qualquer classe filha.

 /** * Create a new element object. * * @throws Exception If there are no classes for the given element type. * @throws Exception If the given element type is not an `Element`. * * @param string $element_type * @param array $options * * @return Element */ private function create_element( $element_type, $options ) { $element_type = __NAMESPACE__ . '\\Elements\\' . $element_type; if ( ! class_exists( $element_type ) ) { throw new Exception( 'No class exists for the specified type' ); } $element = new $element_type( $this->section_id, $options ); if ( ! ( $element instanceof Element ) ) { throw new Exception( 'The specified type is invalid' ); } return $element; } /** * Add a new element object to this field. * * @param string $element_type * @param array $options */ public function add_element( $element_type, $options ) { try { $element = $this->create_element( $element_type, $options ); $this->elements[] = $element; } catch ( Exception $e ) { // Handle the exception } }

Começamos prefixando o tipo de elemento com o nome atual:

 $element_type = __NAMESPACE__ . '\\Elements\\' . $element_type;

A constante mágica __NAMESPACE__ contém o nome do namespace atual.

Em seguida, garantimos que há uma classe para o tipo de elemento especificado:

 if ( ! class_exists( $element_type ) ) { throw new Exception( 'No class exists for the specified type' ); }

Em seguida, criamos um novo objeto:

 $element = new $element_type( $this->section_id, $options );

E por último, garantimos que o objeto recém-criado é de fato uma instância de Element:

 if ( ! ( $element instanceof Element ) ) { return; }

Extensão

Vale ressaltar que construímos nosso plugin para ser extensível. Adicionar diferentes tipos de páginas, seções, elementos é tão fácil quanto criar uma nova classe que estende Admin_Page , Section , Element etc. Essas classes base não incluem nenhum código que precise ser alterado para adicionar uma nova página, seção ou elemento.

O Princípio Aberto/Fechado (ou OCP), o “O” em SOLID, afirma:

“Entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação.”

Isso significa que devemos ser capazes de estender uma classe como Admin_Page e reutilizá-la, mas não devemos modificá -la para fazer isso.

Conclusão

Neste artigo, registramos nossas seções, campos e elementos. Ao implementá-los, examinamos mais de perto o que é polimorfismo e por que ele é útil. Também analisamos alguns princípios SOLID, o “Princípio da Substituição de Liskov” e o “Princípio Aberto/Fechado”.

Fique conosco para a próxima parte desta jornada, onde veremos mais de perto como podemos melhorar a maneira como gerenciamos nossos ganchos do WordPress.

Clique aqui para ler a Parte 7 em nossa Série de Programação Orientada a Objetos

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