Arrivando qui esattamente 2 anni dopo la domanda iniziale, ci sono alcune cose che voglio sottolineare. (Non chiedermi di sottolineare molte cose , mai).
Gancio corretto
Per creare un'istanza di una classe di plug-in, è necessario utilizzare l'hook corretto . Non esiste una regola generale per cui lo sia, perché dipende da ciò che fa la classe.
L'uso di un hook molto precoce come "plugins_loaded"
spesso non ha senso perché un hook del genere viene attivato per richieste di amministrazione, frontend e AJAX, ma molto spesso un hook successivo è molto meglio perché consente di istanziare le classi di plugin solo quando necessario.
Ad esempio una classe che fa cose per i template può essere istanziata "template_redirect"
.
In generale è molto raro che una classe debba essere istanziata prima che "wp_loaded"
sia stata licenziata.
Nessuna classe di Dio
Soprattutto le classi usate come esempi nelle risposte più vecchie usano una classe chiamata like "Prefix_Example_Plugin"
o "My_Plugin"
... Ciò indica che probabilmente esiste una classe principale per il plugin.
Bene, a meno che un plug-in non sia creato da una singola classe (nel qual caso nominarlo con il nome del plug-in è assolutamente ragionevole), per creare una classe che gestisca l'intero plug-in (ad esempio aggiungendo tutti gli hook necessari a un plug-in o creando un'istanza di tutte le altre classi di plug-in ) può essere considerata una cattiva pratica, come esempio di un oggetto divino .
Nel codice di programmazione orientata agli oggetti dovrebbe tendere ad essere SOLIDO dove la "S" sta per "Principio di responsabilità singola" .
Significa che ogni classe dovrebbe fare una sola cosa. Nello sviluppo di plug-in WordPress significa che gli sviluppatori dovrebbero evitare di utilizzare un singolo hook per creare un'istanza di una classe principale di plug-in, ma diversi hook dovrebbero essere utilizzati per creare un'istanza di classi diverse, in base alla responsabilità della classe.
Evitare ganci nel costruttore
Questo argomento è stato introdotto in altre risposte qui, tuttavia voglio sottolineare questo concetto e collegare quest'altra risposta in cui è stata spiegata in modo abbastanza ampio nell'ambito del test unitario.
Quasi 2015: PHP 5.2 è per gli zombi
Dal 14 agosto 2014, PHP 5.3 ha raggiunto la fine della sua vita . È decisamente morto. PHP 5.4 sarà supportato per tutto il 2015, significa che per un altro anno al momento sto scrivendo.
Tuttavia, WordPress supporta ancora PHP 5.2, ma nessuno dovrebbe scrivere una singola riga di codice che supporti quella versione, soprattutto se il codice è OOP.
Ci sono diversi motivi:
- PHP 5.2 è morto molto tempo fa, non sono state rilasciate correzioni di sicurezza, il che significa che non è sicuro
- PHP 5.3 ha aggiunto un sacco di funzioni di PHP, funzioni anonime e spazi dei nomi über alles
- le nuove versioni di PHP sono molto più veloci . PHP è gratuito. L'aggiornamento è gratuito. Perché utilizzare una versione più lenta e insicura se è possibile utilizzarne una più veloce e sicura gratuitamente?
Se non vuoi usare il codice PHP 5.4+, usa almeno 5.3+
Esempio
A questo punto è tempo di rivedere le risposte più vecchie in base a ciò che ho detto fino a qui.
Una volta che non ci dobbiamo più preoccupare di 5.2, possiamo e dovremmo usare gli spazi dei nomi.
Per meglio spiegare il principio della responsabilità singola, il mio esempio utilizzerà 3 classi, una che fa qualcosa sul frontend, una sul backend e una terza usata in entrambi i casi.
Classe amministratore:
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
}
}
Interfaccia strumenti:
namespace GM\WPSE\Example;
interface ToolsInterface {
function doSomething();
}
E una classe di strumenti, utilizzata dalle altre due:
namespace GM\WPSE\Example;
class Tools implements ToolsInterface {
function doSomething() {
return 'done';
}
}
Avendo queste classi, posso istanziarle usando gli hook appropriati. Qualcosa di simile a:
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 );
} );
Dipendenza Inversione e Dipendenza Iniezione
Nell'esempio sopra ho usato spazi dei nomi e funzioni anonime per creare un'istanza di classi diverse con diversi hook, mettendo in pratica ciò che ho detto sopra.
Nota come gli spazi dei nomi consentono di creare classi denominate senza alcun prefisso.
Ho applicato un altro concetto che è stato indirettamente menzionato sopra: Iniezione di dipendenza , è un metodo per applicare il principio di inversione di dipendenza , la "D" nell'acronimo SOLID.
La Tools
classe viene "iniettata" nelle altre due classi quando vengono istanziate, quindi in questo modo è possibile separare la responsabilità.
Inoltre, AdminStuff
e le FrontStuff
classi usano il tipo hinting per dichiarare che hanno bisogno di una classe che implementa ToolsInterface
.
In questo modo noi stessi o gli utenti che utilizzano il nostro codice possono utilizzare diverse implementazioni della stessa interfaccia, rendendo il nostro codice non accoppiato a una classe concreta ma a un'astrazione: questo è esattamente ciò che riguarda il principio di inversione di dipendenza.
Tuttavia, l'esempio sopra può essere ulteriormente migliorato. Vediamo come.
Autoloader
Un buon modo per scrivere codice OOP meglio leggibile è non mescolare la definizione dei tipi (Interfacce, Classi) con altri codici e mettere ogni tipo nel proprio file.
Questa regola è anche uno degli standard di codifica PSR-1 1 .
Tuttavia, prima di poter utilizzare una classe, è necessario richiedere il file che la contiene.
Questo può essere travolgente, ma PHP fornisce funzioni di utilità per caricare automaticamente una classe quando è necessario, usando un callback che carica un file in base al suo nome.
L'uso degli spazi dei nomi diventa molto semplice, perché ora è possibile abbinare la struttura delle cartelle alla struttura dello spazio dei nomi.
Questo non è solo possibile, ma è anche un altro standard PSR (o meglio 2: PSR-0 ora deprecato e PSR-4 ).
Seguendo tali standard è possibile utilizzare diversi strumenti che gestiscono il caricamento automatico, senza dover codificare un caricatore automatico personalizzato.
Devo dire che gli standard di codifica di WordPress hanno regole diverse per la denominazione dei file.
Quindi quando scrivono codice per il core di WordPress, gli sviluppatori devono seguire le regole del WP, ma quando scrivono codice personalizzato è una scelta dello sviluppatore, ma usare lo standard PSR è più facile usare gli strumenti già scritti 2 .
Schemi di localizzazione di accesso globale, registro e servizi.
Uno dei maggiori problemi nell'istanza di classi di plugin in WordPress è come accedervi da varie parti del codice.
WordPress stesso utilizza l' approccio globale : le variabili vengono salvate in ambito globale, rendendole accessibili ovunque. Ogni sviluppatore WP digita la parola global
migliaia di volte nella sua carriera.
Questo è anche l'approccio che ho usato per l'esempio sopra, ma è malvagio .
Questa risposta è già troppo lunga per permettermi di spiegare ulteriormente perché, ma leggere i primi risultati nella SERP per "variabili globali malvagie" è un buon punto di partenza.
Ma come è possibile evitare le variabili globali?
Esistono diversi modi.
Alcune delle risposte precedenti qui usano l' approccio dell'istanza statica .
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self;
}
return self::$instance;
}
È facile e abbastanza bene, ma forza l'implementazione del modello per ogni classe a cui vogliamo accedere.
Inoltre, molte volte questo approccio apre la strada al problema della classe divina, perché gli sviluppatori rendono accessibile una classe principale usando questo metodo e quindi la usano per accedere a tutte le altre classi.
Ho già spiegato quanto sia cattiva una classe god, quindi l'approccio dell'istanza statica è un buon modo per andare quando un plugin deve solo rendere accessibili una o due classi.
Questo non significa che può essere utilizzato solo per plugin che hanno solo un paio di classi, infatti, quando il principio dell'iniezione di dipendenza viene usato correttamente, è possibile creare applicazioni piuttosto complesse senza la necessità di rendere accessibile a livello globale un gran numero di oggetti.
Tuttavia, a volte i plugin devono rendere accessibili alcune classi e in tal caso l'approccio dell'istanza statica è schiacciante.
Un altro possibile approccio consiste nell'utilizzare il modello di registro .
Questa è una sua implementazione molto semplice:
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;
}
}
Utilizzando questa classe è possibile archiviare oggetti nell'oggetto Registro di sistema tramite un ID, quindi avere accesso a un registro è possibile avere accesso a tutti gli oggetti. Naturalmente quando un oggetto viene creato per la prima volta, deve essere aggiunto al registro.
Esempio:
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'esempio sopra chiarisce che per essere utile il registro deve essere accessibile a livello globale. Una variabile globale per l'unico registro non è molto male, tuttavia per i puristi non globali è possibile implementare l'approccio dell'istanza statica per un registro, o forse una funzione con una variabile statica:
function gm_wpse_example_registry() {
static $registry = NULL;
if ( is_null( $registry ) ) {
$registry = new GM\WPSE\Example\Registry;
}
return $registry;
}
La prima volta che viene chiamata la funzione, verrà creata un'istanza del registro, nelle chiamate successive verrà restituita.
Un altro metodo specifico di WordPress per rendere accessibile una classe a livello globale è la restituzione di un'istanza di oggetto da un filtro. Qualcosa come questo:
$registry = new GM\WPSE\Example\Registry;
add_filter( 'gm_wpse_example_registry', function() use( $registry ) {
return $registry;
} );
Dopodiché è necessario il registro ovunque:
$registry = apply_filters( 'gm_wpse_example_registry', NULL );
Un altro modello che può essere utilizzato è il modello di localizzazione di servizio . È simile al modello di registro, ma i localizzatori di servizio vengono passati a varie classi mediante l'iniezione delle dipendenze.
Il problema principale con questo modello è che nasconde le dipendenze delle classi rendendo il codice più difficile da mantenere e leggere.
DI Containers
Indipendentemente dal metodo utilizzato per rendere accessibile a livello globale il registro o il localizzatore di servizi, gli oggetti devono essere archiviati lì e prima di essere archiviati devono essere istanziati.
In applicazioni complesse, dove ci sono molte classi e molte di esse hanno diverse dipendenze, la creazione di un'istanza di classi richiede molto codice, quindi aumenta la possibilità di bug: il codice che non esiste non può avere bug.
Negli ultimi anni sono apparse alcune librerie PHP che aiutano gli sviluppatori PHP a creare istanze e archiviare facilmente istanze di oggetti, risolvendo automaticamente le loro dipendenze.
Queste librerie sono note come contenitori di iniezione di dipendenza perché sono in grado di creare istanze di classi per risolvere dipendenze e anche di archiviare oggetti e restituirli quando necessario, agendo in modo simile a un oggetto del registro.
Di solito, quando si utilizzano contenitori DI, gli sviluppatori devono impostare le dipendenze per ogni classe dell'applicazione, e quindi la prima volta che una classe è necessaria nel codice viene istanziata con dipendenze appropriate e la stessa istanza viene restituita più volte sulle richieste successive .
Alcuni contenitori DI sono anche in grado di rilevare automaticamente dipendenze senza configurazione, ma utilizzando la riflessione PHP .
Alcuni noti contenitori DI sono:
e molti altri.
Voglio sottolineare che per i plugin semplici, che coinvolgono solo poche classi e che le classi non hanno molte dipendenze, probabilmente non vale la pena usare contenitori DI: il metodo dell'istanza statica o un registro accessibile globale sono buone soluzioni, ma per plugin complessi il vantaggio di un contenitore DI diventa evidente.
Naturalmente, anche gli oggetti contenitore DI devono essere accessibili per essere utilizzati nell'applicazione e a tale scopo è possibile utilizzare uno dei metodi visti sopra, variabile globale, variabile di istanza statica, oggetto di ritorno tramite filtro e così via.
Compositore
Usare un contenitore DI spesso significa usare un codice di terze parti. Al giorno d'oggi, in PHP, quando abbiamo bisogno di usare una libreria esterna (quindi non solo contenitori DI, ma qualsiasi codice che non fa parte dell'applicazione), il semplice scaricarlo e metterlo nella nostra cartella dell'applicazione non è considerato una buona pratica. Anche se siamo gli autori di quell'altro pezzo di codice.
Il disaccoppiamento di un codice applicazione da dipendenze esterne è indice di una migliore organizzazione, migliore affidabilità e migliore sanità mentale del codice.
Compositore , è lo standard di fatto nella comunità PHP per gestire le dipendenze PHP. Lontano da essere anche mainstream nella comunità WP, è uno strumento che ogni sviluppatore di PHP e WordPress dovrebbe almeno conoscere, se non usare.
Questa risposta è già a misura di libro per consentire ulteriori discussioni, e anche discutere di Composer qui è probabilmente fuori tema, è stato menzionato solo per completezza.
Per maggiori informazioni visita il sito di Composer e vale anche la pena dare una lettura a questo minisito a cura di @Rarst .
1 PSR sono norme standard PHP rilasciate dal PHP Framework Interop Group
2 Composer (una libreria che verrà menzionata in questa risposta) contiene tra l'altro anche un'utilità di caricamento automatico.