Visión general
Este tutorial está escrito como un ejemplo completo sobre el desarrollo de Extras para MODX Revolution 2.3 y versiones posteriores, así como sobre cómo configurar tu Extra para que se pueda empaquetar fácilmente en un Paquete de Transporte, así como para poder desarrollarse fuera del raíz web de MODX para que un control de fuente (como Git) se puede usar.
Diseccionaremos el extra "Animales", un ejemplo simple que usa una tabla personalizada para almacenar objetos llamados "Animales", que tienen un nombre y una descripción. Tendremos un Snippet que los extraerá de la base de datos y mostrará una lista de ellos, maquetada gráficamente mediante un Chunk. Tendrá también una Página Personalizada en el Manager que usará ExtJS para tener una cuadrícula CRUD para editar, y un script de compilación para empaquetar el extra. Y lo haremos todo compatible con i18n para permitir una traducción fácil.
Este es un tutorial extremadamente completo, por lo que si solo deseas partes específicas, usa la Tabla de contenido siguiente para ir a la parte deseada.
El Extra Animales utilizado en este tutorial se puede encontrar en GitHub, en: https://github.com/guelu63/animales
Configurando los directorios
Puedes comenzar a desarrollar tu Extra de muchas maneras: puedes escribir tus plugins, snippets, etc. dentro de MODX y luego empaquetarlos con una herramienta de empaquetado como PackMan, o puedes desarrollar tu proyecto fuera del administrador MODX y gestionar tus archivos a través de un sistema de control de fuentes como Git. Este tutorial utiliza el último método por varias razones:
- Permite el desarrollo inmediato directamente desde un repositorio Git
- Permite una fácil colaboración entre los desarrolladores, se puede desarrollar en tu IDE preferido y luego, simplemente se realiza una configuración inicial de las rutas.
- Permite el aislamiento de tu código, y sea independiente del core de MODX, por lo que si necesita moverlo, puede hacerlo en un solo lugar.
Empieza por crear un directorio en el raíz de tu servidor local: /htdocs/animales/, asegurándote de que este directorio sea accesible localmente en la web, ya que lo necesitarás más adelante. Yo tengo /htdocs/ como raíz en mi entorno localhost, pero podría ser /www/, /html/, o cualquiera que sea en tu caso el directorio raíz de tu servidor local.
Puede que tengas que agregar una nueva variable en Configuración de Sistema de tu instalación MODX, llamada session_cookie_path y darle un valor de "/" (sin las comillas). Esto le indicará a MODX que use la misma sesión cuando esté ejecutando cosas en http://localhost/animales/. Además, también es una buena idea darle un nombre único a través de la variable session_name (por ejemplo "modxlocaldevsession"). Esto evitará conflictos con otras instalaciones de MODX que pudieras tener en tu servidor local. Si haces esto, vacía el directorio core/cache/ y vuelve a iniciar sesión después de hacerlo.
Crearemos los siguientes directorios:

Tengamos en cuenta algunas cosas. Los 3 directorios principales son core/, assets/ y _build/. Normalmente, los extras en MODX se separan en 2 directorios diferentes cuando se instalan: core/components/animales/ y assets/components/animales/. El motivo es que esto nos permite separar las partes específicas de la web (archivos JavaScript, CSS, imágenes, conectores, etc.) en una ubicación dentro del directorio raíz web, mientras que todos los archivos PHP (salvo los conectores deben permanecer accesibles) se colocan en core/components/ que puede ( y debería) estar fuera del directorio público web por razones de seguridad.
En nuestra estructura de desarrollo están imitando cómo estarán en la instalación de MODX después de que el Paquete de Transporte haya instalado nuestro extra.
El directorio _build/ no se coloca en el archivo zip del Paquete de Transporte. Está ahí como andamiaje, para ayudar en la construcción del Paquete de Transporte. Más sobre eso al final del tutorial.
Veamos más detalladamente cada directorio. En assets/, el único archivo no obvio es assets/components/animales/connector.php. Este archivo nos permitirá tener procesadores exclusivos para la página personalizada del Manager (CMP ó Custom Manager Page) que crearemos para manejar nuestro extra. Lo veremos más en detalle más tarde.
En el directorio core/components/animales/, tenemos algunos directorios que vale la pena explicar:
- controllers: Estos son los controladores para nuestra/s página/s CMP. Explicaremos en detalle más tarde.
- docs: Solo contiene un registro de cambios, un archivo Léeme y una licencia.
- elements: Todos nuestros snippets, chunks, plugins, etc.
- lexicon: Todos nuestros archivos de idioma i18n. Más sobre esto más tarde.
- model: Donde se encuentran todas nuestras clases, así como nuestro archivo de esquema XML para nuestras tablas personalizadas en la base de datos .
- processors: Todos los procesadores personalizados para nuestras páginas CMP.
- templates: Plantillas para nuestras páginas CMP.
Ten en cuenta que aunque este directorio debe ser accesible desde la web, está completamente separado del core de MODX. Es posible que desees instalar MODX en un subdirectorio y colocar su repositorio en un subdirectorio separado. Por ejemplo, yo instalé MODX dentro de /htdocs/modx/ y estoy desarrollando este extra dentro de /htdocs/animales/. El uso de directorios separados puede ayudar a aislarlo de cualquier "travesura" de Git o colocar accidentalmente archivos en el repositorio incorrecto. Siempre que tengas todo bien aislado, puede ejecutar "git init" y hacerte un repositorio Git fuera del directorio /htdocs/animales/ (o el que sea en tu caso). Y puedes "empujarlo" a git, sin tener que preocuparte (mencionaremos algunos archivos más adelante cuando hablemos sobre agregar un archivo .gitignore).
Ahí lo tenemos. Un entorno aislado de MODX para que podamos hacer un desarrollo separado y mantener una colaboración fluida. Sigamos.
Creando el snippet Animales
Continuaremos creando nuestro primer snippet:
/htdocs/animales/core/components/animales/elements/snippets/snippet.animales.php
Tendrás que crear un directorio snippets/ si aún no lo has hecho. Empecemos desde un archivo vacío agregando las siguientes líneas de código:
<?php
$anim = $modx->getService('animales','Animales',$modx->getOption('animales.core_path',null,$modx->getOption('core_path').'components/animales/').'model/animales/',$scriptProperties);
if (!($anim instanceof Animales)) return '';
¿Que es eso? Un poco de magia. Analicemos cada parte. En primer lugar, tenemos la llamada getService. Es una notación abreviada, así que dividámoslo un poco para que sea más fácil de leer:
$defaultAnimalesCorePath = $modx->getOption('core_path').'components/animales/';
$animalesCorePath = $modx->getOption('animales.core_path',null,$defaultAnimalesCorePath);
$anim = $modx->getService('animales','Animales',$animalesCorePath.'model/animales/',$scriptProperties);
Bien, primero, ¿qué es $modx->getOption? Es un método que toma una variable de Configuración de Sistema por su clave (el primer parámetro). En la primera línea, estamos tomando una ruta 'predeterminada', estamos asumiendo cual será nuestra ruta a Animales, al prefijarle la ruta del core de MODX. En mi caso será: /htdocs/modx/core/components/animales/.
A continuación, lo pasaremos como un valor de reserva (fallback) para la próxima llamada getOption. Esta pasa 3 parámetros: una clave, llamada "animales.core_path", null, y nuestra ruta predeterminada que acabamos de asignar. En getOption, el segundo parámetro es un array para buscar la clave (lo que no estamos haciendo, por lo tanto podemos establecerla como nula), y el tercer parámetro es un valor predeterminado si no se encuentra la clave.
Todo este "alboroto" es necesario porque estamos desarrollando nuestro código en un lugar, pero cuando se implemente, existirá en otro. No podemos simplemente hacer referencia a las rutas relativas a MODX porque no estamos desarrollando dentro del núcleo de MODX. Explicaremos esto más en la siguiente sección.
Bien, por ahora, nuestra segunda línea devolverá /htdocs/modx/core/components/animales/. Pero ese no es la ruta principal de nuestro Animales. (el nuestro está en: /htdocs/animales/core/components/animales). Tenemos que decirle como encontrarlo allí. ¿Asi que qué hacemos?
Configuración de la ruta
- doodles.core_path : /htdocs/animales/core/components/animales/
- doodles.assets_url : /animales/assets/components/animales/
Si necesitas cambiar cualquiera de ellas para adaptarlas a las rutas en tu entorno, házlo. Ahora nuestra primera línea devolverá: /htdocs/animales/core/components/animales/ ¡Bingo! Estupendo, ¿no?
¿Por qué hacemos esto? ¿Por qué no simplemente consultar /htdocs/animales/core/components/animales/? Porque eso no funcionaría en la instalación de otra persona. Es muy probable que esté en RUTA_MODX/core/components/animales/. Nuestro Paquete de Transporte (más adelante) se encargará de todas esas cosas de la ruta dinámica, pero queremos agregar una modificación para permitirnos desarrollar Animales fuera de la ruta de MODX. Y acabamos de hacerlo.
Vayamos ahora a la tercera línea:
$anim = $modx->getService('animales','Animales',$animalesCorePath.'model/animales/',$scriptProperties);
De acuerdo, esto parece que se "desmadra". $modx->getService carga una clase y crea un objeto instancia de la misma, si existe, y lo establece en $modx->animales en este caso (el primer parámetro pasado). Puedes encontrar más información sobre getService aquí.
¡Pero espera! Aún no tenemos la clase Animales, y es hora de crearla.
Creando la clase base Animales
<?php
class Animales {
public $modx;
public $config = array();
public function __construct(modX &$modx,array $config = array()) {
$this->modx =& $modx;
$basePath = $this->modx->getOption('animales.core_path',$config,$this->modx->getOption('core_path').'components/animales/');
$assetsUrl = $this->modx->getOption('animales.assets_url',$config,$this->modx->getOption('assets_url').'components/animales/');
$this->config = array_merge(array(
'basePath' => $basePath,
'corePath' => $basePath,
'modelPath' => $basePath.'model/',
'processorsPath' => $basePath.'processors/',
'templatesPath' => $basePath.'templates/',
'chunksPath' => $basePath.'elements/chunks/',
'jsUrl' => $assetsUrl.'js/',
'cssUrl' => $assetsUrl.'css/',
'assetsUrl' => $assetsUrl,
'connectorUrl' => $assetsUrl.'connector.php',
),$config);
}
}
¡Excelente! Bastante sencillo por ahora: simplemente creamos la clase, con un constructor que establece una referencia al objeto modX en $animales->modx. Esto nos será útil más tarde. Además, llena algunas rutas básicas, que podremos usar más adelante, en el array $animales->config, y lo hace con nuestro elegante truco de Configuración de Sistema para que podamos enfocarlo a nuestra ruta /htdocs/animales/.
Ahora, volvamos a nuestro snippet. Avancemos agregando algunas propiedades predeterminadas al mismo, a continuación de las líneas que ya teníamos, para que se vea así:
$anim = $modx->getService('animales','Animales',$modx->getOption('animales.core_path',null,$modx->getOption('core_path').'components/animales/').'model/animales/',$scriptProperties);
if (!($anim instanceof Animales)) return '';
/* setup default properties */
$tpl = $modx->getOption('tpl',$scriptProperties,'rowTpl');
$sort = $modx->getOption('sort',$scriptProperties,'name');
$dir = $modx->getOption('dir',$scriptProperties,'ASC');
$output = '';
return $output;
Bien. Ahora queremos usar xPDO para consultar la base de datos para obtener nuestros registros. Pero todavía no hemos creado un modelo xPDO para ellos. Tenemos que hacerlo.
Creando el Modelo
Los métodos de consulta para acceder a la base de datos proporcionados por xPDO , actualmente soportan múltiples bases de datos, y lo hacen mediante la abstracción de consultas DB. Además, permiten mantener tus archivos de base de datos en clases elegantes y limpias, y hacer todo tipo de operaciones ordenadas en líneas de código muy cortas. Pero para hacer eso, tenemos que agregar un modelo xPDO a nuestro snippet (a través del método $modx->addPackage). Primero tenemos que construir el modelo, usando un esquema xPDO. Puedes ver un tutorial largo y agradable sobre cómo hacerlo aquí, pero lo haremos rápidamente por ahora.
Continúa creando un archivo xml en /htdocs/animales/core/components/animales/model/schema/animales.mysql.schema.xml, y pon lo sigiente en él:
<?xml version="1.0" encoding="UTF-8"?>
<model package="animales" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM" version="1.0">
<object class="Animal" table="animales" extends="xPDOSimpleObject">
<field key="name" dbtype="varchar" precision="255" phptype="string" null="false" default=""/>
<field key="description" dbtype="text" phptype="string" null="false" default=""/>
<field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
<field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
<field key="editedon" dbtype="datetime" phptype="datetime" null="true"/>
<field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
<aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/>
<aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>
</object>
</model>
Bueno, hay muchas cosas aquí. Si es la primera vez que tratas con xPDO o no estás familiarizado con el cómo y el por qué de sus archivos de esquema XML, es posible que quieras revisar más ejemplos de archivos de esquema XML xPDO.
Primero, la primera línea:
<model package="animales" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM" version="1.0">
Esta le dice al esquema que nuestro paquete xPDO se llama 'animales'. Y es al que nos referiremos en nuestra llamada addPackage(). Además, dice que la clase padre para todos los objetos definidos aquí es "xPDOObject", y que este esquema está hecho para MySQL. Y finalmente, establece un motor MySQL predeterminado a MyISAM.
Seguimos.
<object class="Animal" table="animales" extends="xPDOSimpleObject">
Un "objeto" en un esquema xPDO es, básicamente, una tabla de la base de datos. Esta línea está diciendo: asígnese a xPDO un nombre para la tabla llamada '{tableprefix} _animales'. Suponiendo que el prefijo de tabla que hizo en su instalación MODX sea 'modx', se traduciría a 'modx_animales'. Luego dice que extiende "xPDOSimpleObject". xPDOObject es la clase padre para cualquier clase de tabla xPDO. xPDOSimpleObject lo extiende, agregando un pequeño campo de incremento automático "id" a la tabla. Y, como vamos a necesitar un campo "id" en nuestra tabla, usamos xPDOSimpleObject.
<field key="name" dbtype="varchar" precision="255" phptype="string" null="false" default=""/>
<field key="description" dbtype="text" phptype="string" null="false" default=""/>
<field key="createdon" dbtype="datetime" phptype="datetime" null="true"/>
<field key="createdby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
<field key="editedon" dbtype="datetime" phptype="datetime" null="true"/>
<field key="editedby" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" />
Todos estos campos se explican por sí mismos: son campos en la tabla de la base de datos.
Pasemos a las dos últimas líneas:
<aggregate alias="CreatedBy" class="modUser" local="createdby" foreign="id" cardinality="one" owner="foreign"/>
<aggregate alias="EditedBy" class="modUser" local="editedby" foreign="id" cardinality="one" owner="foreign"/>
Aquí es donde entran los objetos relacionados con xPDO. Para los propósitos de este tutorial, solo ten en cuenta que esto le dice a xPDO que el campo createdby se asigna a un modUser, y el campo editedby se asigna a otro modUser.
Ahora vamos a analizar ese archivo xml y crear nuestras clases y mapas.
El script analizador del esquema
<?php
require_once dirname(__FILE__).'/build.config.php';
include_once MODX_CORE_PATH . 'model/modx/modx.class.php';
$modx= new modX();
$modx->initialize('mgr');
$modx->loadClass('transport.modPackageBuilder','',false, true);
$modx->setLogLevel(modX::LOG_LEVEL_INFO);
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');
$sources = array(
'model' => $modx->getOption('animales.core_path').'model/',
'schema_file' => $modx->getOption('animales.core_path').'model/schema/animales.mysql.schema.xml'
);
$manager= $modx->getManager();
$generator= $manager->getGenerator();
if (!is_dir($sources['model'])) { $modx->log(modX::LOG_LEVEL_ERROR,'Model directory not found!'); die(); }
if (!file_exists($sources['schema_file'])) { $modx->log(modX::LOG_LEVEL_ERROR,'Schema file not found!'); die(); }
$generator->parseSchema($sources['schema_file'],$sources['model']);
$modx->addPackage('animales', $sources['model']); // añadimos el paquete para crear los modelos disponibles
$manager->createObjectContainer('Animal'); // creamos la tabla en la base de datos
$modx->log(modX::LOG_LEVEL_INFO, 'Done!');
Básicamente, este archivo analiza nuestro archivo de esquema XML y crea clases y mapas xPDO (representaciones PHP de ese archivo XML) para nuestro extra. Volveremos sobre ello, pero antes que nada, observar que no funcionará, ya que morirá al buscar un archivo /htdocs/doodles/_build/build.config.php. Así que creémoslo para que esto no suceda.
<?php
define('MODX_BASE_PATH', '/htdocs/modx/');
define('MODX_CORE_PATH', MODX_BASE_PATH . 'core/');
define('MODX_MANAGER_PATH', MODX_BASE_PATH . 'manager/');
define('MODX_CONNECTORS_PATH', MODX_BASE_PATH . 'connectors/');
define('MODX_ASSETS_PATH', MODX_BASE_PATH . 'assets/');
define('MODX_BASE_URL','/modx/');
/* define('MODX_CORE_URL', MODX_BASE_URL . 'core/'); */ /* No hay una URL para el core! */
define('MODX_MANAGER_URL', MODX_BASE_URL . 'manager/');
define('MODX_CONNECTORS_URL', MODX_BASE_URL . 'connectors/');
define('MODX_ASSETS_URL', MODX_BASE_URL . 'assets/');
Obviamente, es posible que tengas que cambiar esas rutas a donde sea que esté tu instalación MODX.
Ahora, puedes ir a tu archivo _build/build.schema.php y ejecutarlo. Lo puedes hacer cargandolo en un navegador web la URL http://localhost/animales/_build/build.schema.php. Es posible que necesites cambiar esa URL a donde hayas hecho que el directorio de animales sea accesible por la web (si seguiste los pasos, ya estará hecho. Si no, ahora es buen momento para hacerlo).
El script debería ejecutarse y generarte algunos archivos de clases y mapas muy bonitos.
¡Bien! Acabas de crear nuestros mapas y clases. Vamos a hacer un ajuste a nuestra clase base Animales, para que se agregue automáticamente el paquete Animales xPDO cada vez que cargamos la clase. Añade esta línea después de la parte $this->config = array_merge, al final del constructor:
<?php
class Animales {
public $modx;
public $config = array();
public function __construct(modX &$modx,array $config = array()) {
$this->modx =& $modx;
$basePath = $this->modx->getOption('animales.core_path',$config,$this->modx->getOption('core_path').'components/animales/');
$assetsUrl = $this->modx->getOption('animales.assets_url',$config,$this->modx->getOption('assets_url').'components/animales/');
$this->config = array_merge(array(
'basePath' => $basePath,
'corePath' => $basePath,
'modelPath' => $basePath.'model/',
'processorsPath' => $basePath.'processors/',
'templatesPath' => $basePath.'templates/',
'chunksPath' => $basePath.'elements/chunks/',
'jsUrl' => $assetsUrl.'js/',
'cssUrl' => $assetsUrl.'css/',
'assetsUrl' => $assetsUrl,
'connectorUrl' => $assetsUrl.'connector.php',
),$config);
// añade esto que sigue
$this->modx->addPackage('animales',$this->config['modelPath']);
}
}
Esto le dice a xPDO que queremos agregar el paquete xPDO 'animales', lo que nos permitirá consultar esa tabla personalizada. ¡Excelente!