Al llegar aquí exactamente 2 años después de la pregunta original, hay algunas cosas que quiero señalar. (No me pidas que señale muchas cosas , nunca).
Gancho adecuado
Para crear una instancia de una clase de complemento, se debe usar el gancho adecuado . No hay una regla general para lo que es, porque depende de lo que haga la clase.
Usar un gancho muy temprano a "plugins_loaded"
menudo no tiene sentido porque un gancho como ese se dispara para solicitudes de administrador, frontend y AJAX, pero muy a menudo un gancho posterior es mucho mejor porque permite instanciar clases de complementos solo cuando es necesario.
Por ejemplo, se puede crear una instancia de una clase que hace cosas para plantillas "template_redirect"
.
En términos generales, es muy raro que una clase necesite ser instanciada antes de "wp_loaded"
ser despedida.
No hay clase de Dios
La mayoría de todas las clases usadas como ejemplos en respuestas anteriores usan una clase llamada como "Prefix_Example_Plugin"
o "My_Plugin"
... Esto indica que probablemente haya una clase principal para el complemento.
Bueno, a menos que un complemento esté hecho por una sola clase (en cuyo caso nombrarlo después del nombre del complemento es absolutamente razonable), crear una clase que administre todo el complemento (por ejemplo, agregar todos los ganchos que necesita un complemento o crear instancias de todas las otras clases de complemento ) puede considerarse una mala práctica, como un ejemplo de un objeto dios .
En la programación orientada a objetos, el código debe tender a ser SÓLIDO donde la "S" significa "Principio de responsabilidad única" .
Significa que cada clase debe hacer una sola cosa. En el desarrollo de plugins de WordPress significa que los desarrolladores deben evitar usar un solo gancho para crear una instancia de una clase de complemento principal , pero se deben usar diferentes ganchos para crear instancias de diferentes clases, de acuerdo con la responsabilidad de la clase.
Evitar ganchos en constructor
Este argumento se ha introducido en otras respuestas aquí, sin embargo, quiero comentar este concepto y vincular esta otra respuesta donde se ha explicado ampliamente en el ámbito de las pruebas unitarias.
Casi 2015: PHP 5.2 es para zombies
Desde el 14 de agosto de 2014, PHP 5.3 llegó a su fin . Definitivamente está muerto. PHP 5.4 será compatible para todo 2015, significa un año más en el momento en que estoy escribiendo.
Sin embargo, WordPress aún admite PHP 5.2, pero nadie debería escribir una sola línea de código que admita esa versión, especialmente si el código es OOP.
Hay diferentes razones:
- PHP 5.2 murió hace mucho tiempo, no se lanzaron correcciones de seguridad, lo que significa que no es seguro
- PHP 5.3 agregó muchas características a PHP, funciones anónimas y espacios de nombres über alles
- Las versiones más nuevas de PHP son mucho más rápidas . PHP es gratis. Actualizarlo es gratis. ¿Por qué usar una versión más lenta e insegura si puedes usar una más rápida y segura de forma gratuita?
Si no desea usar el código PHP 5.4+, use al menos 5.3+
Ejemplo
En este punto, es hora de revisar las respuestas anteriores basadas en lo que dije hasta aquí.
Una vez que ya no tengamos que preocuparnos por 5.2, podemos y debemos usar espacios de nombres.
En aras de una mejor explicación del principio de responsabilidad única, mi ejemplo utilizará 3 clases, una que hace algo en el frontend, una en el backend y una tercera utilizada en ambos casos.
Clase de administrador:
namespace GM\WPSE\Example;
class AdminStuff {
private $tools;
function __construct( ToolsInterface $tools ) {
$this->tools = $tools;
}
function setup() {
// setup class, maybe add hooks
}
}
Clase frontend:
namespace GM\WPSE\Example;
class FrontStuff {
private $tools;
function __construct( ToolsInterface $tools ) {
$this->tools = $tools;
}
function setup() {
// setup class, maybe add hooks
}
}
Interfaz de herramientas:
namespace GM\WPSE\Example;
interface ToolsInterface {
function doSomething();
}
Y una clase de Herramientas, utilizada por los otros dos:
namespace GM\WPSE\Example;
class Tools implements ToolsInterface {
function doSomething() {
return 'done';
}
}
Al tener estas clases, puedo crear instancias con los ganchos adecuados. Algo como:
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 );
} );
Inversión de dependencia e inyección de dependencia
En el ejemplo anterior, utilicé espacios de nombres y funciones anónimas para crear instancias de diferentes clases en diferentes ganchos, poniendo en práctica lo que dije anteriormente.
Observe cómo los espacios de nombres permiten crear clases nombradas sin ningún prefijo.
Apliqué otro concepto que se mencionó indirectamente anteriormente: Inyección de dependencias , es un método para aplicar el Principio de inversión de dependencias , la "D" en acrónimo SÓLIDO.
La Tools
clase se "inyecta" en las otras dos clases cuando se instancian, por lo que de esta manera es posible separar la responsabilidad.
Además, AdminStuff
y FrontStuff
clases utilizan tipo dando a entender que declarar que necesitan una clase que implementa ToolsInterface
.
De esta manera, nosotros o los usuarios que usan nuestro código pueden usar diferentes implementaciones de la misma interfaz, haciendo que nuestro código no esté acoplado a una clase concreta sino a una abstracción: de eso se trata exactamente el Principio de Inversión de Dependencia.
Sin embargo, el ejemplo anterior se puede mejorar aún más. A ver cómo.
Cargador automático
Una buena manera de escribir un código OOP mejor legible es no mezclar la definición de tipos (Interfaces, Clases) con otro código y colocar cada tipo en su propio archivo.
Esta regla también es uno de los estándares de codificación PSR-1 1 .
Sin embargo, al hacerlo, antes de poder usar una clase, se necesita el archivo que la contiene.
Esto puede ser abrumador, pero PHP proporciona funciones de utilidad para cargar automáticamente una clase cuando es necesario, utilizando una devolución de llamada que carga un archivo en función de su nombre.
Usar espacios de nombres se vuelve muy fácil, porque ahora es posible hacer coincidir la estructura de la carpeta con la estructura del espacio de nombres.
Eso no solo es posible, sino que también es otro estándar de PSR (o mejor 2: PSR-0 ahora en desuso y PSR-4 ).
Siguiendo esos estándares, es posible utilizar diferentes herramientas que manejan la carga automática, sin tener que codificar un cargador automático personalizado.
Tengo que decir que los estándares de codificación de WordPress tienen diferentes reglas para nombrar archivos.
Entonces, al escribir código para el núcleo de WordPress, los desarrolladores deben seguir las reglas de WP, pero al escribir código personalizado es una elección del desarrollador, pero usar el estándar PSR es más fácil de usar herramientas ya escritas 2 .
Patrones de acceso global, registro y localizador de servicios.
Uno de los mayores problemas al crear instancias de clases de complementos en WordPress es cómo acceder a ellas desde varias partes del código.
WordPress en sí usa el enfoque global : las variables se guardan en un alcance global, haciéndolas accesibles en todas partes. Cada desarrollador de WP escribe la palabra global
miles de veces en su carrera.
Este es también el enfoque que utilicé para el ejemplo anterior, pero es malo .
Esta respuesta ya es demasiado larga para permitirme explicar más por qué, pero leer los primeros resultados en el SERP para "variables globales mal" es un buen punto de partida.
Pero, ¿cómo es posible evitar las variables globales?
Hay diferentes formas
Algunas de las respuestas anteriores aquí usan el enfoque de instancia estática .
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self;
}
return self::$instance;
}
Es fácil y bastante bueno, pero obliga a implementar el patrón para cada clase a la que queremos acceder.
Además, muchas veces este enfoque pone el camino para caer en el problema de la clase de dios, porque los desarrolladores hacen accesible una clase principal usando este método, y luego lo usan para acceder a todas las demás clases.
Ya expliqué lo mala que es una clase de dios, por lo que el enfoque de instancia estática es un buen camino cuando un complemento solo necesita hacer accesibles una o dos clases.
Esto no significa que se pueda usar solo para complementos que tengan solo un par de clases, de hecho, cuando el principio de inyección de dependencia se usa correctamente, es posible crear aplicaciones bastante complejas sin la necesidad de hacer que un número global sea accesible. de objetos.
Sin embargo, a veces los complementos deben hacer accesibles algunas clases, y en ese caso el enfoque de instancia estática es abrumador.
Otro enfoque posible es usar el patrón de registro .
Esta es una implementación muy 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;
}
}
Con esta clase, es posible almacenar objetos en el objeto de registro mediante una identificación, por lo que tener acceso a un registro es posible tener acceso a todos los objetos. Por supuesto, cuando se crea un objeto por primera vez, debe agregarse al registro.
Ejemplo:
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' ) );
El ejemplo anterior deja en claro que para ser útil, el registro debe ser accesible a nivel mundial. Una variable global para el registro único no es muy mala, sin embargo, para los puristas no globales es posible implementar el enfoque de instancia estática para un registro, o tal vez una función con una variable estática:
function gm_wpse_example_registry() {
static $registry = NULL;
if ( is_null( $registry ) ) {
$registry = new GM\WPSE\Example\Registry;
}
return $registry;
}
La primera vez que se llama a la función, creará una instancia del registro, en llamadas posteriores solo lo devolverá.
Otro método específico de WordPress para hacer que una clase sea accesible globalmente es devolver una instancia de objeto desde un filtro. Algo como esto:
$registry = new GM\WPSE\Example\Registry;
add_filter( 'gm_wpse_example_registry', function() use( $registry ) {
return $registry;
} );
Después de eso, en todas partes se necesita el registro:
$registry = apply_filters( 'gm_wpse_example_registry', NULL );
Otro patrón que se puede usar es el patrón de localización de servicios . Es similar al patrón de registro, pero los localizadores de servicios se pasan a varias clases mediante inyección de dependencia.
El principal problema con este patrón es que oculta las dependencias de las clases, lo que dificulta el mantenimiento y la lectura del código.
DI contenedores
No importa el método utilizado para hacer que el localizador de registro o servicio sea accesible globalmente, los objetos deben almacenarse allí, y antes de almacenarse deben ser instanciados.
En aplicaciones complejas, donde hay muchas clases y muchas de ellas tienen varias dependencias, la creación de instancias de clases requiere una gran cantidad de código, por lo que aumenta la posibilidad de errores: el código que no existe no puede tener errores.
En los últimos años aparecieron algunas bibliotecas PHP que ayudan a los desarrolladores PHP a crear instancias y almacenar fácilmente instancias de objetos, resolviendo automáticamente sus dependencias.
Estas bibliotecas se conocen como Contenedores de inyección de dependencias porque son capaces de crear instancias de clases que resuelven dependencias y también de almacenar objetos y devolverlos cuando sea necesario, actuando de manera similar a un objeto de registro.
Por lo general, cuando se usan contenedores DI, los desarrolladores deben configurar las dependencias para cada clase de la aplicación, y luego, la primera vez que se necesita una clase en el código, se instancia con las dependencias adecuadas y la misma instancia se devuelve una y otra vez en las solicitudes posteriores. .
Algunos contenedores DI también son capaces de descubrir dependencias automáticamente sin configuración, pero utilizando la reflexión PHP .
Algunos contenedores DI conocidos son:
y muchos otros.
Quiero señalar que para complementos simples, que involucran solo unas pocas clases y las clases no tienen muchas dependencias, probablemente no valga la pena usar contenedores DI: el método de instancia estática o un registro global accesible son buenas soluciones, pero para complementos complejos El beneficio de un contenedor DI se hace evidente.
Por supuesto, incluso los objetos del contenedor DI deben ser accesibles para ser utilizados en la aplicación y para ese propósito es posible usar uno de los métodos vistos anteriormente, variable global, variable de instancia estática, devolver el objeto a través del filtro, etc.
Compositor
Usar el contenedor DI a menudo significa usar código de terceros. Hoy en día, en PHP, cuando necesitamos usar una biblioteca externa (no solo contenedores DI, sino cualquier código que no sea parte de la aplicación), simplemente descargarlo y ponerlo en nuestra carpeta de aplicaciones no se considera una buena práctica. Incluso si somos los autores de esa otra pieza de código.
Desacoplar un código de aplicación de dependencias externas es señal de una mejor organización, una mayor confiabilidad y una mejor cordura del código.
Composer , es el estándar de facto en la comunidad PHP para administrar dependencias PHP. Lejos de ser una corriente principal en la comunidad WP también, es una herramienta que todo desarrollador de PHP y WordPress debería al menos saber, si no usar.
Esta respuesta ya tiene el tamaño de un libro para permitir una discusión más profunda, y también discutir sobre Composer aquí probablemente esté fuera de tema, solo se mencionó por razones de integridad.
Para obtener más información, visite el sitio de Composer y también vale la pena leer este minisitio curado por @Rarst .
1 PSR son reglas estándar de PHP publicadas por PHP Framework Interop Group
2 Composer (una biblioteca que se mencionará en esta respuesta), entre otras cosas, también contiene una utilidad de autocargador.