miércoles, 26 de diciembre de 2007

Trabajando con múltiples shemas de postgresql en Symfony.

El manejador de BdD postgres, tal véz el mejor motor de BdD libre que existe (que me disculpen los amante de MySQL ;-) ) tiene la posibilidad de trabajar con schemas, que es una manera muy interesante de organizar las distintas tablas de nuestra BdD. Para entender un poco lo que son los schemas en postgres acá les colocó un link a la página de Expresión Digital donde tienen una definición bastante sencilla de lo que son y como se implementan ( Es importante no confundir el concepto de schema de postgres con el concepto aplicado a los archivos de schema utilizado en Symfony).

Una debilidad que tiene Propel/Creole, el ORM utilizado en Symfony, es que no soporta trabajar con múltiples schemas de postgresql. Por defecto este asume que trabajará con el schema por defecto, llamado public. Què pasa si tenemos múltiples schemas en nuestra BdD en Postgresql, y queremos utilizar Symfony?, pues sencillamente con la instalación por defecto de Symfony esto no es viable.
Pero viable no quiere decir que sea imposible. Hace aproximadamente un año me tocó trabajar con una BdD en Postgres con varios schemas con Symfony, y al toparme con esta debilidad de Propel/Creole procedí a reescribir un par de clases de el core de dicho ORM para solventar dicho escollo. Después de 2 días de trabajo logré ajustar las siguiente clase de Creole:
.- PgSQLConnection: Ubicada en la carpeta vendor/creole/drivers/pgsql en las librerías de Symfony, se encarga de realizar la conexión a la BdD en base a un arreglo de parámetros de conexión. A esta clase le agregué una condición para que en caso de que se especifique el parámetro schema se ejecute la instrucción SET search_path TO en la BdD, indicando los schemas que se hayan especificado. Con ese simple cambio es posible especificar un dsn en el archivo database.yml como el que se presenta a continuación:

dev:
propel:
class: sfPropelPgDatabase
param:
dsn: pgsql://miusuario:miclave@miservidor/mibdd?schema='public','VENTAS'

De esta manera nuestra aplicación en Symfony trabajaría con las tablas tanto del schema public como VENTAS de la BdD indicada.

Otro punto importante es si nosotros escribimos manualmente el archivo schema.yml de Symfony o lo generamos a partir de nuestra BdD con el comando symfony propel-build-schema. En el caso de usar el comando propel-build-schema tenemos las misma restricción, y que por defecto generará el schema.yml exclusivamente del schema public de la BdD. Para poder generar el schema.yml con las tablas de varios schemas modifiqué la siguiente clase:

.- PgSQLDatabaseInfo: Ubicada en la carpeta vendor/creole/drivers/pgsql/metadata en las librerías de Symfony, es utilizada para realizar las consultas necesarias a la BdD para la generación del schema.yml. Realicé algunos ajustes al método initTables de dicha clase para recuperar la lista de tablas de varios schemas, en caso de que se hayan especificado. De esta manera se puede editar el archivo propel.ini de la siguiente manera para generar el shcema.yml con las tablas de los schemas public y VENTAS de la BdD:

propel.targetPackage = lib.model
propel.packageObjectModel = false
propel.project = miproyecto
propel.database = pgsql
propel.database.createUrl = pgsql://miservidor/
propel.database.url = pgsql://miusuario:miclave@miservidor/mibdd?schema='public','VENTAS'


Un problema que tiene este mecanismo es el de manejar tablas con el mismo nombre entre los schemas, así por ejemplo pueden tener una tabla llamada persona en el schema public y otra llamada persona en el schema VENTAS, a nivel de postgres esto es válido, pero en Symfony, esto nos traería inconvenientes. Es posible crear una aplicación en nuestro proyecto que trabaje solo con el schema public, y otra que trabaje solo con el schema VENTAS con este cambio que les indico, pero si tienen tablas con el mismo nombre es recomendable que utilizen la propiedad package y/o phpname en el schema.yml para indicar que las clases de dichos schemas se generen en directorios distintos y con nombres de clase diferentes.

A continuación les colocó un link al servicio mediafire (servicio muy bueno por cierto) donde podrán descargar las 2 clases modificadas que les comenté.

Una desventaja que tiene este cambio es que si se actualiza Symfony es necesario volver a reemplazar las clases en cuestión. Hace aproximadamente 10 meses coloqué un ticket en el trac de Propel/Creole solicitando la posibilidad de un cambio o mejora con el código en cuestión, pero hasta la fecha no he tenido una respuesta satisfactoria. Existen otras posibilidades de ajustar el código de Propel/Creole para que trabaje con Schemas de Postgres, pero hasta la fecha, personalmente considero que está es una de las más sencillas.

viernes, 21 de diciembre de 2007

Optimizando Consultas en el modelo. Segunda Parte

Continuando con el punto anterior de como optimizar un poco las consultas en el modelo (y retomando el blog, que lo he tenido, por diversos temas, un poco abandonado) a continuación mostraré como extender el ejemplo anterior para poder utilizarlo con un paginador de un modulo bien sea creado de manera manual o con el admin-generator.

Symfony utiliza un clase llamada sfPropelPager para crear un objeto que nos permita "paginar" los registros generados a partir de una consulta a la BdD. Al instanciar un objeto a partir de dicha clase, se requieren especificar un par de parámetros al mismo, uno es el nombre del Modelo con que trabajará y el segundo es el tamaño de la pagina (cantidad de registros a mostrar por página). Para poder operar correctamente, el paginador necesita realizar 2 consultas a la BdD, una para contar la cantidad de registros que cumplen con el criterio de consulta ( en caso de que especifique alguna condición ) y la segunda para traer los registros de la pagina solicitada.

Retomando el ejemplo que utilizamos en el post anterior vamos a optimizar el paginador de un módulo de personas, que asumiremos fue generado con el admin-generator, para que muestre la información tal cual como la retorna el método getList() del la clase PersonaPeer.

Para esto es necesario crear un método en la clase PersonaPeer que "cuente" los registros con las mismas condiciones que utiliza el método getList(). A continuación se muestra la clase PersonaPeer:



<?php

/**
* Subclass for performing query and update operations on the 'persona' table.
*
*
*
* @package lib.model
*/

class PersonaPeer extends BasePersonaPeer
{
public static function getList(Criteria $criteria)
{
$personas = array();
// Clonamos el objeto, para evitar modificar el objeto original
$criteria = clone $criteria;
// Eliminanos las columnas de selección en caso de que esten definidas
$criteria->clearSelectColumns();
// Agregamos las columnas de las tablas que queremos recuperar
$criteria->addSelectColumn(PersonaPeer::ID_PERSONA );
$criteria->addSelectColumn(PersonaPeer::NOM_PERSONA );
$criteria->addSelectColumn(PaisPeer::NOM_PAIS );
$criteria->addSelectColumn(EstadoPeer::NOM_ESTADO );
$criteria->addSelectColumn(MunicipioPeer::NOM_MUNICIPIO );
// Agregamos los Joins entre las distintas tablas
PersonaPeer::addConditionsList($criteria);
//Recuperamos los registros y generamos el arreglo de hashes
$rs = PersonaPeer::doSelectRS($criteria);
while ($rs->next())
{
$persona['id'] = $rs->getInt(1);
$persona['nombre'] = $rs->getString(2);
$persona['pais'] = $rs->getString(3);
$persona['estado'] = $rs->getString(4);
$persona['municipio'] = $rs->getString(5);
$personas[] = $persona;
}
return $personas;
}

public static function getCountList(Criteria $criteria)
{
$criteria = clone $criteria;
$criteria->clearSelectColumns()->clearOrderByColumns();
$criteria->addSelectColumn(PersonaPeer::COUNT);
PersonaPeer::addConditionsList($criteria);
$rs = PersonaPeer::doSelectRS($criteria, $con);
if ($rs->next()) {
return $rs->getInt(1);
} else {
return 0;
}
}

protected static function addConditionsList(Criteria $criteria)
{
$criteria->addJoin(PersonaPeer::ID_PAIS ,PaisPeer::ID_PAIS,Criteria::LEFT_JOIN );
$criteria->addJoin(PersonaPeer::ID_ESTADO , EstadoPeer::ID_ESTADO,Criteria::LEFT_JOIN );
$criteria->addJoin(PersonaPeer::ID_MUNICIPIO, MunicipioPeer::ID_MUNICIPIO );
}
}




Como vemos, tenemos un método que llamamos getCountList() que permite realizar el conteo de los registros con las mismas condiciones que el método getList(); y aplicando la premisa DRY (Don't Repeat Yourself) las condiciones de filtrado las colocamos en un método protegido que es llamado desde getCountList() y getList().

Ahora que tenemos los 2 métodos necesarios para crear un paginador, vamos a proceder a actualizar nuestro módulo autogenerado, reescribiendo la menor cantidad de código posible.

Primero, vamos a modificar el generator.yml del módulo persona para indicar que se van a utilizar un método de conteo y de recuperación de registros específicos (Por defecto los objetos creados a partir de la clase sfPropelPager utilizan los métodos doCount y doSelect de la clase Peer para realizar esta tarea):


generator:
class: sfPropelAdminGenerator
param:
model_class: Persona
theme: default
list:
display: [id_persona, nom_persona, _pais, _estado, _municipio]
peer_method: getList
peer_count_method: getCountList


En la opción display colocamos el nombre de los campos que queremos mostrar teniendo en cuenta lo siguiente: Los campos que no pertenecen directamente a la tabla del modelo, como el nombre de pais, del estado, entre otros, deben especificarse como parciales, estos para que no se genere link de ordenamiento para dichos campos ( se puede ajustar el modulo para que el paginador acepte ordenamiento por campos que no pertenecen a la tabla del modelo, pero eso puede ser tema para otro post ;) ).
Los otras 2 opciones, peer_method y peer_count_method nos permiten especificar que métodos del post queremos que sean usados por el paginador para realizar la consulta.

Ahora, algo muy importante, por defecto el generator asume que los que le retorna el método de recuperación de registro, en nuestro caso el getList(), es un arreglo de objetos, pero en nuestro caso este método retorna un arreglo de hashes. Que hacemos?, pues la manera más sencilla de hacerlo, y que viene a demostrar la eficiencia del código autogenerado por symfony, es reescribir un parcial, específicamente el que se llama _list_td_tabular.php (en caso de que se especifique que la información sa va a mostrar apilada, debe reescribirse el parcial _list_td_stacked.php). Creamos dicho parcial en la carpeta templates de muestro módulo y, siguiendo con nuestro ejejmplo, le colocamos el siguiente ćodigo:



<td><?php echo link_to($persona['id'] ? $persona['id'] : __('-'), 'persona/edit?id_persona='.$persona['id']) ?></td>
<td><?php echo $persona['nombre'] ?></td>
<td><?php echo $persona['pais'] ?></td>
<td><?php echo $persona['estado'] ?></td>
<td><?php echo $persona['municipio'] ?></td>




Como vemos, con redefinir este parcial de código autogenerado, podemos utilizar el arreglo de hashes que retorna el método getList(), en vez del objeto esperado. Esto incluso nos da la ventaja que no tenemos que definir los parciales que indicamos el la opción display del generator.

Con un poco de creatividad y revisando el código autogenerado, podemos adaptar nuestro módulo para que filtre y ordene por campos del modelo de Pais, Estado o Municipio.

martes, 16 de octubre de 2007

Optimizando consultas en el modelo.

Utilizar un ORM tienes sus ventajas, pero también sus desventajas. Manejar el acceso a registros específicos de la base de datos para hacer modificaciones o consultas resulta muy cómodo utilizando objetos, y la penalización en el rendimiento es prácticamente imperceptible. Pero que pasa cuando necesitamos realizar una consulta que involucra campos de varias tablas, y la cantidad de registros a recuperar son cientos o quizás miles, como por ejemplo, la consulta para un reporte. Es viable realizarlo creando un método que cree objetos "enlazados", similar a como lo hace los métodos doSelectJoinXXXX de las clases peer de los modelos base, pero si la cantidad de registros es muy grande los tiempos de respuestas serán demasiado altos, y probablemente la aplicación se quede sin memoria, dependiendo la configuración memory_limit de PHP.
Para solventar este escollo es preferible crear un método en el modelo que genere en vez de un arreglo de objetos, un arreglo de hashes, en el cual solo se retornen los valores que nos interesan.
Volvamos al ejemplo con que trabajamos en el post anterior. Supongamos que queremos crear, en nuestro módulo personas, un reporte de tipo listado, donde aparezca el nombre de la persona, el país, estado y municipio donde reside. En la mayoría de las recomendaciones que encontramos en internet con respecto a estos casos indican que debemos crear método donde se defina el query con un string, parsearle los campos, y ejecutarlo. Veamos un ejemplo para entenderlo mejor:

<?php
/**
* Subclass for performing query and update operations on the 'persona' table.
*
*
*
* @package lib.model
*/

class PersonaPeer extends BasePersonaPeer
{
public static function getList()
{
$personas = array();
$con = Propel::getConnection();
$query = 'SELECT %s , %s, %s,
%s, %s
FROM %s
left join %s on %s = %s
left join %s on %s = %s
left join %s on %s = %s'
;

$query = sprintf($query, self::ID_PERSONA , self::NOM_PERSONA ,
PaisPeer::NOM_PAIS , EstadoPeer::NOM_ESTADO , MunicipioPeer::NOM_MUNICIPIO ,
self::TABLE_NAME,
PaisPeer::TABLE_NAME ,self::ID_PAIS , PaisPeer::ID_PAIS ,
EstadoPeer::TABLE_NAME , self::ID_ESTADO , EstadoPeer::ID_ESTADO ,
MunicipioPeer::TABLE_NAME , self::ID_MUNICIPIO , MunicipioPeer::ID_MUNICIPIO );

$stmt = $con->prepareStatement($query);
$rs = $stmt->executeQuery();
while ($rs->next())
{
$persona['id'] = $rs->getInt(1);
$persona['nombre'] = $rs->getString(2);
$persona['pais'] = $rs->getString(3);
$persona['estado'] = $rs->getString(4);
$persona['municipio'] = $rs->getString(5);
$personas[] = $persona;
}
return $personas;
}
}

Como vemos, este método del peer retorna un arreglo de hashes, cuya busqueda y armado es más rápido que armar un arreglo de objetos. Pero, que desventajas tiene?. La principal desventaja es que, a diferencias de otros métodos del peer, esto no nos permite pasarle condiciones de filtrado u ordenamiento a través de criteria. Para solventar esa deficiencia vamos a reescribir el mismo pero usando un objeto criteria, que se recibirá como parámetro:

<?php
/**
* Subclass for performing query and update operations on the 'persona' table.
*
*
*
* @package lib.model
*/

class PersonaPeer extends BasePersonaPeer
{
public static function getList(Criteria $criteria)
{
$personas = array();
// Clonamos el objeto, para evitar modificar el objeto original
$criteria = clone $criteria;
// Eliminanos las columnas de selección en caso de que esten definidas
$criteria->clearSelectColumns();
// Agregamos las columnas de las tablas que queremos recuperar
$criteria->addSelectColumn(self::ID_PERSONA );
$criteria->addSelectColumn(self::NOM_PERSONA );
$criteria->addSelectColumn(PaisPeer::NOM_PAIS );
$criteria->addSelectColumn(EstadoPeer::NOM_ESTADO );
$criteria->addSelectColumn(MunicipioPeer::NOM_MUNICIPIO );
// Agregamos los Joins entre las distintas tablas
$criteria->addJoin(self::ID_PAIS ,PaisPeer::ID_PAIS,Criteria::LEFT_JOIN );
$criteria->addJoin(self::ID_ESTADO , EstadoPeer::ID_ESTADO,Criteria::LEFT_JOIN );
$criteria->addJoin(self::ID_MUNICIPIO, MunicipioPeer::ID_MUNICIPIO );
//Recuperamos los registros y generamos el arreglo de hashes
$rs = self::doSelectRS($criteria);
while ($rs->next())
{
$persona['id'] = $rs->getInt(1);
$persona['nombre'] = $rs->getString(2);
$persona['pais'] = $rs->getString(3);
$persona['estado'] = $rs->getString(4);
$persona['municipio'] = $rs->getString(5);
$personas[] = $persona;
}
return $personas;
}
}


Aunque fue un poco más difícil de escribir, este método hace el mismo trabajo que el anterior, con la ventaja que recibe un objeto criteria, con lo cual podemos definir filtros por cualquier campo de las 4 tablas involucradas en la consulta ( Ejem: Obtener los datos de personas que viven en un estado específico, entre otros), lo cual hace este método más reusable. En un próximo post veremos como podemos utilizar este mismo método en la acción list autogenerada de nuestro módulo persona.

viernes, 28 de septiembre de 2007

Crear listas dependientes con Ajax en Symfony. Segunda Parte.

En el penúltimo post abordé el tema de como manejar una lista dependiente usando Ajax. El ejemplo planteado es muy efectivo cuando tenemos una lista que depende de otra. Pero, que pasa cuando existe otra lista que a su vez dependa de esta última lista en cuestión, por ejemplo, tener la lista de municipios, que dependa de una lista de estados, que a su vez dependa de una lista de paises. A continuación veremos como con unas pequeñas variaciones del dicho ejemplo podemos conseguir esto, e incluso, poder hacer listas de n niveles de dependencia.

La Dirección de una Persona.

Para el ejemplo vamos a suponer que tenemos un modelo con 4 tablas; país, estado, municipio y persona, donde el registro de la persona contiene un referencia a las otras 3 tablas.
Para cada una de ellas vamos a crear un módulo administrativo.

symfony propel-init-admin test pais Pais
symfony propel-init-admin test estado Estado
symfony propel-init-admin test municipio Municipio
symfony propel-init-admin test persona Persona

  • Los componentes
Aprovechando la potencia que ofrecen los componentes, vamos a crear 3 de ellos, uno para país, otro para el estado y otro para el municipio. Los componentes para el Estado y Municipio reciben hasta 3 parámetros; el nombre que tendrá el objeto select a nivel de formulario, el id seleccionado por defecto, y el id del objeto padre para filtrar la búsqueda ( por ejemplo, el componente del municipio recibe el id del estado para filtrar solo los municipios que pertenecen a ese estado). En el caso del país, recibe solo 2 parámetros, el nombre del objeto select y el id del país seleccionado por defecto (El componente del municipio es el mismo que se mostró en el post anterior).
A continuación se muestra la parte de la lógica de negocio del componente de país:

<?php
class paisComponents extends sfComponents
{
public function executeSelectAll()
{
$this->paises = PaisPeer::doSelect(New Criteria());
}
}

Y acá el parcial _selectAll.php:

<?php echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($paises),
isset($id_pais)?$id_pais:''),array());?>

Ahora el componente en el módulo estado:

<?php
class estadoComponents extends sfComponents
{
public function executeSelectByPais()
{
$this->estados = EstadoPeer::doSelectByPais($this->id_pais);
}
}

El parcial _selectByPais.php del componente.

<?php
echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($estados),
isset($id_estado)?$id_estado:''),array());?>

  • Las Acciones que se llamarán vía Ajax.

Para el ejemplo que nos compete necesitamos tener 2 acciones a ejecutar vía Ajax: Una que se ejecutará al seleccionar un país, y que debe actualizar la lista de estados, y otra que se ejecutará al seleccionar un estado y que debe actualizar la lista de municipios.
Hay un punto muy importante a tener en consideración. Como vimos en el post previo relacionado con este caso, a cada lista de la que dependa otra lista le tenemos que crear un observador. En este caso tenemos que crear un observador al campo que selecciona el país y otro al que selecciona el estado. En este punto en donde la cosas se complican un poco. Haciendo un poco de memoria debemos recordar que no podemos actualizar el objeto select de manera directa, esto por un bug presente en Internet Explorer, y que nos obligo a colocar el objeto select dentro de un span y "recrearlo" completamente vía Ajax. Volviendo al ejemplo, creamos un acción llamada listByPais en el módulo de Estado, y una acción llamada listByEstado en el módulo municipio, y siguiendo el ejemplo del post previo creamos la vistas respectivas, donde se llaman a los componentes ya descritos; procedemos a crear los parciales _id_pais.php , _id_estado.php y _id_municipio.php en el módulo de personas, donde se llaman a los componentes de cada campo y colocando además un observador al campo país y otro al campo estado. Editamos el generator.yml especificando los parciales y ejecutamos desde el navegador el modulo persona, llamando la acción create. Seleccionamos el país, y la lista de estados se actualiza perfectamente, pero cuando seleccionamos un estado, la lista de municipios no se actualiza. Si tienes una herramienta como firebug instalada observarás que la llamada a la acción municipio/listByEstado no se ejecutó. Por qué?, porque al seleccionar el país, el objeto estado fue literalmente "borrado y creado nuevamente", por lo que el observador "pierde la conexión" con dicho objeto. Que hacemos?, como solventamos esta falla?. Personalmente conozco 4 formas de resolver este problema en Symfony, pero para nuestro ejemplo voy a explicar la que considero más al "estilo symfony". Para este caso la acción estado/listByPais no nos sirve, esta solamente es útil para casos donde no exista una lista que dependa de la que el genera. Procedemos entonces a crear en el modelo persona una nueva acción llamada listEstadosByPais:

<?php
class personaActions extends autopersonaActions
{
public function executeListEstadosByPais()
{
}
}


Ahora en la vista listEstadosByPaisSuccess.php vamos a llamar al componente selectByPais del módulo estado, y adicionalmente, vamos a crear nuevamente el observador a dicho campo.


<?php use_helper('Object','Javascript') ?>

<?php include_component('estado','selectByPais',array(
'id_pais' => $sf_request->getParameter('id_pais'),
'nombre' => 'persona[id_estado]',
));
?>
<?php echo observe_field('persona_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value + '&nombre=persona[id_municipio]'",
))
?>

ahora vamos al parcial _id_pais.php y lo vamos a cambiar de la siguiente manera:

<?php include_component('pais','selectAll',array(
'id_pais'=>$persona->getIdPais(),
'nombre'=>'persona[id_pais]',
));?>
<?php echo observe_field('persona_id_pais',array(
'update' => 'estado',
'url' => 'persona/listEstadosByPais',
'with' => "'id_pais=' + value",
'script' => true,
)) ?>

Como vemos, ahora el observador llamará a la acción que acabamos de crear. Aparte le configuramos la acción script en true. Esto es para que cuando se haga el llamado a la acción persona/listEstadosByPais, el código javascript que crea el observador sea ejecutado.
Así, cuando ejecutemos nuevamente la acción create del módulo persona, al seleccionar un país, se regenerá la lista de estados, y se recrea el observador del mismo.
Anexo un link para descargar el proyecto completo, que incluye además un ejemplo de como utilizar listas dependientes para filtrar (en el módulo municipio, al listar, puede filtrarse por pais y estado).

En un próximo post estaré hablando de como hacer consultas complejas a nivel del modelo explotando las bondades del objeto criteria.

domingo, 16 de septiembre de 2007

De Vacaciones

Si bien en mi primer post ofrecí por lo menos una entrada semanal en el blog enfocado primordialmente a Symfony, desde el primero de este mes estoy de vacaciones, el el Estado Merida, acá en Venezuela, recorriendo los distintos pueblos de la región en compañia de mi esposa e hija, y donde la conexión a internet es casi nula. Tengo como punto de honor colocar un post que sería la continuación del post sobre las listas dependientes actualizadas vía Ajax, para el cual pienso colocar los códigos fuentes del ejemplo. Regresó a la rutina el día 25 de este mes, por lo tanto espero escribir el post antes de que el mes finalice.

martes, 28 de agosto de 2007

Crear listas dependientes con Ajax en Symfony

Trabajar con Ajax puede ser un verdadero dolor de cabeza. gracias a Dios Symfony trae integrado prototype y un buen numero de helpers que facilitan el trabajo a la hora de hacer páginas que requieran actualización dinámica utilizando esta tecnología. Un tópico recurrente es el de las listas dependientes (ejemplo: Estado-Municipio-Parroquia). Existen varios mecanismos para hacer este tipo de Ajax. El que presentó a continuación tiene la particularidad que puede ser usado para aplicaciones que se codifiquen "a mano" o generadas vía admin_generator, lo cual hace que el ćodigo sea muy reusable.
  • El modelo.
En el modelo, especificamente en la clase Peer, es recomendable crear un método que recupere los objetos asociados a la clave foranea, por ejemplo, recuperar los municipios que pertenecen a un estado en particular.


<?php
class MunicipioPeer extends BaseMunicipioPeer
{
static public function doSelectByEstado($estado)
{
$c= new Criteria();
$c->add(MunicipioPeer::ID_ESTADO ,$estado);
return MunicipioPeer::doSelect($c);
}
}


  • El componente.
Ahora nos vamos al modulo respectivo del modelo, en este caso municipio y creamos un componente. En nuestro ejemplo, en la parte de la lógica de negocio del componente se generará el arreglo de objetos de tipo municipio asociados a un estado en particular, y en el parcial se creará el objeto select a partir de dicho arreglo. Para que el componente sea lo más reusable posible, el mismo debe recibir 3 parametros: el id de la clave foranea (el id del estado ), que se utiliza para hacer la búsqueda en el modelo, el nombre que llevará el objeto select al generar en el parcial, y el valor por defecto que tendrá dicho objeto. A continuación se muestra la parte de la lógica de negocio del componente:


<?php
class municipioComponents extends sfComponents
{
public function executeSelectByEstado()
{
$this->municipios = MunicipioPeer::doSelectByEstado($this->id_estado);
}
}


El parcial _selectByEstado.php de el componente sería el siguiente:



<?php
echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($municipios),
isset($id_municipio)?$id_municipio:''),array());?>


En el parcial observamos las variables comentadas previamente ($nombre para el nombre del objeto select, $municipios, el arreglo de municipios que se generó previamente en la parte de la lógica de negocio del componente y $id_municipio, que es el valor seleccionado por defecto).
  • Usando el componente en un modulo generado con admin generator.
En el ejemplo supondremos que tenemos una tabla llamada medio, la cual tiene un id_estado y un id_municipio, las cuales son claves foráneas contra las respectivas tablas de estado y municipio.
generamos el modulo medio con el admin generator y en el generator.yml indicamos que queremos capturar en la pantalla de edición del medio el estado y municipio del medio.

edit:
title: REGISTRO DE MEDIOS
display: [nom_medio, id_estado, id_municipio .....]


Al ejecutar la acción edit del modulo medio veremos que se listan todos los municipios, sin filtro por estados. Lo primero que vamos a hacer es colocar que por defecto, cuando se ejecute la acción los municipios salgan filtrados. Para eso vamos a crear un parcial _id_municipio.php en el módulo de medios y en ese parcial llamamos al componente que creamos previamente.



<span id="municipio">
<?php include_component('municipio','selectByEstado',array(
'id_estado'=>$medio->getIdEstado(),
'nombre'=>'medio[id_municipio]',
'id_municipio'=>$medio->getIdMunicipio(),
));
?>
</span>


Tal vez surja la pregunta de por qué el select se colocó dentro de un span, pero más adelante aclararé dicha duda. Ahora vamos a proceder a establecer que la lista de municipio se actualice via Ajax cada vez que se seleccione un estado distinto. Para ello vamos a hacer un parcial llamado _id_estado.php para el select del estado y lo colocaremos un observador, que dispare una acción ajax cada vez que cambie el mismo:




<?php $value = object_select_tag($medio, 'getIdEstado', array (
'related_class' => 'Estado',
'control_name' => 'medio[id_estado]',
'include_custom' => 'Seleccione',
)); echo $value ? $value : ' ' ?>

<?php echo observe_field('medio_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value+'&nombre=medio[id_municipio]'",
)) ?>

Note que en el helper observe_field, se utiliza el id del objeto select, y no el nombre. El id lo puedes ubicar observando el código fuente de la página que genera la acción, o lo puedes deducir del nombre. Adicionalmente el helper observe_field hace una llamada asíncrona a una acción en particular, en nuestro ejemplo esa acción esta en el módulo municipio y se llama listByEstado. A esta acción se le pasarán 2 parámetros, que son el id del estado y el nombre del objeto a generar.
  • Creando la acción del Ajax.
En el módulo municipio creamos la acción listByEstado. Se puede hacer de 2 maneras: dejar la acción en blanco y en la vista listByEstadoSuccess.php llamar al componente selectByEstado creado previamente, o en el acción generar el arreglo de municipios y en la vista solo llamar al parcial. La primera opción, es muy simple y se deja al lector. El segundo ejemplo sería el siguiente:
En actions.class.php.


<?php
class municipioActions extends autoMunicipioActions
{
public function executeListByEstado()
{
$this->municipios = MunicipioPeer::doSelectByEstado($this->getRequestParameter('id_estado'));
}



en la vista listByEstadoSuccess.php:


<?php use_helper('Object') ?>
<?php include_partial('selectByEstado',array(
'municipios'=>$municipios,
'nombre'=>$sf_request->getParameter('nombre'),
));
?>


Solo queda indicar en el generetor.yml de medio que utilize los 2 parciales que creamos.


edit:
title: REGISTRO DE MEDIOS
display: [nom_medio, _id_estado, _id_municipio .....]


A partir de ahora, cuando se ejecute el la acción edit del modulo medio, si estamos editando un registro nuevo, veremos la lista de municipios blanco la primera vez, y si es un registro existente, veremos la lista de municipios relacionadas con el id del estado del registro. En cualquiera de los casos, al seleccionar un nuevo estado, automáticamente se actualizará vía ajax el span municipio con una nueva lista de municipios, basados en el estado seleccionado.
  • Algunas consideraciones.

  1. Por que actualizar un span y no actualizar el objeto select directamente?. Es posible hacerlo, solo bastaría con indicar al observer que el update lo haga directamente al objeto DOM medio_id_municipio, y en la vista de la acción Ajax usar solamente el helper options_for_select para que solo se genere el código html de las opciones. Esto funcionaría perfectamente en Firefox, pero no en internet explorer, ya que este navegador tiene un bug que hace que falle cualquier intento de actualizar un objeto select vía javascript con un innerHTML.
  2. Se puede hacer que la acción listbyEstado del módulo municipio solo se ejecute si se llama via Ajax colocandole una condicion if (!$this->getRequest()->isXmlHttpRequest()) en el ćodigo de la acción.
  3. Se le pueden colocar efectos a la llamada realizada por el observer_field (desvanecimiento, condiciones en caso de que la acción genere un error, entre otras).
  • Y que pasa si tengo un subnivel adicional?.
Y que pasa si tengo un campo, por ejemplo, la parroquia, que depende del municipio seleccionado. Bueno, el código que hemos creado, con algunas pequeñas modificaciones, nos servirá perfectamente, en el siguiente tópico detallaremos esas modificaciones

viernes, 24 de agosto de 2007

Singleton para un indice de texto basado en Search Lucene en Symfony

Es relativamente fácil poder integrar herramientas del framework Zend dentro de Symfony. Con la creación del plugin sfZendPlugin se tienen las potencias de ambos frameworks para desarrollar aplicaciones. Unas de las carencias de Symfony que suple muy bien el Zend Framework es la posibilidad de manejar índices de texto, con su port de la herramienta Lucene.
El el blog de Spindrop hay un excelente ejemplo de como usar esta herramienta. Para facilitar aún más el uso de la misma, supongamos que tenemos un índice de texto para toda la aplicación, y queremos tener la posibilidad de instanciar el mismo desde cualquier parte de la misma. Para eso lo mejor es crear una clase singleton que podamos llamar y que nos retorne un objeto de tipo Zend_search_lucene. A continuación se muestra una clase que realiza este proceso. La misma incluso, en caso de que sea un índice nuevo, lo crea, y queda disponible para su posterior uso.


<?php

class sfIndex extends Zend_Search_Lucene
{
protected static $instance = null;

public static function getInstance()
{
if (!isset(self::$instance))
{
$class = __CLASS__;
$new = false;
$default_dir = sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'index';
$index_dir = sfConfig::get('app_index',$default_dir);
if (!is_dir($index_dir)) {
mkdir($index_dir);
$new = true;
}
self::$instance = new $class($index_dir,$new);
}

return self::$instance;
}

public function deleteDocByKeyword($keyword, $value)
{
$term = new Zend_Search_Lucene_Index_Term($value, $keyword);
$query = new Zend_Search_Lucene_Search_Query_Term($term);
$hits = array();
$hits = $this->find($query);

foreach ($hits AS $hit)
{
$this->delete($hit->id);
}
}


}
}

Esta clase se debe colocar en la carpeta lib de proyecto. Para poder usarla basta con definir el valor index en el archivo app.yml para indicar la carpeta donde se generará el índice, y posteriormente crear una instancia del objeto


$index = sfIndex::getInstance();

viernes, 30 de marzo de 2007

Como establecer automaticamente los campos created_by y updated_by en symfony

Hace tiempo puse un snippet en la página symfony de como establecer automáticamente los campos created_by y updated_by con el usuario que estuviese autenticado en ese momento. Este código permite que cualquier tabla de la BdD que tenga estos campos se actualizen automáticamente. aca en link al snippet (en ingles )

sábado, 24 de febrero de 2007

Grabar llamadas en el movil.

Hoy un pana que conocí en un curso, y que es un hacker fumado, me pidió información sobre algún teléfono que pudiese grabar las llamadas. Me puse a buscar y conseguí algo mejor, un sotfware para teléfonos basados en symbian que hace ese trabajo. No se si será bueno, ya que no tengo un teléfono que lo soporte, pero suena interesante. Acá el link. La principal desventaja es que corre solo en un selecto grupo de teléfonos, y de paso es pago.
http://www.rock-your-mobile.com/call-recorder-pro-s60.3.php

Erase una vez un blog.

He tratado de mantener este blog por mucho tiempo. Una vez intenté hacerlo personal, escribiendo un poco de mi historia, luego trate de pasar a la parte laboral, pero ahora voy a tratar de escribir principalmente de desarrollo de sotfware, especificamente del Framework Symfony, con el cual tengo unos 7 meses trabajando y me va bien.