17. Juegos multiusuario. AJAX polling con CakePHP y Couperin

¡Hola, compi! He empezado a dar mis primeros pasos con AJAX hace poco y por momentos me pierdo porque no tengo muy bien asimilados algunos de los conceptos. Proceso de cliente por aquí, tiempo de espera por acá, proceso de servidor por allá… Todo un lío, vaya. Sé que hay que ordenar todas estas cosas de algún modo; de hecho, me ha dado por pensar que una aplicación AJAX es algo así como una orquesta cuyo director somos nosotros, los programadores. ¿Te gusta esta analogía?

Sí, como también coincide que últimamente me ha dado por escuchar a Couperin (a quien recién descubrí, por cierto), inserto a continuación este video que espero que te guste. A ver si su música nos ayuda a estimular el sistema cognitivo y nuestras aplicaciones AJAX dejan de lanzar errores por todas partes, como las mías ahora.

Pero vayamos al grano. Resulta que estoy trabajando en un juego multiusuario que implementa la técnica AJAX polling. Por cierto, estoy casi convencido de que sería mejor utilizar otras técnicas -long polling, sockets o COMET- pero finalmente utilizo esta porque para mí es la más sencilla. Bueno, también me tranquilizó un poco saber que esto puede hacerse así y que está muy bien para acercarse a AJAX long polling y a COMET.

¿Qué es AJAX polling?

AJAX polling es una técnica de programación que consiste en solicitar a intervalos de tiempo regulares la ejecución de la lógica determinada que implementa el servidor. El principal inconvieniente de esta técnica es su escalabilidad porque el número de peticiones puede llegar a incrementarse drásticamente si ese intervalo regular de tiempo es pequeño. Por ejemplo, si hay 1000 usuarios conectados a la aplicación y cada uno de ellos hace una petición por segundo, entonces el servidor recibe 2000 peticiones por segundo.

Además de esto, otra de las desventajas de AJAX polling es que el servidor siempre responde a las peticiones que recibe de los clientes, independientemente de si tiene alguna información nueva que entregarles. En otras palabras, los clientes siempre ejecutan la lógica de turno (actualizan la página cada segundo, por ejemplo) cada n segundos aunque no sea necesario.

Implementación de AJAX polling con CakePHP

Finalmente, mi aplicación multiusuario tiene varias tablas a las que acceden los jugadores de forma dinámica. Además de consultar y actualizar estas tablas, los jugadores calculan sus puntuaciones a partir de ellas y las visualizan en su pantalla en tiempo real. He implementado esta idea utilizando el método remoteTimer:


   echo $ajax->remoteTimer (  array(   'url' => array('controller' => 'multijugadores', 'action' => 'preguntar_sistema'),
                                       'update' => 'aciertos',
                                       'frequency' => 1
                                    )
                           );

Este método, envoltorio de los objetos Prototype PeriodicalExecuter y Ajax.Updater, pide cada segundo la ejecución de la acción preguntar_sistema. Esta acción es la que se encarga de realizar las consultas a las tablas comunes (un conjunto de tablas que todos los usuarios comparten) e imprimir las puntuaciones, calculadas a partir de éstas.

Estoy trabajando en local, la aplicación funciona y de momento no sé qué consecuencias puede tener esta implementación en el rendimiento del servidor compartido al que estoy a punto de subirla. ¿Tienes alguna experiencia con aplicaciones de este tipo? Si es así, cuéntanos… 😉

¿Y la música… De dónde sale?

Bueno, compi, pues Couperin aparece para ayudarme a abordar el problema de una forma no racional. Me refiero al problema de definir la idea de la aplicación multiusuario. Sí, Couperin es la fuente de inspiración que ha derribado las barreras lógica, hipotética y deductiva de la perspectiva izquierda de mi pensamiento y me ha ayudado a crear la idea. Seguro que las hay mejores (¡explícanos alguna, si las conoces!), pero estoy contento porque esta funciona. Espero que te animes a probarla y, si quieres, la comentamos aquí.

16. Borrando de la memoria cache las páginas privadas de la aplicación

El origen

A raíz de este hilo en el oráculo, el Grupo Google CakePHP en español, y gracias a la respuesta que obtuve de Joaquin Windmüller, he implementado un componente para borrar de la memoria cache las páginas privadas. Todo esto empezó cuando se me ocurrió probar mi primer programa.

¿Qué puede escribir un usuario en su barra de direcciones?, pensaba. Exacto, pensaba lo mismo que en este otro hilo de CakePHP en español: voy a salir del programa y voy a escribir en la barra de direcciones de IE7 un URL privado, ¡a ver qué pasa! Por cierto, aunque lo intento, creo que todas las cosas que pasan por mi cabeza en el momento de hacer estas cosas nunca van a ser 100% reales. ¿Qué harán los usuarios delante de su PC? ¡Hacen falta muchas personas!

Haciendo las pruebas

El caso es que si en IE7 escribía un URL privado después de salir del programa, podía acceder al contenido, mientras que, por ejemplo, en Firefox 3.0.3, esto no pasaba. No he probado otros navegadores y supongo que será cuestión de armar un listado con los más utilizados para ver qué sucede.

Mi intento por arreglar esto, que, funcionar, funciona. ¿Será mejor el remedio que la enfermedad?

Según he entendido, muchos agentes de usuario guardan en su cache el contenido que reciben por HTTP. Sin embargo, uno puede cambiar este comportamiento predeterminado modificando los campos de la cabecera de respuesta. Más concretamente, y siguiendo las indicaciones de la documentación oficial de php, he escrito este componente:


class LimpiarcacheComponent extends Object{
   function limpiar(){
      header("Cache-Control: no-cache, must-revalidate");
      header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
   }
}

Este componente sirve para no guardar en la memoria cache las páginas privadas que sirve la aplicación CakePHP y lo primero que tenemos que hacer es cargarlo en la variable $components del controlador que sirve páginas privadas:


var $components = array('Limpiarcache');

A continuación, llamamos al método limpiar() al comienzo de las acciones que sirven contenido privado porque lo primero que queremos hacer es añadir estos campos a la cabecera del mensaje de respuesta HTTP. Por ejemplo, la acción que arma un listado de categorías quedaría así:


/**
       Método para listar y gestionar las categorías.
   */
   function listar(){
      $this->Limpiarcache->limpiar();
      /* Poblamos la variable $categorias de la vista listar.ctp. */
      $this->set('categorias', $this->paginate('Categoria'));
      $this->render('listar','zonaprivada');
   } // fin del método listar()

Si ahora usamos la aplicación CakePHP, salimos y, finalmente, escribimos el URL de alguna página privada, entonces ya no podemos acceder a su contenido. A nivel de HTTP, esto se traduce en un mensaje de respuesta con código de estado 302. Por ejemplo, para el URL de la acción que arma el listado de las categorías, http://www.midominio.com/usuarios/categorias/listar, el mensaje de respuesta HTTP es este:

HTTP/1.1 302 Found
Date: Thu, 15 Jan 2009 12:24:43 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.2.6
Set-Cookie: CAKEPHP=lq2pgsj7elrgifonjs1dd5ofh3; expires=Thu, 22 Jan 2009 12:24:43 GMT; path=/
P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"
Location: http://www.midominio.com/usuarios/login
Content-Length: 0
Connection: close
Content-Type: text/html
HTTP/1.1 200 OK
Date: Thu, 15 Jan 2009 12:24:43 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.2.6
Set-Cookie: CAKEPHP=c7j1jsf6lpbsnd53vr6cdnfui2; expires=Thu, 22 Jan 2009 12:24:44 GMT; path=/
P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"
Content-Length: 6528
Connection: close
Content-Type: text/html

Mis dudas

Aún así, no me acaba de quedar muy claro el por qué de este código de respuesta 302. Consultando las definiciones de los códigos de respuesta HTTP 1.1 del RFC 2616, uno puede leer esto para el 302:

“The requested resource resides temporarily under a different URI. Since the redirection might be altered on occasion, the client SHOULD continue to use the Request-URI for future requests. This response is only cacheable if indicated by a Cache-Control or Expires header field. The temporary URI SHOULD be given by the Location field in the response. Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s). If the 302 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.”

El final

Resumiendo, ¿qué ha pasado? Yo puedo leer hasta aquí. Funcionar, parece que funciona, y espero que este componente sirva para la mayoría de agentes de usuario. Si te animas a usarlo, recuerda: tú eres el único responsable del error catastrófico que puede producirse; si puedes ampliar o aclarar todo esto un poco, ¡deja aquí algún comentario!

15. Comportamientos (o behaviors) en CakePHP. Comportamiento árbol (1).

El comportamiento árbol y las ACL

Estoy en un proyecto en que algo más adelante surgirá probablemente la necesidad de clasificar y presentar el contenido del sitio web en función del tipo de usuario. Para hacer esto, CakePHP viene con el comportamiento ACL (Access Control List, o lista de control de acceso, es un concepto informático que se utiliza para separar privilegios y determinar permisos de acceso). Por cierto, un apunte de última hora: a raíz de este hilo sobre este tema en el grupo Google CakePHP en español me acabo de enterar que IMHO quiere decir In my honest opinion; también puede ser que las ACL no sean necesarias para esto.

Navegando por Internet, y leyendo un poquito sobre este tema, he descubierto que CakePHP 1.2 incorpora cuatro comportamientos o behaviors: ACL, Containable, Translate y Tree. Como resulta que no puedo ir directamente al comportamiento ACL porque es un poco complicado y utiliza Tree, que es más sencillo, he hecho algunas pruebas con este último. Básicamente, en este post pongo en práctica la explicación del manual oficial de CakePHP, que está muy bien.

¿Qué es un comportamiento?

Hasta ahora nunca hemos hablado de los comportamientos y vamos a presentarlos. Del mismo modo que los componentes extienden la funcionalidad de los controladores y los ayudantes extienden la de las vistas, los comportamientos de CakePHP sirven para extender la funcionalidad de los modelos (recordemos que un modelo CakePHP es una clase ActiveRecord que representa una tabla de la base de datos con todo el código PHP para acceder, añadir, modificar y borrar los registros de ésta). El manual de CakePHP los define muy bien: “Los comportamientos son una manera de organizar parte de la funcionalidad definida en los modelos de CakePHP”.

El comportamiento árbol

El comportamiento árbol -o Tree behavior– sirve, por ejemplo, para montar una jerarquía de categorías. Para poder utilizar esta funcionalidad de CakePHP sólo tenemos que añadir tres campos extra a la tabla de la base de datos: padre_id, izquierdo y derecho. Más concretamente, el SQL para la tabla es este:


create table categorias(

id integer(10) unsigned not null auto_increment,

padre_id integer(10) default null,

izquierdo integer(10) default null,

derecho integer(10) default null,

nombre varchar(255) default ' ',

primary key(id)

);

Continuará…

14. Hacia las ACL: más sobre ActiveRecord y CakePHP

Dentro de poco empiezo con las listas de control de acceso de CakePHP y creo que la mejor forma de hacerlo (aunque me temo que antes tengo que pasar por los behaviors) es con la explicación del manual oficial y reescribiendo el post anterior, 13. ActiveRecord en CakePHP y ORM. Me estoy preparando psicológicamente porque, por algunas impresiones que he podido recoger, las listas de control de acceso son de lo más complicado que hay en CakePHP.

Ha pasado un tiempo y he asimilado mejor los conceptos gracias al libro CakePHP Application Development, de Ahsanul Bari y Anupom Syam, editado por PACKT. En realidad, no sé si el tiempo ha hecho que entienda mejor el funcionamiento de los patrones de diseño de software, o son las explicaciones que hacen Ahsanul Bari y Anupom Syam. Yo creo que se trata de esto último; en la página 6 del primer capítulo presentan los patrones de diseño de software en CakePHP así:

“Un patrón de diseño es una solución general a un problema común del desarrollo web. Un patrón de diseño no es un código completo; más bien es una descripción de la solución de un problema, que además puede reutilizarse en situaciones diferentes. En el desarrollo web hay muchos patrones de diseño que se utilizan para solucionar problemas repetitivos y comunes. CakePHP integra muchos de estos patrones de diseño: ActiveRecord, Association Data Mapping, Front Controller y MVC”.

Es decir, lo que a mí me hubiera gustado escribir (y no acababa de entender) en 13. ActiveRecord en CakePHP y ORM: “Con la capa de abstracción de Cake, ya no necesitamos escribir consultas SQL para recuperar o modificar los datos; llamando a las funciones apropiadas del modelo podremos acceder fácilmente a los datos.” 

librocake

Más adelante, en el capítulo 5, Models: Accessing Data, definen los modelos CakePHP como implementaciones PHP del popular patrón de diseño ActiveRecord; también dicen que los modelos CakePHP son mucho más que simples capas de abstracción de la base de datos. Resumiendo, y personalmente, todas las explicaciones, en general, me gustan mucho.

Es una pena que en CakePHP Application Development no se profundice en el funcionamiento de las ACL. De todas formas, no pasa nada porque he apuntado estos dos recursos para las lisas de control de acceso: la explicación del manual oficial y el capítulo 8 del libro Practical CakePHP Projects, editado por Appress. A ver si en futuros posts puedo reflejar algún avance sobre este oscuro tema de CakePHP.

13. ActiveRecord en CakePHP y ORM

Son las siete de la tarde de un jueves 13 de noviembre de 2008, Google sólo tiene 1570 páginas con las palabras ActiveRecord y CakePHP, y yo salgo del armario. Sí, ¿qué pasa?. Hasta estos días no sabía exactamente qué era ActiveRecord, ni cómo lo implementaba CakePHP. ¿Y tú? Además, ¡resulta que CakePHP es un framework ORM! Esto es increíble y lo voy a apuntar en esta lista.

El origen del problema

Empecé creando estos modelos:

class Empresa extends AppModel{
   var $name = 'Empresa';
   var $validate = array(  'empresa'=>VALID_NOT_EMPTY,
                           'localidad'=>VALID_NOT_EMPTY,
                           'provincia'=>VALID_NOT_EMPTY,
                           'cp'=>VALID_NOT_EMPTY,
                           'telefono'=>VALID_NOT_EMPTY,
                           );
 

   var $belongsTo = array('Categoria'=>array('className'=>'Categoria'));

   var $hasMany = array('Notaprensa'=>array('className'=>'Notaprensa'));

   var $displayField = 'empresa';

} 
class Notaprensa extends AppModel{
   var $name = 'Notaprensa';
   var $validate = array('nota'=>VALID_NOT_EMPTY,);
   var $belongsTo = array('Empresa'=>array('className'=>'Empresa'));
}

Aquí se ve que la clase Empresa implementa una relación de tipo hasMany, y, Notaprensa, una de tipo belongsTo. Pues bien, yo quería obtener un listado de notas de prensa donde apareciera también el nombre de la empresa de cada nota.

Hacía poco había utilizado el ayudante HTML selectTag para rellenar la lista desplegable de una vista y mi tendencia instintiva -por así decirlo- fue razonar por analogía, sin darme cuenta. En otras palabras, si la lista desplegable de una vista se rellenaba con un ayudante HTML, ¿qué impedía utilizar otro para imprimir cualquier campo de un registro relacionado y no su identificador? Es decir, me pasó lo mismo que a Fran. Por cierto, también hay que decir que son varios los principiantes del Grupo Google CakePHP -en inglés- que encuentran algo confuso este ayudante HTML, como hacen aquí.

¿Y cómo es que es esto?

Estos enlaces me han servido para darme contexto y situarme; para saber un poco de qué va todo esto de la magia de CakePHP.

http://es.wikipedia.org/wiki/Mapeo_objeto-relacional

http://es.wikipedia.org/wiki/ActiveRecord

http://metodologiasdesistemas.blogspot.com/2007/10/que-es-un-orm-object-relational-mapping.html

http://www.mononeurona.org/pages/display/747

Resumen

ActiveRecord es el nombre de un patrón de diseño de software que consiste en envolver la tabla de una base de datos relacional en una clase. Este patrón propone una solución al problema de acceder a la base de datos y funciona de tal forma que la fila de una tabla se asocia a un objeto. El framework CakePHP, que se inspira en Ruby, implementa el patrón de diseño ActiveRecord a través de su clase Model; Ruby -que personalmente no conozco- lo hace mediante su clase ActiveRecord. Model y ActiveRecord son las clases de mapeo objeto-relacional de CakePHP y Ruby on Rails, respectivamente. Se puede decir que estas dos clases son equivalentes.

La clase Model de CakePHP implementa el método findAll(). Este método devuelve una matriz asociativa que contiene los registros que cumplen las condiciones que se le especifican. Gracias a la capa de abstracción que proporciona el patrón ActiveRecord -que implementa CakePHP a través de Model- la matriz asociativa que devuelve findAll() contiene registros relacionados, accesibles al programador.

12. Validación de datos en CakePHP 1.1

Hasta ahora hemos dado nuestros primeros pasos en CakePHP de la mano del ejemplo de la base de datos de la discografía y ha llegado el momento de despedirnos de él, aunque siempre puede uno releer los posts anteriores para repasar.

Vamos a ver cómo se validan los datos en CakePHP a través de otro ejemplo que trabaja con una tabla de usuarios y que se puede crear con el siguiente script MySQL:


create table usuarios (

id mediumint unsigned NOT NULL auto_increment,

nombre varchar(100) NOT NULL,

telefono varchar(9) NOT NULL,

email varchar(100),

primary key(id)

);

Una vez tenemos la tabla, lo primero que debemos hacer es definir las reglas de validación en el modelo.


class Usuario extends AppModel{

   var $name = 'Usuario';

   var $validate = array(  'nombre'=>VALID_NOT_EMPTY,
                           'telefono'=>VALID_NOT_EMPTY,
                           'email'=>VALID_EMAIL,
                           );

}

En este ejemplo hemos utilizado las validaciones predeterminadas de CakePHP VALID_NOT_EMPTY y VALID_EMAIL. Además de estos dos tipos de validación, CakePHP 1.1 cuenta con otros dos: VALID_NUMBER y VALID_YEAR.

Vamos a crear esta vista para ver el efecto de las dos validaciones que acabamos de crear:

<h1>Nuevo Usuario</h1>
<form method="post" action="<?php echo $html->url('/usuarios/anadir_usuario')?>">
   <fieldset>
      <div class="required">
         <label for="usuario_nombre">Nombre:</label>
         <?php echo $html->input('Usuario/nombre', array('size' => '40'))?>
         <?php echo $html->tagErrorMsg('Usuario/nombre', 'El nombre es obligatorio'); ?>
      </div>
      <div class="required">
         <label for="usuario_telefono">Telefono:</label>
         <?php echo $html->input('Usuario/telefono', array('size' => '40'))?>
      </div>
      <div class="required">
         <label for="usuario_email">E-mail:</label>
         <?php echo $html->input('Usuario/email', array('size' => '40'))?>
         <?php echo $html->tagErrorMsg('Usuario/email', 'El e-mail es obligatorio'); ?>
      </div>
</fieldset>
<?php echo $html->submit('Añadir usuario') ?>
</form>

En la vista hemos especificado los campos obligatorios -“required”-. Por una parte, para que la magia de CakePHP funcione, el método validates de la clase Controller comprueba las validaciones que se añaden al modelo, y, por otra, el método validationErrors() de Controller devuelve los mensajes de error que lanza el modelo. El método tagErrorMsg del objeto $html muestra estos errores en la vista. Puedes repasar cómo funcionan los ayudantes HTML aquí.

11. Relaciones en las tablas artistas y discos: error de convención

Después de crear las vistas index.thtml y add.thtml es el momento de definir las relaciones que existen en la base de datos. Según la documentación oficial de CakePHP, “una de las características más potentes es el mapeo relacional que proporciona el modelo. En CakePHP, las asociaciones manejan los enlaces entre las tablas”. Como en la base de datos discografía sólo hay dos tablas -artistas y discos- y la relación que existe entre ellas es de tipo uno a muchos -un artista puede grabar varios discos y un disco pertenece sólo a un artista-, escribimos lo siguiente en los modelos Artista y Disco.

En el modelo Artista:

class Artista extends AppModel{
   // Es una buena práctica incluir esta variable
   var $name = 'Artista';
   // Información sobre las asociaciones del modelo
   var $hasMany = array('Disco'=>array('className'=>'Disco'));
}

En el modelo Disco:

class Disco extends AppModel{
   // Es una buena práctica incluir esta variable
   var $name = 'Disco';
   // Información sobre las asociaciones del modelo
   var $belongsTo = array('Artista'=>array('className'=>'Artista'));
}

 

Escribí este código y, al escribir http://localhost/discografia/artistas, obtuve estos errores:

Query: SELECT `Disco`.`id`, `Disco`.`nombre`, `Disco`.`ano`, `Disco`.`artista_id` FROM `discos` AS `Disco` WHERE `Disco`.`artista_id` IN (1, 2, 3)

Warning: SQL Error: 1054: Unknown column ‘Disco.artista_id’ in ‘field list’ in C:\wamp\www\discografia\cake\libs\model\datasources\dbo_source.php on line 439
Warning: Invalid argument supplied for foreach() in C:\wamp\www\discografia\cake\libs\model\datasources\dbo_source.php on line 757

Esto pasó porque en el momento de crear las tablas no contemplé el hecho de que existe una relación entre ellas: tendría que haber creado una clave externa en la tabla discos, para que apuntara a la tabla artistas. Para hacer esto, hay que respetar las convenciones que establece CakePHP: todas las tablas deben tener un campo id para su clave primaria, y, como se va a utilizar una clave externa en la tabla Discos, el nombre del campo tiene que ser artista_id. Qué mejor sitio que el oficial para ver más información sobre las convenciones: http://book.cakephp.org/es/view/22/Convenciones-de-CakePHP.

Después de darme cuenta de esto, borré las tablas artistas y discos y las volví a crear. Finalmente quedaron así:

A continuación aproveché para insertar, con el comando insert, algunos registros desde el monitor MySQL y creé la vista index.thtml para la tabla discos; escribí este código, que imprimió el identificador del artista sin lanzar ningún error:

<h1>Discos de discografia</h1>
<table>
       <tr>
           <th>Disco</th>
       </tr>
       <?php foreach ($discos as $disco): ?>
       <tr>
           <td>
               <?php
                   echo $disco['Disco']['nombre'];
                   echo $disco['Disco']['artista_id'];
               ?>
           </td>
       </tr>
       <?php endforeach; ?>
</table>