Partie 8 – WordPress et Programmation Orientée Objet : Un Exemple WordPress – Implémentation : Options

Publié: 2022-02-04

Jusqu'à présent, nous n'avions besoin que de stocker des options définies par l'utilisateur, nous avons donc utilisé l'API Settings. Cependant, notre plugin doit être capable de lire/écrire des options lui-même pour "se souvenir" du nombre de fois qu'une adresse IP a tenté de se connecter sans succès, si elle est actuellement verrouillée, etc.

Nous avons besoin d'un moyen orienté objet pour stocker et récupérer les options. Au cours de la phase de « conception », nous en avons brièvement discuté, mais nous avons fait abstraction de certains détails d'implémentation, en nous concentrant uniquement sur les actions que nous aimerions pouvoir effectuer : obtenir , définir et supprimer une option.

Nous allons également trier les options de « groupe » en fonction de leur section pour les garder organisées. C'est purement basé sur les préférences personnelles.

Transformons cela en une 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 ); }

Idéalement, nous serions capables d'interagir avec l'API WordPress Options, en faisant quelque chose comme ceci :

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

À ce stade, vous vous demandez peut-être pourquoi nous n'utilisons pas simplement la fonction WordPress get_option() , au lieu de créer notre propre interface et classe. Bien que l'utilisation directe des fonctions WordPress soit une manière parfaitement acceptable de développer notre plugin, en allant un peu plus loin et en créant une interface sur laquelle dépendre, nous restons flexibles.

Notre classe WP_Options va implémenter notre interface Options . De cette façon, nous serons prêts si nos besoins changent à l'avenir. Par exemple, nous pourrions avoir besoin de stocker nos options dans une table personnalisée, dans une base de données externe, en mémoire (par exemple Redis), etc. En dépendant d'une abstraction (c'est-à-dire d'une interface), changer quelque chose dans l'implémentation est aussi simple que de créer une nouvelle classe implémentant la même interface.

WP_Options

Commençons à écrire notre classe WP_Options , en récupérant toutes les options à l'aide de la fonction WordPress get_option() dans son constructeur.

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

Étant donné que la propriété $options sera utilisée en interne, nous la déclarerons private afin qu'elle ne soit accessible que par la classe qui l'a définie, la classe WP_Options .

Maintenant, implémentons notre interface Options en utilisant l'opérateur implements .

 class WP_Options implements Options { // ...

Notre IDE nous crie soit de déclarer notre classe abstraite, soit d'implémenter les méthodes get() , set() et remove() , définies dans l'interface.

Alors, commençons à mettre en œuvre ces méthodes !

Obtenir une option

Nous allons commencer par la méthode get() , qui va rechercher le nom d'option spécifié dans notre propriété $options , et retourner sa valeur ou false si elle n'existe pas.

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

C'est maintenant le bon moment pour réfléchir aux options par défaut.

Options par défaut

Comme mentionné précédemment, nous aimerions regrouper les options en fonction de leur section. Donc, nous allons probablement diviser les options en quelques sections. La section "Options générales" et une autre pour les données dont nous avons besoin de suivre. Verrouillages, tentatives, journaux de verrouillage et nombre total de verrouillages : nous appellerons arbitrairement cet état.

Nous utiliserons une constante pour stocker nos options par défaut. La valeur d'une constante ne peut pas être modifiée pendant l'exécution de notre code, ce qui la rend idéale pour quelque chose comme nos options par défaut. Les constantes de classe sont allouées une fois par classe, et non pour chaque instance de classe.

NOTE : Le nom d'une constante est en majuscule par convention.

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

Dans le tableau imbriqué DEFAULT_OPTIONS , nous avons défini une valeur par défaut pour toutes nos options.

Ce que nous aimerions faire ensuite, c'est stocker les valeurs d'option par défaut dans la base de données une fois le plugin initialisé, en utilisant la fonction 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; } }

Regardons de plus près cet extrait. Tout d'abord, nous parcourons le tableau d'options par défaut et récupérons les options à l'aide de la fonction 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 ); // ...

Ensuite, nous vérifions si chaque option existe déjà dans la base de données, et si ce n'est pas le cas, nous stockons son option par défaut.

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

Enfin, nous recueillons les options de toutes les sections.

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

Et stockez-les dans la propriété $options afin que nous puissions y accéder plus tard.

 $this->options = $all_options;

La table d'options WordPress dans la base de données va avoir quelques lignes, où l' option_name se compose du préfixe du plugin concaténé au nom de la section.

Passons maintenant au reste des méthodes que nous devons mettre en œuvre.

Mémorisation d'une option

De même, nous aimerions stocker facilement une nouvelle option dans la base de données et écraser toute valeur précédente, comme ceci :

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

Alors, implémentons la méthode set() , qui va utiliser la fonction 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 ); }

Suppression d'une option

Enfin, nous allons implémenter la méthode remove() , qui va définir l'option à sa valeur initiale :

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

Nous avons tout regroupé dans une seule classe. Toutes les données relatives aux options (c'est-à-dire nos propriétés) et les détails d'implémentation (c'est-à-dire les méthodes que nous venons d'implémenter) sont encapsulés dans la classe WP_Options .

Encapsulation/Abstraction

Envelopper tout dans une seule classe, enfermer les éléments internes (comme dans une capsule), les «cacher» essentiellement du monde extérieur, c'est ce que nous appelons l' encapsulation . L'encapsulation est un autre concept fondamental de la programmation orientée objet.

Hébergez votre site web avec Pressidium

GARANTIE DE REMBOURSEMENT DE 60 JOURS

VOIR NOS FORFAITS

En utilisant l'interface Options , nous nous sommes concentrés sur ce que nous faisons avec nos options plutôt que sur la façon dont nous le faisons, en faisant abstraction de l'idée d'options, en simplifiant les choses conceptuellement. C'est ce que nous appelons l'abstraction , un autre concept de base de la programmation orientée objet.

L'encapsulation et l'abstraction sont des concepts complètement différents , mais clairement, comme vous pouvez le voir, fortement liés. Leur principale différence est que l'encapsulation existe au niveau de la mise en œuvre, tandis que l'abstraction existe au niveau de la conception.

Dépendances

Considérons le scénario suivant :

Il existe une classe Lockouts , chargée de déterminer si une adresse IP doit être verrouillée, quelle devrait être la durée de ce verrouillage, si un verrouillage actif est toujours valide ou a expiré, etc. Cette classe contient une méthode should_get_locked_out() , chargée de déterminer si une adresse IP doit être verrouillée. Cette méthode aurait besoin de lire le nombre maximum de tentatives autorisées avant qu'une adresse IP ne soit verrouillée, ce qui est une valeur configurable, ce qui signifie qu'elle est stockée en tant qu'option .

Ainsi, le code que nous venons de décrire ressemblerait à ceci :

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

Fondamentalement, nous créons une nouvelle instance de WP_Options dans le constructeur, puis utilisons cette instance pour récupérer la valeur de l'option allowed_retries .

C'est très bien, mais nous devons garder à l'esprit que notre classe Lockouts dépend désormais de WP_Options . Nous appelons WP_Options une dépendance .

Si nos besoins changent à l'avenir, par exemple, nous devons lire/écrire des options sur une base de données externe, nous devrons remplacer WP_Options par une classe DB_Options . Cela ne semble pas si mal, si nous avons besoin de récupérer des options dans une seule classe. Cependant, cela peut devenir un peu délicat lorsqu'il existe de nombreuses classes avec plusieurs dépendances. Toute modification apportée à une seule dépendance se répercutera probablement sur la base de code, nous obligeant à modifier une classe si l'une de ses dépendances change.

Nous pouvons éliminer ce problème en réécrivant notre code pour suivre le principe d'inversion de dépendance .

Découplage

Le principe d'inversion de dépendance (DIP), le « D » de SOLID, stipule :

  • Les modules de haut niveau ne doivent rien importer des modules de bas niveau. Les deux devraient dépendre d'abstractions.
  • Les abstractions ne doivent pas dépendre des détails. Les détails (implémentations concrètes) doivent dépendre des abstractions.

Dans notre cas, la classe Lockouts est le "module de haut niveau" et elle dépend d'un "module de bas niveau", la classe WP_Options .

Nous allons changer cela en utilisant Dependency Injection , ce qui est plus simple qu'il n'y paraît. Notre classe Lockouts recevra les objets dont elle dépend, au lieu de les créer.

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

Donc, nous injectons une dépendance :

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

Nous venons de rendre notre classe Lockouts plus facile à maintenir car elle est maintenant couplée de manière lâche avec sa dépendance WP_Options . De plus, nous pourrons simuler les dépendances, ce qui rendra notre code plus facile à tester. Remplacer WP_Options par un objet qui imite son comportement nous permettra de tester notre code sans réellement exécuter de requêtes sur une base de données.

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

Même si nous avons donné le contrôle des dépendances de Lockouts à une autre classe (par opposition à Lockouts contrôlant les dépendances lui-même), Lockouts attend toujours un objet WP_Options . Cela signifie que cela dépend toujours de la classe concrète WP_Options , au lieu d'une abstraction. Comme mentionné précédemment, les deux modules doivent dépendre des abstractions .

Réparons ça !

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

Et en changeant simplement le type de l'argument $options de la classe WP_Options à l'interface Options , notre classe Lockouts dépend d'une abstraction et nous sommes libres de passer un objet DB_Options , ou une instance de n'importe quelle classe qui implémente la même interface, à son constructeur.

Responsabilité unique

Il convient de noter que nous avons utilisé une méthode appelée should_get_locked_out() pour vérifier si l'adresse IP doit être verrouillée ou non.

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

Nous pourrions facilement écrire un one-liner comme celui-ci :

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

Cependant, déplacer ce morceau de logique dans sa propre petite méthode présente de nombreux avantages.

  • Si la condition pour déterminer si une adresse IP doit être verrouillée change, nous n'aurons qu'à modifier cette méthode (au lieu de rechercher toutes les occurrences de notre instruction if)
  • L'écriture de tests unitaires devient plus facile lorsque chaque "unité" est plus petite
  • Améliore beaucoup la lisibilité de notre code

Lisant cela:

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

nous semble bien plus simple que de lire ça :

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

Nous avons fait cela pour à peu près toutes les méthodes de notre plugin. Extraire des méthodes de plus longues jusqu'à ce qu'il n'y ait plus rien à extraire. Il en va de même pour les classes, chaque classe et méthode devrait avoir une seule responsabilité.

Le principe de responsabilité unique (SRP) , le « S » de SOLID, stipule :

"Chaque module, classe ou fonction d'un programme informatique devrait avoir la responsabilité d'une seule partie de la fonctionnalité de ce programme, et il devrait encapsuler cette partie."

Ou, comme le dit Robert C. Martin (« Oncle Bob ») :

"Une classe devrait avoir une, et une seule, raison de changer."

Revisiter le fichier principal du plugin

Pour le moment, notre fichier de plugin principal ne contient que ceci :

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

Encore une fois, nous allons tout encapsuler dans une classe Plugin, cette fois juste pour éviter les collisions de noms.

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

Nous allons instancier cette classe Plugin à la fin du fichier, qui va exécuter le code dans son constructeur.

 new Plugin();

Dans le constructeur, nous allons nous connecter à l'action plugins_loaded, qui se déclenche une fois que les plugins activés ont été chargés.

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

Nous appellerons également une méthode require_files() pour charger tous nos fichiers 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'; // ... }

Enfin, nous allons initialiser notre plugin en créant des objets dans notre méthode init() .

REMARQUE : L'extrait de code suivant ne contient qu'une petite partie du fichier principal du plug-in. Vous pouvez lire le fichier réel dans le référentiel GitHub du plugin.

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

Organisation des fichiers

Il est essentiel de garder vos fichiers organisés, en particulier lorsque vous travaillez sur de gros plugins avec beaucoup de code. Votre structure de dossiers doit regrouper les fichiers similaires, vous aidant ainsi que vos coéquipiers à rester organisés.

Nous avons déjà défini un espace de noms ( Pressidium\Limit_Login_Attempts ), contenant plusieurs sous-espaces de noms pour Pages , Sections , Fields , Elements , etc. En suivant cette hiérarchie pour organiser nos répertoires et fichiers, nous nous sommes retrouvés avec une structure similaire à ceci :

 . ├── 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

Chaque fichier contient une seule classe. Les fichiers sont nommés d'après les classes qu'ils contiennent, et les répertoires et sous-répertoires sont nommés d'après les (sous-) espaces de noms.

Il existe plusieurs modèles d'architecture et schémas de nommage que vous pouvez utiliser. A vous de choisir celui qui vous convient et qui correspond aux besoins de votre projet. Lorsqu'il s'agit de structurer votre projet, l'important est d' être cohérent .

Conclusion

Toutes nos félicitations! Vous avez terminé notre série d'articles sur WordPress et la programmation orientée objet.

J'espère que vous avez appris quelques choses et que vous êtes impatient de commencer à appliquer ce que vous avez appris sur vos propres projets !

Voici un bref récapitulatif de ce que nous avons couvert dans cette série :

  • Collecte des exigences : nous avons décidé de ce que le plugin doit faire.
  • Conception : nous avons réfléchi à la manière dont le plugin sera structuré, aux relations entre nos classes potentielles et à un aperçu de haut niveau de nos abstractions.
  • Implémentation : Nous avons écrit le code réel de certaines parties clés du plugin. Ce faisant, nous vous avons présenté plusieurs concepts et principes.

Cependant, nous avons à peine effleuré la surface de ce qu'est et a à offrir la POO. Devenir bon dans une nouvelle compétence demande de la pratique, alors allez-y et commencez à créer vos propres plugins WordPress orientés objet. Bon codage !

Voir également

  • WordPress et la programmation orientée objet - Un aperçu
  • Partie 2 – WordPress et la programmation orientée objet : un exemple concret
  • Partie 3 – WordPress et la programmation orientée objet : Α Exemple WordPress – Définition de la portée
  • Partie 4 – WordPress et la programmation orientée objet : un exemple WordPress – Conception
  • Partie 5 – WordPress et la programmation orientée objet : un exemple WordPress – Implémentation : le menu Administration
  • Partie 6 – WordPress et la programmation orientée objet : un exemple WordPress – Implémentation : enregistrement des sections
  • Partie 7 – WordPress et Programmation Orientée Objet : Un Exemple WordPress – Implémentation : Gestion des Hooks WordPress