This site might not be up to date. New documentation sites are being created: for developers and for users.

The content of this page was written for the no longer supported Joomla version 3.x or earlier and will no longer be updated.

Although not all information may be out of date, it is strongly recommended that the pages for the current Joomla version be visited.

J3.x

Desarrollo de un Componente MVC/Agregar Niveles

From Joomla! Documentation

This page is a translated version of the page J3.x:Developing an MVC Component/Adding Levels and the translation is 100% complete.
Joomla! 
3.x
Tutorial
Desarrollo de un Componente MVC

Agregar una variable de petición en el tipo de menú

Utilizando la base de datos

Lado servidor básico

Agregar gestión de idioma

Agregar acciones del lado servidor

Agregar decoraciones del lado servidor

Agregar verificaciones

Agregar categorías

Agregar configuración

  1. Agregar ACL

Agregar un archivo de secuencia de comandos instalar-desinstalar-actualizar

Agregar un formulario del lado cliente

  1. Agregar una imagen
  2. Agregar un mapa
  3. Agregar AJAX
  4. Agregar un alias

Usar la facilidad filtro de idioma

  1. Agregar una Modal
  2. Agregar Asociaciones
  3. Agregar Comprobación
  4. Agregar Ordenamiento
  5. Agregar Niveles
  6. Agregar Control de Versiones
  7. Agregar Etiquetas
  8. Agregar Accesos
  9. Agregar procesos por lote
  10. Agregar Caché
  11. Agregar un Canal de Noticias

Agregar un servidor de actualización

  1. Agregar campos personalizados
  2. Upgrading to Joomla4



Esta es una serie multi-artículos de tutoriales sobre cómo desarrollar un Componente Modelo-Vista-Controlador para Joomla! VersiónJoomla 3.x.

Comenzar con la Introducción, y navegar por los artículos de esta serie usando el botón de navegación en la parte inferior o en el cuadro de la derecha (los "Artículos de esta serie").



Este artículo es parte del tutorial Desarrollo de un Componente MVC para Joomla! 3.2. Te invitamos a leer las partes anteriores del tutorial antes de leer esto.

En este paso, agregamos niveles a nuestro componente helloworld, lo que básicamente significa que estamos implementando una estructura de árbol en nuestros registros de helloworld.

Puede ver un vídeo (en inglés) complementario en Agregar niveles.

Introducción

Joomla implementa elementos del menú y categorías como estructuras arbóreas. Esto significa, por ejemplo, que los elementos del menú en un menú pueden tener submenús fuera de ellos (como se puede ver en el menú de administración de Joomla) y esos subelementos del menú a su vez pueden tener submenús fuera de ellos, y así sucesivamente. Los submenús están ordenados, por lo que puede definir en qué orden aparecen los subelementos del menú. Alguna terminología:

  1. cada uno de nuestros registros es un nodo dentro de la estructura de árbol
  2. hay un nodo raíz que ocupa la posición más alta en la jerarquía; dentro de Joomla definimos este nodo raíz como nivel 0
  3. el nodo raíz tiene varios hijos, y estos estarán en el nivel 1. Cada uno de los padres de estos hijos es el nodo raíz
  4. estos niños a su vez pueden tener sus propios hijos, en el nivel 2, y así sucesivamente.
Nested Set Model
Nested Set Model
Node Left Right
Clothing 1 22
Men's 2 9
Suits 3 8
Slacks 4 5
Jackets 6 7
Women's 10 21
Dresses 11 16
Evening Gowns 12 13
Sun Dresses 14 15
Skirts 17 18
Blouses 19 20

Para implementar esta estructura de árbol dentro de una base de datos relacional, Joomla usa el Nested Set Model (texto en inglés). Esto implica asignar a cada registro un campo izquierdo (en Joomla lft) y derecho (en Joomla rgt), como se muestra en el diagrama y la tabla asociada. Deberías poder ver cómo, al usar los valores lft y rgt, se puede determinar para cualquier nodo su nivel en el árbol, su padre y para los nodos con el mismo padre, el orden de esos nodos.

Si bien los valores de lft y rgt pueden ser suficientes para definir un árbol completamente en teoría, en la práctica, agregar campos adicionales brinda más oportunidades para navegar por el árbol de manera eficiente, y en Joomla hay 5 campos asociados con una estructura de árbol:

  • campos lft y rgt
  • parent ID: el ID del nodo padre
  • level: el nivel en la jerarquía, el nivel 0 (Clothing en el ejemplo anterior) es el más alto
  • path; ruta

Path es como la ruta en una estructura de directorio, y es una combinación de los valores de alias que van desde el nodo raíz al nodo en cuestión, con una barra que separa los alias. Por ejemplo, refiriéndose al diagrama anterior, si Clothing es el nodo raíz, entonces la ruta para Jackets sería Men-s/Suits/Jackets.

Observa cómo los campos adicionales nos proporcionan mecanismos eficientes para navegar por el árbol. Por ejemplo, podemos hacer una consulta seleccionando registros DONDE los padres son Suits y ORDENAR por lft, y eso nos dará los Slacks y Jackets en orden. Este tipo de operaciones generalmente se realizan en el lado cliente, donde queremos que el sitio funcione bien. Por el contrario, las operaciones de administración pueden implicar muchas actualizaciones. Por ejemplo, si movemos Blouses para que sea el primer hijo bajo Men's, entonces esto implicará actualizar los valores lft y rgt de casi todos los registros en la tabla de la base de datos.

Como es de esperar, Joomla proporciona funciones de biblioteca para ayudar a ubicar un nuevo nodo en el árbol, reparar un elemento, eliminar un elemento (y todos sus elementos secundarios), reordenar elementos, etc.

Functionalidad

Primero, deseamos que nuestra instalación de este paso construya la estructura de árbol en nuestra tabla de helloworld.

En nuestra vista helloworlds de administración, mostraremos el nivel del registro con sangría del título, como se hace para las categorías y elementos del menú. También mostraremos los valores de nuestros nuevos campos, realmente para nuestros propios fines de diagnóstico, ya que no lo haría en una aplicación real.

En el paso anterior, introdujimos una funcionalidad que permitía al administrador arrastrar los registros para reordenarlos. Continuaremos usando este mecanismo, pero la reordenación se limitará a los registros que tengan el mismo padre (es decir, hermanos), y los registros deberán ordenarse en orden ascendente primero (la clasificación por "Orden descendente" no funcionará).

Si eliminamos un elemento, todos sus hijos se moverán hacia la jerarquía, para tener como padre el padre del registro eliminado.

En la vista de edición de administrador, permitiremos al administrador:

  • Especificar un nuevo padre para el artículo
  • Especificar el posicionamiento de este artículo dentro del orden de sus hermanos.

(aunque, si cambiamos el padre, los nuevos hermanos aparecerán solo después de Guardar).

También nos aseguraremos de que la funcionalidad Nuevo y Guardar como copia funcione como se espera.

En el lado cliente, cambiaremos la página que muestra un registro de HelloWorld para presentar también a los padres y los descendientes de ese registro.

También actualizaremos el formulario de lado cliente para que al crear un nuevo registro de helloworld también solicite al padre.

Enfoque

Inicialmente necesitaremos configurar nuestra base de datos como un árbol de conjunto anidado. Queremos que el registro con id=1 sea la raíz del árbol, por lo tanto, si hay un registro existente con id=1, le asignaremos un nuevo número y cambiaremos los registros asociados en la tabla de Activos y Asociaciones. Una vez que el registro raíz esté en su lugar, estableceremos los otros registros de helloworld como elementos secundarios directos de la raíz y estableceremos los nuevos campos en consecuencia. Como esto es demasiado complejo para hacer en el script SQL, codificaremos esto dentro del archivo de instalación script.php.

El código para realizar operaciones en los campos de la base de datos del conjunto anidado está dentro de JTableNested, por lo que cambiamos nuestra clase de table helloworld para heredarla de ella en lugar de JTable. Usaremos una serie de métodos dentro de esta clase para aplicar los cambios en la base de datos que surgen de las operaciones de administración:

  • setLocation($referenceId, $position) para definir cómo se posiciona el registro actual (para ordenar) con respecto a sus hermanos: usaremos esto cuando el administrador use el formulario de edición de helloworld y cambie el padre o el orden del registro con respecto a sus hermanos.
  • saveorder($idArray, $lft_array) para almacenar un orden revisado de hermanos: usaremos esto siempre que el administrador use la función de arrastrar registro para mover un registro
  • store($updateNulls) para almacenar un registro nuevo o actualizado: el método save() lo llamará en JModelAdmin
  • delete($pk, $children) para eliminar un registro y (opcionalmente) todos sus descendientes: usaremos esto cada vez que el administrador elimine uno o más registros de helloworld, y usaremos la opción de NO borrar a los descendientes.
  • rebuild(...) para recalcular los valores de lft, rgt y path para los registros de helloworld: usaremos esto después de reposicionar un registro cambiando su padre o reordenado en el formulario de edición.

En nuestro archivo de diseño de Helloworlds, la visualización de los nuevos campos es sencilla. Los otros cambios implican lo siguiente.

  1. Para sangrar los registros según el nivel (como se hace para los menús y categorías) usaremos el diseño estándar de joomla en layouts/joomla/html/treeprefix.php
  2. Para admitir el arrastre de las filas a reordenar, el código javascript en sortablelist.js que encontramos en el paso anterior Agregar Ordenamiento se usará nuevamente, pero para entregar el conjunto anidado especificamos diferentes parámetros a la función, y necesitamos agregar atributos diferentes a los elementos .

En nuestro diseño (editar) de Helloworld mostraremos los elementos de entrada a capturar

  • el registro padre: esta será una lista de todos los registros de hellowworld, pero excluyendo el propio registro (ya que un registro no puede tener su propio padre) y sus descendientes (ya que esto formaría un bucle en el árbol), y
  • su posición con respecto a los hermanos del registro - para dar salida a los hermanos en orden.

Debido a los requisitos específicos de estas listas de selección, ninguno de los dos puede implementarse utilizando un campo de formulario estándar de Joomla y XML, y definiremos un campo de formulario personalizado para cada uno y definiremos la lista de valores posibles de forma dinámica.

En el lado cliente, usaremos otra función de tabla anidada getTree() para encontrar los descendientes del registro que estamos mostrando. Y en el formulario del lado cliente para crear un nuevo registro, reutilizaremos el campo de formulario personalizado de administración para presentar la lista de registros para elegir un padre.

Donde en el paso del tutorial anterior usamos el campo de base de datos ordering para ordenar, en este paso usaremos el campo de base de datos lft. Así que eso es un cambio global tanto en nuestro código de administrador como en el de nuestro sitio.

Base de datos e instalación

Archivo de instalación de SQL actualizado:

admin/sql/install.mysql.utf8.sql

DROP TABLE IF EXISTS `#__helloworld`;

CREATE TABLE `#__helloworld` (
	`id`       INT(11)     NOT NULL AUTO_INCREMENT,
	`asset_id` INT(10)     NOT NULL DEFAULT '0',
	`created`  DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00',
	`created_by`  INT(10) UNSIGNED NOT NULL DEFAULT '0',
	`checked_out` INT(10) NOT NULL DEFAULT '0',
	`checked_out_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
	`greeting` VARCHAR(25) NOT NULL,
	`alias`  VARCHAR(40)  NOT NULL DEFAULT '',
	`language`  CHAR(7)  NOT NULL DEFAULT '*',
	`parent_id`	int(10)    NOT NULL DEFAULT '1',
	`level`	int(10)    NOT NULL DEFAULT '0',
	`path`	VARCHAR(400)    NOT NULL DEFAULT '',
	`lft`	int(11)    NOT NULL DEFAULT '0',
	`rgt`	int(11)    NOT NULL DEFAULT '0',
	`published` tinyint(4) NOT NULL DEFAULT '1',
	`catid`	    int(11)    NOT NULL DEFAULT '0',
	`params`   VARCHAR(1024) NOT NULL DEFAULT '',
	`image`   VARCHAR(1024) NOT NULL DEFAULT '',
	`latitude` DECIMAL(9,7) NOT NULL DEFAULT 0.0,
	`longitude` DECIMAL(10,7) NOT NULL DEFAULT 0.0,
	PRIMARY KEY (`id`)
)
	ENGINE =MyISAM
	AUTO_INCREMENT =0
	DEFAULT CHARSET =utf8;

CREATE UNIQUE INDEX `aliasindex` ON `#__helloworld` (`alias`, `catid`);

/*We added the published column to the INTO statement in order to set the published column, in the helloworld root record, to 1 otherwise you won't be able to publish items.*/

INSERT INTO `#__helloworld` (`greeting`,`alias`,`language`, `parent_id`, `level`, `path`, `lft`, `rgt`, `published`) VALUES
('helloworld root','helloworld-root-alias','en-GB', 0, 0, '', 0, 5, 1),
('Hello World!','hello-world','en-GB', 1, 1, 'hello-world', 1, 2, 0),
('Goodbye World!','goodbye-world','en-GB', 1, 1, 'goodbye-world', 3, 4, 0);

Para la actualización definimos la nueva estructura de la base de datos en el siguiente archivo ...

/admin/sql/updates/mysql/0.0.26.sql

ALTER TABLE `#__helloworld` DROP COLUMN `ordering`;
ALTER TABLE `#__helloworld` ADD COLUMN `parent_id` INT(10) NOT NULL DEFAULT '1' AFTER `language`;
ALTER TABLE `#__helloworld` ADD COLUMN `level`	int(10)    NOT NULL DEFAULT '0' AFTER `parent_id`;
ALTER TABLE `#__helloworld` ADD COLUMN `path`	varchar(400)    NOT NULL DEFAULT '' AFTER `level`;
ALTER TABLE `#__helloworld` ADD COLUMN `lft`	int(11)    NOT NULL DEFAULT '0' AFTER `path`;
ALTER TABLE `#__helloworld` ADD COLUMN `rgt`	int(11)    NOT NULL DEFAULT '0' AFTER `lft`;
UPDATE `#__helloworld` SET `path` = `alias`;

Pero para construir el árbol usamos un archivo de instalación script.php. Este código hace lo siguiente:

  1. comprueba si ya hay un registro raíz con id=1. Si es así, asume que el árbol se ha construido y sale sin cambiar nada.
  2. de lo contrario, si hay un registro ordinario con id=1, entonces cambia su id a uno más grande que el id máximo en la tabla. Luego cambia cualquier registro asociado en la tabla de Activos y en la tabla de Asociaciones. (Observamos cómo se creó la clave en la tabla de Asociaciones usando un hash md5 en Agregando Asociaciones. Si el registro de helloworld con id=1 que tuviera esta nueva identificación fuera un id original, entonces la clave de asociaciones formada por el hash md5 habría sido diferente, pero en realidad eso no importa. De hecho, cuando se realiza el hash md5, si las asociaciones en la matriz están en un orden diferente, entonces produciría una clave diferente de todos modos).
  3. crea el registro raíz, con id=1, y establece los valores de lft y rgt para él (según el número total de registros en la tabla).
  4. actualiza los valores lft y rgt para todos los registros de helloworld existentes en la tabla.

script.php

<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * Script file of HelloWorld component.
 *
 * The name of this class is dependent on the component being installed.
 * The class name should have the component's name, directly followed by
 * the text InstallerScript (ex:. com_helloWorldInstallerScript).
 *
 * This class will be called by Joomla!'s installer, if specified in your component's
 * manifest file, and is used for custom automation actions in its installation process.
 *
 * In order to use this automation script, you should reference it in your component's
 * manifest file as follows:
 * <scriptfile>script.php</scriptfile>
 *
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
class com_helloWorldInstallerScript
{
    /**
     * This method is called after a component is installed.
     *
     * @param  \stdClass $parent - Parent object calling this method.
     *
     * @return void
     */
    public function install($parent) 
    {
        $parent->getParent()->setRedirectURL('index.php?option=com_helloworld');
    }

    /**
     * This method is called after a component is uninstalled.
     *
     * @param  \stdClass $parent - Parent object calling this method.
     *
     * @return void
     */
    public function uninstall($parent) 
    {
        echo '<p>' . JText::_('COM_HELLOWORLD_UNINSTALL_TEXT') . '</p>';
    }

    /**
     * This method is called after a component is updated.
     *
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    public function update($parent) 
    {
        echo '<p>' . JText::sprintf('COM_HELLOWORLD_UPDATE_TEXT', $parent->get('manifest')->version) . '</p>';
    }

    /**
     * Runs just before any installation action is preformed on the component.
     * Verifications and pre-requisites should run in this function.
     *
     * @param  string    $type   - Type of PreFlight action. Possible values are:
     *                           - * install
     *                           - * update
     *                           - * discover_install
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    public function preflight($type, $parent) 
    {
        echo '<p>' . JText::_('COM_HELLOWORLD_PREFLIGHT_' . $type . '_TEXT') . '</p>';
    }

    /**
     * Runs right after any installation action is preformed on the component.
     *
     * @param  string    $type   - Type of PostFlight action. Possible values are:
     *                           - * install
     *                           - * update
     *                           - * discover_install
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    function postflight($type, $parent) 
    {
		$db = JFactory::getDbo();
		
		echo '<p>Checking if the root record is already present ...</p>';
		
		$query = $db->getQuery(true);
		$query->select('id');
		$query->from('#__helloworld');
		$query->where('id = 1');
		$query->where('alias = "helloworld-root-alias"');
		$db->setQuery($query);
		$id = $db->loadResult();
		
		if ($id == '1')
		{   // assume tree structure already built
			echo '<p>Root record already present, install program exiting ...</p>';
			return;
		}

		echo '<p>Checking if there is a record with id = 1 ...</p>';
		
		$query = $db->getQuery(true);
		$query->select('id');
		$query->from('#__helloworld');
		$query->where('id = 1');
		$db->setQuery($query);
		$id = $db->loadResult();
			
		if ($id)
		{
			echo '<p>Record with id = 1 found</p>';
			
			// get new id
			$query = $db->getQuery(true)
				->select('max(id) + 1')
				->from('#__helloworld');
			$db->setQuery($query);
			$newid = $db->loadResult(); 
			echo "<p>Changing id to $newid</p>";
			
			// update id in helloworld table
			$query = $db->getQuery(true)
				->update('#__helloworld')
				->set("id = $newid")
				->where("id = $id");
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>Id in helloworld table changed, records updated: $nrows</p>";
			}
			else
			{
				echo "<p>Error: Id in helloworld table not changed</p>";
				var_dump($result);
			}
			
			// update id in the associations table
			$query = $db->getQuery(true)
				->update('#__associations')
				->set("id = $newid")
				->where("id = $id")
				->where('context = "com_helloworld.item"');
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>Id in associations table changed, records updated: $nrows</p>";
			}
			else
			{
				echo "<p>Error: Id in associations table not changed</p>";
				var_dump($result);
			}
			
			// update id in the assets table
			$query = $db->getQuery(true)
				->update('#__assets')
				->set('name = "com_helloworld.helloworld.' . $newid . '"')
				->where('name = "com_helloworld.helloworld.' . $id . '"');
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>Id in assets table changed, records updated: $nrows</p>";
			}
			else
			{
				echo "<p>Error: Id in assets table not changed</p>";
				var_dump($result);
			}
		}
		else 
		{
			echo '<p>No record with id = 1 found</p>';
		}
		
		// find number of records in helloworld table
		$query = $db->getQuery(true)
			->select('count(*)')
			->from('#__helloworld');
		$db->setQuery($query);
		$total = $db->loadResult(); 
		
		// insert root record
		$columns = array('id','greeting','alias','parent_id','rgt');
		$values = array(1, 'helloworld root','helloworld-root-alias',0, 2 * (int)$total + 1);

		$query = $db->getQuery(true)
			->insert('#__helloworld')
			->columns($db->quoteName($columns))
			->values(implode(',', $db->quote($values)));
		$db->setQuery($query);
		$result = $db->execute();
		if ($result)
		{
			$nrows = $db->getAffectedRows();
			echo "<p>$nrows inserted into helloworld table</p>";
		}
		else
		{
			echo "<p>Error creating root record</p>";
			var_dump($result);
		}
		
		// update lft and rgt for each of the other records (ie not root)
		$query = $db->getQuery(true)
			->select('id')
			->from('#__helloworld')
			->where('id > 1');
		$db->setQuery($query);
		$ids = $db->loadColumn(); 
		for ($i = 0; $i < $total; $i++)
		{
			$lft = 2 * (int)$i + 1;
			$rgt = 2 * (int)$i + 2;
			$query = $db->getQuery(true)
				->update('#__helloworld')
				->set("lft = {$lft}")
				->set("rgt = {$rgt}")
				->where("id = {$ids[$i]}");
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>$nrows updated in helloworld table, for id = {$ids[$i]}</p>";
			}
			else
			{
				echo "<p>Error updating record</p>";
				var_dump($result);
			}
		}
    }
}

MVC Helloworlds

Archivo de modelo actualizado para incluir los nuevos campos. El orden predeterminado se ha establecido para que se base en la estructura del árbol, ya que posiblemente sea más apropiado que basarse en el saludo.

admin/models/helloworlds.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorldList Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorlds extends JModelList
{
        /**
         * Constructor.
         *
         * @param   array  $config  An optional associative array of configuration settings.
         *
         * @see     JController
         * @since   1.6
         */
        public function __construct($config = array())
        {
                if (empty($config['filter_fields']))
                {
                        $config['filter_fields'] = array(
                                'id',
                                'greeting',
                                'author',
                                'created',
                                'language',
                                'lft',
                                'category_id',
                                'association',
                                'published'
                        );
                }

                parent::__construct($config);
        }

        protected function populateState($ordering = 'lft', $direction = 'asc')
        {
                $app = JFactory::getApplication();

                // Adjust the context to support modal layouts.
                if ($layout = $app->input->get('layout'))
                {
                        $this->context .= '.' . $layout;
                }

                // Adjust the context to support forced languages.
                $forcedLanguage = $app->input->get('forcedLanguage', '', 'CMD');
                if ($forcedLanguage)
                {