Introducción al manejo de eventos con Symfony
Juan Pablo Romero
Software EngineerEl tratamiento de eventos en el desarrollo de aplicaciones es algo muy común, para la muestra algunas situaciones típicas:
- Necesitamos enviar un correo electrónico al usuario cuando se registra
- Es necesario registrar las acciones que ejecutan los usuarios (creación y actualización de información)
- Cada vez que un usuario sube un archivo o califica algún contenido (imagen, artículo, etc.) se debe registrar la calificación junto con la hora y el usuario.
y se pueden seguir listando muchas otras situaciones (dependiendo del tipo de aplicación). Una de las más usuales y cuya implementación es poco recomendable (desde mi punto vista) es cuando queremos registrar qué usuario creó y/o actualizó determinado registro en nuestra base de datos y el procedimiento a seguir es la sobrecarga del método save() de las clases (generadas por Doctrine o Propel) que componen nuestro modelo, agregando código que no corresponde con la responsabilidad de la clase. Otra posible implementación es agregar código en las acciones (actions.class.php) que se encargue de hacer el registro de los datos mencionados, haciendo más extensos los métodos y más complicados de leer.
Bien, para tratar de una mejor manera estos eventos existe un componente llamado
(el cual viene integrado en Symfony desde su versión 1.1) y que hace parte de una serie de
que proveen los desarrolladores de Symfony. Este componente es una implementación ligera del patrón Observador y que permite de forma muy flexible definir y notificar eventos en nuestra aplicación.
El objeto Dispatcher y los eventos
El Event Dispatcher se compone de dos objetos fundamentales: Dispatcher (clase sfEventDispatcher) y eventos (clase sfEvent). El Objeto Dispatcher es el objeto central encargado de recibir todas las notificaciones y registros de los eventos y disparar los listeners asociados a dichos eventos. Los eventos (instancias de la clase sfEvent) se describen a través de una cadena (por ejemplo: usuario.login), no es necesario implementar interfaces o crear nuevas clases que hereden de sfEvent para crear un evento. Es importante saber que el constructor de un objeto sfEvent toma tres argumentos:
- El subject (quien notifica el evento)
- El nombre del evento
- Un arreglo de parámetros (que se pasarán al listener y puede ser nulo)
Bien, una vez creado nuestro evento es necesario hacerle saber al objeto Dispatcher la existencia del mismo a través del método connect y posteriormente notificar su ocurrencia. Ahora veamos esto en la práctica.
Implementando el evento
Tomaremos como referencia que tenemos un proyecto con el plugin de sfGuard para el control básico de usuarios y que queremos registrar en una tabla de auditoria cada vez que un usuario crea un producto. A continuación la descripción del esquema de las tablas de producto, tipo de producto y auditoria:
Producto:
tableName: Producto
columns:
id:
type: integer(4)
fixed: false
unsigned: false
primary: true
autoincrement: true
nombre:
type: string(255)
fixed: false
unsigned: false
primary: false
notnull: true
autoincrement: false
tipoproducto_id:
type: integer(4)
fixed: false
unsigned: false
primary: false
notnull: true
autoincrement: false
relations:
TipoProducto:
local: tipoproducto_id
foreign: id
type: one
TipoProducto:
connection: doctrine
tableName: TipoProducto
columns:
id:
type: integer(4)
fixed: false
unsigned: false
primary: true
autoincrement: true
nombre:
type: string(255)
fixed: false
unsigned: false
primary: false
notnull: true
autoincrement: false
relations:
Producto:
local: id
foreign: tipoproducto_id
type: many
Auditoria:
connection: doctrine
tableName: auditoria
actAs: [Timestampable]
columns:
id:
type: integer(4)
fixed: false
unsigned: true
primary: true
autoincrement: true
registro_id:
type: integer(4)
fixed: false
unsigned: true
primary: false
notnull: true
autoincrement: false
entidad:
type: string
sf_guard_user_id:
type: integer(4)
fixed: false
unsigned: false
primary: false
notnull: false
autoincrement: false
relations:
SfGuardUser:
local: sf_guard_user_id
foreign: id
type: one
Con este modelo en mente, vamos a generar los módulos para el crud de productos y tipos de productos, a través de la tarea:
php symfony doctrine:generate-module frontend productos Producto
php symfony doctrine:generate-module frontend tiposproducto TipoProducto
Ahora, vamos a crear el evento y a notificarlo al objeto Dispatcher, modificando el método que guarda el producto (método processForm) de la clase productoActions como sigue:
protected function processForm(sfWebRequest $request, sfForm $form)
{
$form->bind($request->getParameter($form->getName()));
if ($form->isValid()) {
try
{
$producto = $form->save();
// Se crea el evento y se registra en el dispatcher
$this->dispatcher->notify(new sfEvent($this, 'producto.create',
array('entidad' => 'Producto', 'id' => $producto->getId())));
$this->getUser()->setFlash('save_ok',
'Registro creado/editado correctamente');
} catch (Doctrine_Exception $pe) {
$this->getUser()->setFlash('exception',
'Ocurrió un error al momento de agregar/editar el registro');
}
$this->redirect('productos/index');
}
}
}
Bien, la línea 9 es la importante:
$this->dispatcher->notify(new sfEvent($this, 'producto.create',
array('entidad' => 'Producto', 'id' => $producto->getId())));
Cuando se crea el objeto sfEvent se le están pasando tres argumentos:
- Subject: en nuestro caso quien notifica el evento es la clase productoActions, por eso usamos $this
- Nombre del evento: hemos llamado al evento: producto.create
- Arrreglo de argumentos: pasaremos como argumentos al listener la entidad que estamos creando (entidad) y el id.
Una vez creado el evento, accedemos la instancia del objeto dispatcher y usamos el método notify (para otras formas de notificación se pueden usar los métodos notifyUntil y filter, ver más en la documentación del componente), para notificar que un producto ha sido creado.
Ahora, es necesario que asociemos un listener a ese evento (pues hasta el momento la notificación no sirve de nada). Para esto usamos el método connect del Dispatcher. Vamos a agregar esta llamada dentro de la clase que configura nuestra aplicación (en este caso en el archivo frontendConfiguration.class.php):
class frontendConfiguration extends sfApplicationConfiguration
{
public function configure()
{
$this->dispatcher->connect('producto.create', array('Auditoria','registrarCreacion'));
}
}
El método connect recibe el nombre del evento y una función o método de una clase que será llamado cuando el evento sea notificado. En este caso, estamos pasando un arreglo como segundo argumento indicando que se llamará el método registrarCreacion de la clase Auditoria, aunque también es posible pasar solamente el nombre de una función (debemos asegurarnos que el archivo donde se encuentre la definción de esta función haya sido cargado). Si utilizamos el método de una clase, este debe estar declarado como estático. Ahora bien, revisemos el código del método registrarCreacion:
class Auditoria extends BaseAuditoria
{
public static function registrarCreacion(sfEvent $event){
$usuario = $event->getSubject()->getUser()->getGuardUser();
$a = new Auditoria();
$a->setRegistroId($event['id']);
$a->setEntidad($event['entidad']);
$a->setSfGuardUserId($usuario);
$a->save();
}
}
Una vez se notifica el evento y se llama al método registrarCreacion a este se le pasa como primer argumento el objeto sfEvent correspondiente, desde el cual podemos acceder el arreglo de argumentos que se pasaron al momento de notificar el evento (en el método processForm), como se ve en:
$a->setRegistroId($event['id']);
$a->setEntidad($event['entidad']);
Listo, con este método creado, cada vez que un producto sea creado, se registrará en la tabla auditoria el usuario quien lo creó junto con la fecha y la hora. Esto mismo puede aplicarse para envio de correo (usando SwitfMailer), envio de mensajes al usuario o a otras clases, todo dependiendo el gusto del desarrollador. Espero que sea de utilidad y como siempre, los comentarios serán bien recibidos.
Written by Juan Pablo Romero
Juan Pablo designs and builds robust software solutions with a focus on performance and usability. His problem-solving skills and attention to detail ensure high-quality and efficient applications.