Partie 6 – WordPress et la programmation orientée objet : un exemple WordPress – Implémentation : enregistrement des sections

Publié: 2022-02-04

Bienvenue à nouveau dans notre série sur la programmation orientée objet.

Comme nous l'avons expliqué dans la partie Design de la série, une page d'administration se compose de sections . Chaque section contient un ou plusieurs champs , et chacun de ces champs contient un ou plusieurs éléments .

À quoi cela ressemblerait-il dans le code ?

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

Très bien, cela semble facile à utiliser et nous pouvons déjà dire que nous aurons probablement besoin de créer trois nouvelles classes : Section , Field et Element .

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

Prenons un moment et demandons-nous ce que nous savons jusqu'à présent sur ces classes.

  • $my_section->add_field() → La classe Section devrait pouvoir ajouter (et stocker) un nouvel objet Field
  • $my_field->add_element() → La classe Field devrait pouvoir ajouter (et stocker) un nouvel objet Element .

Nous commençons par stocker nos objets Field dans un tableau, comme nous le ferions normalement :

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

Cette variable $fields est un membre de classe et c'est ce que nous appelons une propriété . Les propriétés sont des variables PHP, vivant dans une classe, et elles peuvent être de n'importe quel type de données ( string , integer , object , etc.).

Nous allons également écrire la méthode add_field() pour créer et ajouter un nouveau champ.

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

Cette méthode crée un nouvel objet Field , l'ajoute à la propriété fields et renvoie cet objet nouvellement créé. Assez simple.

Répétons également le même processus pour la classe Field .

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

C'est un début ! Et après?

La classe de section

Nous devons appeler add_settings_section(), lorsqu'une nouvelle section est créée. Encore une fois, la méthode constructeur est un excellent moyen d'effectuer notre initialisation. Ajoutons-le dans la classe :

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

Il semble qu'une section ait besoin d'un nom de slug pour l'identifier (utilisé dans l'attribut id des balises). Il peut aussi avoir un titre, une description, et appartenir à une page spécifique.

 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;

Nous pourrions définir le titre de la section, en faisant quelque chose comme ceci :

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

Eh bien, ce n'est pas tout à fait vrai. Même si le code ci-dessus est parfaitement valide, il ne fait pas vraiment ce que nous attendons de lui.

La méthode constructeur est exécutée lorsqu'un nouvel objet Section est créé. Ainsi, add_settings_section() sera appelé avant même que nous ayons la possibilité de définir le titre. Par conséquent, la section n'aura pas de titre.

Hébergez votre site web avec Pressidium

GARANTIE DE REMBOURSEMENT DE 60 JOURS

VOIR NOS FORFAITS

Le titre doit être disponible lors de l'initialisation de notre objet, nous devons donc le faire dans le constructeur.

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

Attention, $this->title fait référence à la propriété de la classe title, où $title fait référence à l'argument du constructeur.

Ici, on profite aussi de la visibilité . Puisque notre propriété $title ne sera accessible que par la classe qui l'a définie, nous pouvons la déclarer private . Par conséquent, nous empêchons qu'il soit accessible en dehors de la classe.

Oh, et nous devons également ajouter une méthode print_description() qui va, eh bien, imprimer la description de la section.

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

En mettant tout ensemble, notre classe Section ressemble à ceci.

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

La classe de terrain

De la même manière que Section , nous pouvons maintenant continuer et construire la classe Field , qui va utiliser la fonction WordPress add_settings_field() .

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

Ici, nous aimerions également fournir des valeurs par défaut pour l'ID, l'étiquette et la description du champ. Nous pouvons le faire en passant un tableau d'options au constructeur et en utilisant la fonction WordPress wp_parse_args() pour analyser ces options.

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

La fonction wp_parse_args() nous permettra de fusionner les valeurs définies par l'utilisateur (le tableau $options ) avec les valeurs par défaut.

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

Nous devons également définir des étiquettes uniques pour chaque champ. Nous pouvons gérer cela en définissant l'étiquette sur un préfixe ( 'field_' ) suivi d'un nombre, qui sera augmenté à chaque fois qu'un nouvel objet Field est créé. Nous allons stocker ce nombre dans la propriété statique $number_of_fields .

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

Une propriété statique est accessible directement sans avoir à créer d'abord une instance d'une classe.

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

Le mot-clé self est utilisé pour faire référence à la classe actuelle et, à l'aide de l'opérateur de résolution de portée :: (communément appelé "double colon"), nous pouvons accéder à notre propriété statique.

De cette façon, dans le constructeur, nous accédons toujours à la même propriété $number_of_fields , augmentant sa valeur à chaque fois qu'un objet est créé, ce qui se traduit par une étiquette unique attachée à chaque champ.

À l'avenir, la méthode render() , après avoir imprimé la description (s'il en existe une), parcourt tous les éléments et restitue chacun d'eux.

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

Mettre tous ensemble…

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

La classe d'éléments

À l'avenir, nous construirons la classe Element de la même manière !

Nous allons commencer à écrire la classe comme ceci :

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

Assurez-vous que vous échappez votre sortie - comme nous le faisons ici, en utilisant les fonctions WordPress esc_attr() et esc_html() - pour empêcher toute attaque de script intersite. Même si nous rendons nos éléments uniquement dans les pages d'administration, c'est toujours une bonne idée de toujours échapper à toutes les données de sortie.

REMARQUE : Le script intersite (ou XSS) est un type de vulnérabilité de sécurité que l'on trouve généralement dans les applications Web. XSS permet aux attaquants d'injecter du code côté client dans les pages Web consultées par d'autres utilisateurs. Une vulnérabilité de script intersite peut être utilisée par des attaquants pour contourner les contrôles d'accès tels que la politique de même origine.

Lorsque nous avons rassemblé les exigences du plugin, nous avons remarqué qu'il existe plusieurs types d'éléments : cases à cocher, boutons radio, champs numériques, etc. Lorsque nous avons conçu notre conception, nous avons pris la décision de créer une classe Element destinée à être étendue. Donc, nous savons que nous allons nous retrouver avec une classe enfant pour chaque type d'élément.

La sortie doit différer selon le type d'élément, nous allons donc transformer render() en une méthode abstraite. Cela signifie, bien sûr, que la classe elle-même doit également être abstraite.

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

Par exemple, une classe Number_Element ressemblerait à ceci :

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

De même, nous pouvons créer une classe Checkbox_Element , une Radio_Element et même une classe Custom_Element pour le reste de nos éléments.

Notez que nous construisons nos classes afin qu'elles puissent toutes être utilisées de la même manière . L'appel de la méthode render() sur n'importe quel enfant de Element produira du code HTML.

C'est un exemple de polymorphisme , l'un des concepts fondamentaux de la programmation orientée objet.

Polymorphisme

« Polymorphisme » signifie littéralement « plusieurs formes » (des mots grecs « poly » signifiant « plusieurs » et « morphe » signifiant « forme »). Une classe enfant Element peut avoir plusieurs formes , car elle peut prendre n'importe quelle forme d'une classe dans sa hiérarchie parent.

Nous pouvons utiliser un Number_Element , un Checkbox_Element , ou tout autre sous-type à n'importe quel endroit où un objet Element est attendu, puisque tous les objets enfants peuvent être utilisés exactement de la même manière (c'est-à-dire en appelant leur méthode render() ), tout en étant toujours capable de se comporter différemment (la sortie sera différente pour chaque type d'élément).

Comme vous pouvez probablement le constater, le polymorphisme et l'héritage sont des concepts étroitement liés.

Substituabilité

Le principe de substitution de Liskov (ou LSP) , le « L » dans SOLID, stipule :

« Dans un programme informatique, si S est un sous-type de T, alors les objets de type T peuvent être remplacés par des objets de type S (c'est-à-dire qu'un objet de type T peut être remplacé par n'importe quel objet d'un sous-type S) sans altérer aucun des les propriétés souhaitables du programme.

En termes simples, vous devriez pouvoir utiliser n'importe quelle classe enfant à la place de sa classe parent sans aucun comportement inattendu.

Des usines

Revenons à notre classe Field , où nous avons actuellement une méthode create_element() créant un nouvel 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; }

Une méthode qui retourne un nouvel objet est souvent appelée une fabrique simple (à ne pas confondre avec la "méthode de fabrique", qui est un modèle de conception).

Sachant que n'importe quel sous-type est utilisable à la place de la classe parent Element , nous allons continuer et modifier cette fabrique, afin qu'elle puisse créer des objets de n'importe quelle classe enfant.

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

Nous commençons par préfixer le type d'élément avec le nom actuel :

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

La constante magique __NAMESPACE__ contient le nom de l'espace de noms actuel.

Ensuite, nous nous assurons qu'il existe une classe pour le type d'élément spécifié :

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

Ensuite, nous créons un nouvel objet :

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

Et enfin, nous nous assurons que l'objet nouvellement créé est bien une instance de Element :

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

Extension

Il convient de souligner que nous avons construit notre plugin pour qu'il soit extensible. Ajouter différents types de pages, sections, éléments est aussi simple que de créer une nouvelle classe qui étend Admin_Page , Section , Element etc. Ces classes de base n'incluent aucun code qui doit être modifié pour ajouter une nouvelle page, section ou élément.

Le principe ouvert/fermé (ou OCP), le « O » dans SOLID, stipule :

"Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification."

Cela signifie que nous devrions pouvoir étendre une classe comme Admin_Page et la réutiliser, mais nous ne devrions pas avoir à la modifier pour ce faire.

Conclusion

Dans cet article, nous avons enregistré nos sections, champs et éléments. Lors de leur mise en œuvre, nous avons examiné de plus près ce qu'est le polymorphisme et pourquoi il est utile. Nous avons également jeté un coup d'œil sur quelques principes SOLID, le « principe de substitution de Liskov » et le « principe ouvert/fermé ».

Restez avec nous pour la prochaine partie de ce voyage, où nous examinerons de plus près comment nous pouvons améliorer la façon dont nous gérons nos crochets WordPress.

Cliquez ici pour lire la partie 7 de notre série sur la programmation orientée objet

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