En arrivant ici exactement 2 ans après que la question initiale a été posée, il y a quelques points que je voudrais souligner. (Ne me demandez pas de souligner beaucoup de choses , jamais).
Crochet approprié
Pour instancier une classe de plug-in, le hook approprié doit être utilisé. Il n’ya pas de règle générale, car cela dépend de ce que fait la classe.
Utiliser un hook très précoce comme "plugins_loaded"
souvent n'a pas de sens car un tel hook est déclenché pour les requêtes admin, frontend et AJAX, mais très souvent, un hook plus récent est bien meilleur car il permet d'instancier les classes de plug-in uniquement lorsque cela est nécessaire.
Par exemple, une classe qui fait des choses pour les modèles peut être instanciée "template_redirect"
.
De manière générale, il est très rare qu'une classe ait besoin d'être instanciée avant "wp_loaded"
d'être renvoyée.
Aucune classe de Dieu
La plupart des classes utilisées comme exemples dans les réponses plus anciennes utilisent une classe nommée comme "Prefix_Example_Plugin"
ou "My_Plugin"
... Cela indique qu'il existe probablement une classe principale pour le plugin.
Bien, à moins qu'un plugin ne soit créé par une seule classe (auquel cas le nommer après le nom du plugin est absolument raisonnable), pour créer une classe qui gère l'intégralité du plugin (par exemple, ajouter tous les points d'ancrage dont un plugin a besoin ou instancier toutes les autres classes du plugin. ) peut être considéré comme une mauvaise pratique, à titre d'exemple d' objet divin .
Dans le code de programmation orienté objet, le code devrait avoir tendance à être SOLID, le "S" signifiant "Principe de responsabilité unique" .
Cela signifie que chaque classe devrait faire une seule chose. Dans le développement de plugins WordPress, cela signifie que les développeurs devraient éviter d'utiliser un seul hook pour instancier une classe de plugin principale , mais différents hooks doivent être utilisés pour instancier différentes classes, en fonction de la responsabilité de la classe.
Éviter les crochets dans le constructeur
Cet argument a été introduit dans d'autres réponses ici, cependant, je voudrais faire remarquer ce concept et lier cette autre réponse là où il a été assez largement expliqué dans le domaine des tests unitaires.
Presque 2015: PHP 5.2 est pour les zombies
Depuis le 14 août 2014, PHP 5.3 est arrivé en fin de vie . C'est définitivement mort. PHP 5.4 sera supporté pour tout 2015, cela signifie pour une autre année au moment où j'écris.
Cependant, WordPress supporte toujours PHP 5.2, mais personne ne devrait écrire une seule ligne de code prenant en charge cette version, en particulier si le code est OOP.
Il y a différentes raisons:
- PHP 5.2 est mort depuis longtemps, aucun correctif de sécurité n'a été publié, ce qui signifie qu'il n'est pas sécurisé.
- PHP 5.3 a ajouté de nombreuses fonctionnalités à PHP, des fonctions anonymes et des espaces de noms über alles
- les nouvelles versions de PHP sont beaucoup plus rapides . PHP est gratuit. La mise à jour est gratuite. Pourquoi utiliser une version plus lente et peu sûre si vous pouvez en utiliser une gratuitement plus rapide et plus sécurisée?
Si vous ne voulez pas utiliser le code PHP 5.4+, utilisez au moins 5.3+
Exemple
À ce stade, il est temps de passer en revue les réponses précédentes en fonction de ce que j’avais dit jusqu’ici.
Une fois que nous n’avons plus à nous soucier de la version 5.2, nous pouvons et devrions utiliser des espaces de noms.
Afin de mieux expliquer le principe de responsabilité unique, mon exemple utilisera 3 classes, une qui fait quelque chose sur le frontend, une sur le backend et une troisième utilisée dans les deux cas.
Classe Admin:
namespace GM\WPSE\Example;
class AdminStuff {
private $tools;
function __construct( ToolsInterface $tools ) {
$this->tools = $tools;
}
function setup() {
// setup class, maybe add hooks
}
}
Classe frontend:
namespace GM\WPSE\Example;
class FrontStuff {
private $tools;
function __construct( ToolsInterface $tools ) {
$this->tools = $tools;
}
function setup() {
// setup class, maybe add hooks
}
}
Interface des outils:
namespace GM\WPSE\Example;
interface ToolsInterface {
function doSomething();
}
Et une classe Tools, utilisée par les deux autres:
namespace GM\WPSE\Example;
class Tools implements ToolsInterface {
function doSomething() {
return 'done';
}
}
Ayant ces classes, je peux les instancier avec les hooks appropriés. Quelque chose comme:
require_once plugin_dir_path( __FILE__ ) . 'src/ToolsInterface.php';
require_once plugin_dir_path( __FILE__ ) . 'src/Tools.php';
add_action( 'admin_init', function() {
require_once plugin_dir_path( __FILE__ ) . 'src/AdminStuff.php';
$tools = new GM\WPSE\Example\Tools;
global $admin_stuff; // this is not ideal, reason is explained below
$admin_stuff = new GM\WPSE\Example\AdminStuff( $tools );
} );
add_action( 'template_redirect', function() {
require_once plugin_dir_path( __FILE__ ) . 'src/FrontStuff.php';
$tools = new GM\WPSE\Example\Tools;
global $front_stuff; // this is not ideal, reason is explained below
$front_stuff = new GM\WPSE\Example\FrontStuff( $tools );
} );
Inversion de dépendance et injection de dépendance
Dans l'exemple ci-dessus, j'ai utilisé des espaces de noms et des fonctions anonymes pour instancier différentes classes à différents hooks, mettant en pratique ce que j'ai dit ci-dessus.
Notez comment les espaces de noms permettent de créer des classes nommées sans préfixe.
J'ai appliqué un autre concept qui a été indirectement mentionné ci-dessus: Injection de dépendance , c'est une méthode pour appliquer le principe d'inversion de dépendance , le "D" dans l'acronyme SOLID.
La Tools
classe est "injectée" dans les deux autres classes lorsqu'elles sont instanciées, ce qui permet de séparer les responsabilités.
De plus, AdminStuff
et les FrontStuff
classes utilisent le type hinting de déclarer qu'ils ont besoin d' une classe qui implémente ToolsInterface
.
De cette manière, nous ou les utilisateurs de notre code pouvons utiliser différentes implémentations de la même interface, ce qui fait que notre code n'est pas couplé à une classe concrète mais à une abstraction: c'est exactement ce que dit le principe d'inversion de dépendance.
Cependant, l'exemple ci-dessus peut être encore amélioré. Voyons comment.
Autochargeur
Un bon moyen d'écrire du code POO mieux lisible est de ne pas mélanger la définition des types (interfaces, classes) avec un autre code et de mettre chaque type dans son propre fichier.
Cette règle fait également partie des normes de codage PSR-1 1 .
Cependant, avant de pouvoir utiliser une classe, il faut avoir besoin du fichier qui la contient.
Cela peut être accablant, mais PHP fournit des fonctions utilitaires pour charger automatiquement une classe lorsque cela est nécessaire, en utilisant un rappel qui charge un fichier en fonction de son nom.
Utiliser les espaces de noms devient très simple, car il est maintenant possible de faire correspondre la structure des dossiers à la structure des espaces de noms.
C'est non seulement possible, mais c'est aussi un autre standard PSR (ou mieux 2: PSR-0 maintenant obsolète et PSR-4 ).
En respectant ces normes, il est possible d'utiliser différents outils de gestion du chargement automatique, sans avoir à coder un chargeur automatique personnalisé.
Je dois dire que les normes de codage WordPress ont des règles différentes pour nommer les fichiers.
Ainsi, lors de l'écriture de code pour le noyau WordPress, les développeurs doivent suivre les règles WP, mais lors de l'écriture de code personnalisé, c'est un choix pour les développeurs, mais l'utilisation de la norme PSR est plus facile à utiliser avec les outils déjà écrits 2 .
Modèles d'accès global, de registre et de localisateur de services.
L’un des plus gros problèmes lors de l’instanciation des classes de plugins dans WordPress est de savoir comment y accéder depuis différentes parties du code.
WordPress lui-même utilise l' approche globale : les variables sont enregistrées dans une portée globale, ce qui les rend accessibles partout. Chaque développeur WP tape le mot des global
milliers de fois dans sa carrière.
C’est aussi l’approche que j’ai utilisée pour l’exemple ci-dessus, mais c’est un mal .
Cette réponse est déjà beaucoup trop longue pour me permettre d'expliquer davantage pourquoi, mais la lecture des premiers résultats du SERP pour «variables globales mauvaises» constitue un bon point de départ.
Mais comment est-il possible d'éviter les variables globales?
Il y a différentes manières.
Certaines des réponses plus anciennes ici utilisent l' approche d'instance statique .
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self;
}
return self::$instance;
}
C'est facile et très bien, mais cela oblige à mettre en œuvre le modèle pour chaque classe à laquelle nous voulons accéder.
De plus, cette approche est souvent mise à mal, car les développeurs rendent accessible une classe principale à l' aide de cette méthode, puis l'utilisent pour accéder à toutes les autres classes.
J'ai déjà expliqué à quel point une classe de dieu est mauvaise. L'approche d'instance statique est donc un bon choix lorsqu'un plugin n'a besoin que de rendre accessible une ou deux classes.
Cela ne signifie pas qu'il ne peut être utilisé que pour des plugins n'ayant que quelques classes. En fait, lorsque le principe d'injection de dépendance est utilisé correctement, il est possible de créer des applications assez complexes sans avoir à rendre globalement accessible un grand nombre d'objets.
Cependant, parfois, les plugins doivent rendre certaines classes accessibles , et dans ce cas, l'approche d'instance statique est écrasante.
Une autre approche possible consiste à utiliser le modèle de registre .
Ceci est une implémentation très simple:
namespace GM\WPSE\Example;
class Registry {
private $storage = array();
function add( $id, $class ) {
$this->storage[$id] = $class;
}
function get( $id ) {
return array_key_exists( $id, $this->storage ) ? $this->storage[$id] : NULL;
}
}
À l'aide de cette classe, il est possible de stocker des objets dans l'objet de registre à l'aide d'un identifiant. Vous avez donc accès à un registre. Vous pouvez également accéder à tous les objets. Bien sûr, lorsqu'un objet est créé pour la première fois, il doit être ajouté au registre.
Exemple:
global $registry;
if ( is_null( $registry->get( 'tools' ) ) ) {
$tools = new GM\WPSE\Example\Tools;
$registry->add( 'tools', $tools );
}
if ( is_null( $registry->get( 'front' ) ) ) {
$front_stuff = new GM\WPSE\Example\FrontStuff( $registry->get( 'tools' ) );
$registry->add( 'front', front_stuff );
}
add_action( 'wp', array( $registry->get( 'front' ), 'wp' ) );
L'exemple ci-dessus montre clairement que, pour être utile, le registre doit être accessible de manière globale. Une variable globale pour le registre unique n'est pas très mauvaise, cependant, pour les puristes non globaux, il est possible d'implémenter l'approche d'instance statique pour un registre, ou peut-être une fonction avec une variable statique:
function gm_wpse_example_registry() {
static $registry = NULL;
if ( is_null( $registry ) ) {
$registry = new GM\WPSE\Example\Registry;
}
return $registry;
}
La première fois que la fonction est appelée, elle instanciera le registre, elle sera simplement renvoyée lors d'appels suivants.
Une autre méthode spécifique à WordPress pour rendre une classe accessible globalement renvoie le retour d'une instance d'objet à partir d'un filtre. Quelque chose comme ça:
$registry = new GM\WPSE\Example\Registry;
add_filter( 'gm_wpse_example_registry', function() use( $registry ) {
return $registry;
} );
Après cela, le registre est nécessaire partout:
$registry = apply_filters( 'gm_wpse_example_registry', NULL );
Un autre modèle pouvant être utilisé est le modèle de localisation de service . Il est similaire au modèle de registre, mais les localisateurs de service sont transmis à différentes classes à l'aide de l'injection de dépendance.
Le principal problème de ce modèle est qu’il masque les dépendances des classes, ce qui rend le code plus difficile à maintenir et à lire.
Conteneurs DI
Quelle que soit la méthode utilisée pour rendre le registre ou le localisateur de services accessible à l’ensemble du monde, les objets doivent y être stockés et, avant d’être stockés, ils doivent être instanciés.
Dans les applications complexes, où il y a beaucoup de classes et beaucoup d'entre elles ont plusieurs dépendances, l'instanciation des classes nécessite beaucoup de code, donc la possibilité de bogues augmente: le code qui n'existe pas ne peut pas avoir de bogues.
Il est apparu ces dernières années des bibliothèques PHP qui aident les développeurs PHP à instancier et à stocker facilement des instances d’objets, en résolvant automatiquement leurs dépendances.
Ces bibliothèques sont appelées conteneurs d'injection de dépendances car elles sont capables d'instancier des classes résolvant des dépendances et également de stocker des objets et de les renvoyer si nécessaire, agissant de la même manière pour un objet de registre.
Habituellement, lors de l'utilisation de conteneurs DI, les développeurs doivent configurer les dépendances pour chaque classe de l'application, puis la première fois qu'une classe est nécessaire dans le code, elle est instanciée avec les dépendances appropriées et la même instance est renvoyée à plusieurs reprises lors de requêtes ultérieures. .
Certains conteneurs DI sont également capables de détecter automatiquement les dépendances sans configuration, mais en utilisant la réflexion PHP .
Certains conteneurs DI bien connus sont:
et plein d'autres.
Je tiens à souligner que pour les plugins simples, qui impliquent seulement quelques classes et que les classes n'ont pas beaucoup de dépendances, l'utilisation de conteneurs DI ne vaut probablement pas la peine: la méthode de l'instance statique ou un registre accessible mondial sont de bonnes solutions, mais pour les plugins complexes l'avantage d'un conteneur DI devient évident.
Bien entendu, même les objets de conteneur DI doivent être accessibles pour pouvoir être utilisés dans l'application. Pour cela, il est possible d'utiliser l'une des méthodes décrites ci-dessus, variable globale, variable d'instance statique, renvoyer un objet via un filtre, etc.
Compositeur
Utiliser un conteneur DI signifie souvent utiliser un code tiers. De nos jours, en PHP, lorsque nous devons utiliser une bibliothèque externe (donc non seulement les conteneurs DI, mais tout code ne faisant pas partie de l'application), le télécharger et le placer dans notre dossier d'application n'est pas considéré comme une bonne pratique. Même si nous sommes les auteurs de cet autre morceau de code.
Découpler un code d'application des dépendances externes est le signe d'une meilleure organisation, d'une meilleure fiabilité et d'une meilleure santé du code.
Composer , est le standard de facto de la communauté PHP pour gérer les dépendances PHP. Loin d’être aussi courant dans la communauté WP, c’est un outil que tous les développeurs PHP et WordPress devraient au moins connaître, sinon utiliser.
Cette réponse a déjà la taille d'un livre pour permettre une discussion plus approfondie. Discuter de Composer ici est probablement hors sujet, elle n'a été mentionnée que par souci d'exhaustivité.
Pour plus d'informations, visitez le site Composer et lisez également ce minisite organisé par @Rarst .
1 PSR sont les règles standards de PHP publiées par le Framework PHP Interop Group
2 Composer (une bibliothèque qui sera mentionnée dans cette réponse) contient entre autres un utilitaire de chargement automatique.