Parte 8 – WordPress y Programación Orientada a Objetos: Un Ejemplo de WordPress – Implementación: Opciones

Publicado: 2022-02-04

Hasta ahora, solo necesitábamos almacenar opciones definidas por el usuario, por lo que utilizamos la API de configuración. Sin embargo, nuestro complemento debe poder leer/escribir opciones para "recordar" cuántas veces una dirección IP ha intentado iniciar sesión sin éxito, si actualmente está bloqueada, etc.

Necesitamos una forma orientada a objetos para almacenar y recuperar opciones. Durante la fase de "diseño", discutimos esto brevemente, pero abstrajimos algunos de los detalles de implementación, enfocándonos únicamente en las acciones que nos gustaría poder realizar: obtener , configurar y eliminar una opción.

También vamos a "agrupar" las opciones según su sección para mantenerlas organizadas. Eso se basa puramente en la preferencia personal.

Vamos a convertir esto en una interfaz:

 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, podríamos interactuar con la API de opciones de WordPress haciendo algo como esto:

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

En este punto, es posible que se pregunte por qué no usamos simplemente la función de WordPress get_option() , en lugar de tomarnos la molestia de crear nuestra propia interfaz y clase. Si bien usar las funciones de WordPress directamente sería una forma perfectamente aceptable de desarrollar nuestro complemento, al ir un paso más allá y crear una interfaz en la que depender, nos mantenemos flexibles.

Nuestra clase WP_Options implementará nuestra interfaz de Options . De esa manera, estaremos preparados si nuestras necesidades cambian en el futuro. Por ejemplo, es posible que necesitemos almacenar nuestras opciones en una tabla personalizada, en una base de datos externa, en la memoria (por ejemplo, Redis), lo que sea. Al depender de una abstracción (es decir, la interfaz), cambiar algo en la implementación es tan simple como crear una nueva clase que implemente la misma interfaz.

WP_Opciones

Comencemos a escribir nuestra clase WP_Options , recuperando todas las opciones usando la función de WordPress get_option() en su constructor.

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

Dado que la propiedad $options se usará internamente, la declararemos private para que solo pueda acceder a ella la clase que la definió, la clase WP_Options .

Ahora, implementemos nuestra interfaz de Options usando el operador de implements .

 class WP_Options implements Options { // ...

Nuestro IDE nos está gritando que declaremos nuestra clase como abstracta o que implementemos los métodos get() , set() y remove() , definidos en la interfaz.

Entonces, ¡comencemos a implementar estos métodos!

Obtener una opción

Comenzaremos con el método get() , que buscará el nombre de la opción especificada en nuestra propiedad $options y devolverá su valor o false si no existe.

 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 ]; } }

Ahora es un buen momento para pensar en las opciones predeterminadas.

Opciones predeterminadas

Como se mencionó anteriormente, nos gustaría agrupar las opciones, según su sección. Entonces, probablemente dividiremos las opciones en un par de secciones. El apartado de “Opciones generales” y otro de los datos que necesitamos controlar. Bloqueos, reintentos, registros de bloqueos y número total de bloqueos: llamaremos arbitrariamente a este estado.

Usaremos una constante para almacenar nuestras opciones predeterminadas. El valor de una constante no se puede cambiar mientras nuestro código se está ejecutando, lo que lo hace ideal para algo como nuestras opciones predeterminadas. Las constantes de clase se asignan una vez por clase y no para cada instancia de clase.

NOTA: El nombre de una constante está en mayúsculas por convención.

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

En la matriz anidada DEFAULT_OPTIONS , hemos establecido un valor predeterminado para todas nuestras opciones.

Lo que nos gustaría hacer a continuación es almacenar los valores de opción predeterminados en la base de datos una vez que se inicializa el complemento, mediante el uso de la función de WordPress add_option() .

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

Echemos un vistazo más de cerca a este fragmento. Primero, iteramos la matriz de opciones predeterminada y recuperamos las opciones usando la función de WordPress get_option() .

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

Luego, verificamos si cada opción ya existe en la base de datos y, si no, almacenamos su opción predeterminada.

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

Por último, recogemos las opciones de todos los apartados.

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

Y guárdelos en la propiedad $options para que podamos acceder a ellos más adelante.

 $this->options = $all_options;

La tabla de opciones de WordPress en la base de datos tendrá un par de filas, donde option_name consiste en el prefijo del complemento concatenado con el nombre de la sección.

Pasemos ahora al resto de los métodos que necesitamos implementar.

Almacenamiento de una opción

De manera similar, nos gustaría almacenar fácilmente una nueva opción en la base de datos y sobrescribir cualquier valor anterior, así:

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

Entonces, implementemos el método set() , que usará la función de WordPress update_option() .

 /** * 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 ); }

Eliminando una opción

Por último, implementaremos el método remove() , que establecerá la opción en su 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 ); }

Hemos agrupado todo en una sola clase. Todos los datos relacionados con las opciones (es decir, nuestras propiedades) y los detalles de implementación (es decir, los métodos que acabamos de implementar) están encapsulados en la clase WP_Options .

Encapsulación/Abstracción

Envolver todo en una sola clase, encerrar las partes internas (como en una cápsula), esencialmente “ocultándolas” del mundo exterior, es lo que llamamos encapsulación . La encapsulación es otro concepto central de la programación orientada a objetos.

Aloje su sitio web con Pressidium

GARANTÍA DE DEVOLUCIÓN DE DINERO DE 60 DÍAS

VER NUESTROS PLANES

Usando la interfaz de Options , nos enfocamos en lo que hacemos con nuestras opciones en lugar de cómo lo hacemos, abstrayendo la idea de las opciones, simplificando las cosas conceptualmente. Esto es lo que llamamos abstracción , otro concepto central de la programación orientada a objetos.

La encapsulación y la abstracción son conceptos completamente diferentes , pero claramente, como puedes ver, muy relacionados. Su principal diferencia es que la encapsulación existe en el nivel de implementación, mientras que la abstracción existe en el nivel de diseño.

dependencias

Consideremos el siguiente escenario:

Hay una clase de Lockouts , responsable de determinar si una dirección IP debe bloquearse, cuál debe ser la duración de ese bloqueo, si un bloqueo activo sigue siendo válido o ha expirado, etc. Esa clase contiene un método should_get_locked_out() , responsable de determinar si una dirección IP debe quedar bloqueada. Ese método necesitaría leer la cantidad máxima de reintentos permitidos antes de que se bloquee una dirección IP, que es un valor configurable, lo que significa que se almacena como una opción .

Entonces, el código que acabamos de describir sería similar 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; } // ... }

Básicamente, estamos creando una nueva instancia de WP_Options en el constructor y luego usamos esa instancia para recuperar el valor de la opción allowed_retries .

Eso está absolutamente bien, pero debemos tener en cuenta que nuestra clase Lockouts ahora depende de WP_Options . Llamamos a WP_Options una dependencia .

Si nuestras necesidades cambian en el futuro, por ejemplo, necesitamos leer/escribir opciones en una base de datos externa, debemos reemplazar WP_Options con una clase DB_Options . Eso no parece tan malo, si necesitamos recuperar opciones en una sola clase. Sin embargo, puede ser un poco complicado cuando hay muchas clases con múltiples dependencias. Es probable que cualquier cambio en una sola dependencia se extienda por el código base, lo que nos obligará a modificar una clase si cambia una de sus dependencias.

Podemos eliminar este problema reescribiendo nuestro código para seguir el Principio de Inversión de Dependencia .

desacoplamiento

El Principio de Inversión de Dependencia (DIP), la "D" en SOLID, establece:

  • Los módulos de alto nivel no deberían importar nada de los módulos de bajo nivel. Ambos deberían depender de abstracciones.
  • Las abstracciones no deben depender de los detalles. Los detalles (implementaciones concretas) deberían depender de abstracciones.

En nuestro caso, la clase Lockouts es el “módulo de alto nivel” y depende de un “módulo de bajo nivel”, la clase WP_Options .

Cambiaremos eso, usando Dependency Injection , que es más fácil de lo que parece. Nuestra clase Lockouts recibirá los objetos de los que depende, en lugar de crearlos.

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

Entonces, inyectamos una dependencia:

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

Acabamos de hacer que nuestra clase Lockouts sea más fácil de mantener, ya que ahora está ligeramente acoplada con su dependencia WP_Options . Además, podremos simular las dependencias, lo que hará que nuestro código sea más fácil de probar. Reemplazar WP_Options con un objeto que imite su comportamiento nos permitirá probar nuestro código sin ejecutar ninguna consulta en una base de datos.

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

Aunque le hemos dado el control de las dependencias de Lockouts a otra clase (en lugar de que Lockouts controle las dependencias en sí), Lockouts aún espera un objeto WP_Options . Lo que significa que todavía depende de la clase WP_Options concreta, en lugar de una abstracción. Como se mencionó anteriormente, ambos módulos deben depender de abstracciones .

¡Arreglemos eso!

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

Y simplemente cambiando el tipo del argumento $options de la clase WP_Options a la interfaz de Options , nuestra clase Lockouts depende de una abstracción y podemos pasar un objeto DB_Options , o una instancia de cualquier clase que implemente la misma interfaz, a su constructor.

Responsabilidad Única

Vale la pena señalar que usamos un método llamado should_get_locked_out() para verificar si la dirección IP debe bloquearse o no.

 /** * 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; }

Fácilmente podríamos escribir una línea como esta:

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

Sin embargo, mover esa pieza de lógica a su propio pequeño método tiene muchos beneficios.

  • Si la condición para determinar si una dirección IP debe bloquearse alguna vez cambia, solo tendremos que modificar este método (en lugar de buscar todas las apariciones de nuestra instrucción if)
  • Escribir pruebas unitarias se vuelve más fácil cuando cada "unidad" es más pequeña
  • Mejora mucho la legibilidad de nuestro código.

Leyendo esto:

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

nos parece mucho más fácil que leer eso:

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

Hemos hecho esto para casi todos los métodos de nuestro complemento. Extraer métodos de otros más largos hasta que no haya nada más que extraer. Lo mismo ocurre con las clases, cada clase y método debe tener una única responsabilidad.

El Principio de responsabilidad única (SRP) , la "S" en SOLID, establece:

“Cada módulo, clase o función en un programa de computadora debe tener responsabilidad sobre una sola parte de la funcionalidad de ese programa, y ​​debe encapsular esa parte”.

O, como dice Robert C. Martin (“Tío Bob”):

“Una clase debe tener una, y solo una, razón para cambiar”.

Revisando el archivo del complemento principal

Por el momento, nuestro archivo de complemento principal contiene solo esto:

 /** * 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; }

Una vez más, envolveremos todo en una clase de complemento, esta vez solo para evitar colisiones de nombres.

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

Crearemos una instancia de esta clase de Plugin al final del archivo, que ejecutará el código en su constructor.

 new Plugin();

En el constructor, nos conectaremos a la acción plugins_loaded, que se activa una vez que se han cargado los complementos activados.

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

También llamaremos al método require_files() para cargar todos nuestros archivos 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 nuestro complemento creando algunos objetos en nuestro método init() .

NOTA: El siguiente fragmento contiene solo una pequeña parte del archivo del complemento principal. Puede leer el archivo real en el repositorio de GitHub del complemento.

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

Organizando los archivos

Mantener sus archivos organizados es vital, especialmente cuando se trabaja en complementos grandes con mucho código. Su estructura de carpetas debe agrupar archivos similares, ayudándolos a usted y a sus compañeros de equipo a mantenerse organizados.

Ya hemos definido un espacio de nombres ( Pressidium\Limit_Login_Attempts ), que contiene varios subespacios de nombres para Pages , Sections , Fields , Elements , etc. Siguiendo esa jerarquía para organizar nuestros directorios y archivos, terminamos con una estructura similar 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 archivo contiene una sola clase. Los archivos se nombran según las clases que contienen, y los directorios y subdirectorios se nombran según los (sub)espacios de nombres.

Existen múltiples patrones de arquitectura y esquemas de nomenclatura que puede utilizar. Depende de usted elegir uno que tenga sentido para usted y se adapte a las necesidades de su proyecto. A la hora de estructurar tu proyecto, lo importante es ser constante .

Conclusión

¡Felicidades! Has completado nuestra serie de artículos sobre WordPress y la programación orientada a objetos.

¡Espero que hayas aprendido algunas cosas y estés emocionado de comenzar a aplicar lo que aprendiste en tus propios proyectos!

Aquí hay un resumen rápido de lo que cubrimos en esta serie:

  • Recopilación de requisitos: decidimos qué debe hacer el complemento.
  • Diseño: pensamos en cómo se estructurará el complemento, las relaciones entre nuestras clases potenciales y una descripción general de alto nivel de nuestras abstracciones.
  • Implementación: escribimos el código real de algunas partes clave del complemento. Al hacerlo, le presentamos varios conceptos y principios.

Sin embargo, apenas arañamos la superficie de lo que es OOP y tiene para ofrecer. Ser bueno en una nueva habilidad requiere práctica, así que continúe y comience a crear sus propios complementos de WordPress orientados a objetos. ¡Feliz codificación!

Ver también

  • WordPress y la programación orientada a objetos: una descripción general
  • Parte 2 – WordPress y Programación Orientada a Objetos: Un Ejemplo del Mundo Real
  • Parte 3 – WordPress y Programación Orientada a Objetos: Α Ejemplo de WordPress – Definición del Alcance
  • Parte 4 – WordPress y Programación Orientada a Objetos: Un Ejemplo de WordPress – Diseño
  • Parte 5 – WordPress y Programación Orientada a Objetos: Un Ejemplo de WordPress – Implementación: El Menú de Administración
  • Parte 6 – WordPress y Programación Orientada a Objetos: Un Ejemplo de WordPress – Implementación: Registro de las Secciones
  • Parte 7 - WordPress y programación orientada a objetos: un ejemplo de WordPress - Implementación: administración de ganchos de WordPress