sábado, 28 de abril de 2012

Crear un RSS con CakePHP 1.3

Hace tiempo escribí un tutorial sobre cómo generar feeds RSS en CakePHP pero ya está muy desactualizado. Así que ahí va otro un poco más moderno, que vale para la versión 1.3. Tampoco es que esté muy actualizado, ya que aún no me he planteado migrar a CakePHP 2, pero bueno, espero que  pueda ser útil.

Además, como bonus, intentaré explicar cómo voy resolviendo el tema de que el feed sea validado en el feed validator del W3C, cosa que tiene su dificultad ya que la especificación RSS es un tanto laxa en algunos aspectos.


Paso 1: preparar la aplicación Cake para responder a las peticiones de feeds

Básicamente hay que decirle a CakePHP que queremos parsear urls con extensiones, de la forma: /controller/action.rss. Esto es así porque la versión 1.2 de CakePHP puede seleccionar al vuelo diferentes vistas en función de la extensión que le pasemos. Por lo tanto, en un paso posterior crearemos una vista específica para el feed y la pondremos en el sitio adecuado para que la use Cake.

En tu /app/config/routes.php tienes que añadir:


Router::parseExtensions('rss');

Esto le indica a Cake que si se encuentra una URL con la extensión 'rss' busque las vistas en la carpeta /views/models/rss. De este modo, puedes tener una misma acción que responde de manera diferente a una petición html o a una petición de rss.

Podemos no especificar ninguna extensión, para que parsee cualquiera, o bien dar una lista limitada de ellas 'rss', 'xml'...

Otra ventaja es que CakePHP seleccionará automáticamente un layout específico para rss, que podemos sobreescribir en /views/layouts/rss/default.ctp, o crear otros alternativos con otros nombres. En ese último caso hay que recordar poner en la acción del controlador un paso $this->layout = 'nombre del layout alternativo'; para que la aplicación sepa cuál queremos usar.

Paso 2: cosas para hacer en el Controller

Hay varias, así que vayamos por partes.

Lo primero es habilitar el uso del componente RequestHandler en el controlador. Aparte de para este tema de los feeds es útil siempre que necesites obtener información sobre la petición que está recibiendo el controlador. Lo segundo es indicar que quieres utilizar el RSSHelper, claro.

var $components = array('RequestHandler');
var $helpers = array ('RSS');

Una cosa opcional: El componente HandleRequest te permite comprobar si el usuario está pidiendo un RSS (u otra cosa) y actuar en consecuencia. Por ejemplo, si es para el feed obtener sólo los últimos 15 items que cumplan ciertas condiciones y si no obtener todos. En cualquier caso, usarías:

if ($this->RequestHandler->prefers('rss')) {
    // Cosas para hacer si es rss
} else {
    // Cosas para hacer si no lo es
}

Lo segundo es enviarle los datos a la vista del feed (que aún no hemos creado, ¡ojo!). Lo puedes hacer así:

Configure::write('debug', '0');
$data = $this->Paginate ();
$this->set('channelData', array ('title' => 'Ejemplo',
  'link' => 'http://localhost:8888/micake/',
  'description' => 'Frases y citas célebres para que las puedas leer'
));
$this->set ('frases', $data);

Lo explico un poco:

La primera línea es para desactivar el Debug (en el caso de que lo tengas distinto de 0, como ocurre en un entorno de pruebas o de desarrollo). Con Debug, Cake añade cosas a la salida generada y los feeds no validarían de ninguna forma aunque estuviesen bien construidos. Opcionalmente puedes poner un if para controlar si es necesario hacerlo o no.

La segunda línea es para recabar los datos de la manera habitual.

La tercera línea ajusta los valores para el channel. El layout por defecto para feeds rss espera que definas una variable $channelData como un array con claves que serán los elementos del channel (como title, link, description y los demás). Si no la defines, CakePHP se busca la vida para poner algunos. Sin embargo, puede que en la vista tengamos que hacer algunos ajustes a esta información antes de enviarla al Layout.

La cuarta línea pasa el array de datos que serán los ítems del modelo. En este caso, la variable es $frases, pero se supone que esto ya sabías hacerlo. Aquí no vamos a hacer nada más.

Paso 3. Preparando los datos

Los feeds rss tienen una estructura relativamente sencilla, puedes consultar la especificación para tener todos los detalles. Cada uno lleva la información de un canal (que puede ser un blog), el cual puede contener varios ítems (cada una de las entradas del blog). Cada uno de estos elementos puede contener otros, como descripción, autor, link y algunos más. Según el caso puede que tengas la información del canal en un modelo, y la de los ítems en un modelo relacionado.
Canal

El canal debería proporcionar como mínimo los siguientes campos:
  • title: El título o nombre del canal
  • description: Un texto breve que describa o resuma el contenido del canal
  • link: El enlace a la página principal del canal
Aparte de eso, hay un par de campos que son recomendables:

docs: Un enlace a la especificación de RSS (Yo uso http://blogs.law.harvard.edu/tech/rss)
atom:link: Este elemento está recomendado para mejorar la interoperabilidad del feed con las aplicaciones lectoras.

$channel['atom:link'] = array(
     'attrib' => array( 
             'href' => Router::url(null, true), 
             'rel' => 'self', 
             'type' => 'application/rss+xml' 
));

Este elemento contiene varios atributos y, como puedes ver, se indican en forma de array asociativo. Básicamente href es un enlace al propio feed, que podemos obtener fácilmente usando la clase Router, y pasándolo el parámetro nulo para que utilice la url que genera el propio feed.

En mi caso particular, he añadido estos campos en el layout /views/layouts//rss/default.ctp ya que se generan igual para cualquier feed de mi aplicación y, de este modo, me puedo olvidar de ellos en cada vista concreta.

Otra cuestión que hay que controlar es que ni title ni description deberían contener HTML. Respecto a title, suele ser normal tener un campo de título de texto puro. Pero en description puede ocurrir que los usuarios añadan algo de HTML si el sistema lo permite, como es mi caso. Lo mejor es utilizar la función de php strip_tags, que realiza el trabajo sin más problemas:

$channel['description'] = strip_tags($channel['description']);

Esta transformación también la hago en la vista (no en el controlador) porque se refiere al modo en que se va a mostrar la información en el documento final.

Items

Los items se han de pasar a la vista en forma de array de arras a partir de datos del modelo que corresponda. El proceso  de los items en la vista se realiza mediante un bucle foreach, y la tarea que hay que hacer con cada ítem es generar correctamente los elementos hijos necesarios:
  • title
  • description
  • link
  • guid
  • pubDate
  • author
  • enclosure
RSSHelper incluye un método que nos devuelve un elemento ítem correctamente formado si le pasamos un array asociativo con estas claves.

Vamos a ver ahora qué tipo de contenido tiene cada clave y como habría que prepararlo.

Title es una cadena de texto puro, así que no debería tener más complicaciones. Usa strip_tags si es necesario.
Description sería un resumen del contenido. Sin embargo, puedes querer incluir el contenido completo del artículo en cuestión, incluyendo el HTML. El RSSHelper te ayudará a codificar correctamente el HTML, pero tendrás que asegurarte de que algunas etiquetas no son incluidas, como iframe, object o script. La solución es pasar este campo por Sanitize::stripTags

$item['description'] = Sanitize::stripTags($post['Post']['content'], 'iframe', 'object', 'param', 'script');

link: es un enlace al artículo. Puedes usar una URL en forma de array de CakePHP, ya que el RSSHelper lo reconoce y lo procesa, y te beneficias del Reverse Routing si tienes rutas personalizadas.
guid: se trataría de un identificador global único para el artículo, el cual podría ser perfectamente su url. Pero para hacerlo bien debes pasar un formato particular:

$item['guid']  = array( 
    'url' => array(
             'controller' => 'posts', 
             'action' => 'view', 
             $post['Post']['slug'] ), 
    'isPermaLink' => 'true'
));

El atributo isPermalink indica que el guid se corresponde con el enlace permanente del recurso.
pubDate: se trata de la fecha de publicación del ítem.

Author se refiere al autor o autora del artículo. El dato principal es su email, de modo que el formato recomendado sería:

    correo@example.com (Autor del Item)

Sin embargo, no consigo que valide si pongo más de un autor.

Enclosure es un elemento opcional para indicar un contenido multimedia asociado. Tiene tres atributos, por lo que se pasa en forma de array con las claves:
  • url: que nos indicaria la URL del recurso multimedia
  • length: su tamaño
  • type: el mime type del recurso

Paso 4: preparando la vista, o cómo pasar tu modelo al feed

La vista tienes que ponerla en /views/nombre_del_modelo_plural/rss/nombre_de_la_action.ctp
En la vista basicamente lo que vamos a hacer son dos cosas: revisar la información de Channel, y pasarla al Layout, y procesar la información de cada registro para generar el Item correspondiente.

Para el layout, he copiado en APP/views/layouts/rss/default.ctp el layout que viene por defecto con el core de CakePHP, a fin de poder hacer algunas modificaciones. Lo detalle en el Paso 5.

En este caso, la información de Channel la ventilamos rápidamente asegurándonos de que description no contiene HTML. Luego pasamos la variable al layour, pero con el nombre $channelData, que es el que espera.

$channel['description'] = strip_tags($channel['description']);
$this->set('channelData', $channel);


Lo más normal es que hayamos pasado la información sobre los ítems en un array típico de resultados de un find. Tenemos entonces que recorrer ese array, registro a registro, y construir un array asociativo con los datos del ítem, por ejemplo:

foreach ($posts as $post) {
    $item['title'] = $post['Post']['title'];
    $item['description'] = Sanitize::stripTags($item['Post']['content'], 'iframe', 'object', 'param', 'script');

    // Otros campos
    echo $this->Rss->item(array(), $item).chr(10);
}



En algunos casos, los campos del registro pueden pasarse de forma directa. En otros casos, como description, necesitamos hacer algunos ajustes. En nuestro ejemplo, eliminamos algunos elementos HTML del contenido que no validan.


La vista resultante contiene los items del RSS listos para insertar en el layout.

Paso 5. Layout para RSS

A continuación, el código del layout que tengo en APP/views/layouts/rss/default.ctp

Rss->header(); ?>
"http://www.w3.org/2005/Atom");
}
if (!isset($channelData)) {
$channelData = array();
}
if (!isset($channelData['title'])) {
$channelData['title'] = $title_for_layout;
}
$channelData['atom:link'] = array('attrib' => array(
'href' => Router::url(null, true),
'rel' => 'self',
'type' => 'application/rss+xml'
));
$channelData['docs'] = 'http://blogs.law.harvard.edu/tech/rss';

$channel = $this->Rss->channel(array(), $channelData, $content_for_layout);
echo $this->Rss->document($documentData,$channel);
?> 


Lo más interesante de este código es que aprovecho esta instancia para incluir algunos parámetros que mejorarán las posibilidades de validación del feed.

Paso 6: Ya está, ¿cómo suscribirse?

Pues sí, el feed ya está listo para servir. La URL para suscribirse sería:

http://exemple.com/controller/action.rss

Sustituye lo que haga falta. Supongo que se podrían hacer rutas para que ciertas url se sirvan como feeds.

lunes, 23 de abril de 2012

Construyendo un permalink con el Router

Se supone que un Permalink es una URL que apunta a un recurso y que no va a cambiar con el tiempo, o dicho de otro modo, es la URL que nos lleva siempre a cierto recurso dentro de una web. Debe ser una URL absoluta.

En CakPHP la URL habría que construirla obteniendo la URL básica del sitio, añadiendo el controlador, la acción y los parámetros necesarios para identificar ese recurso.

En el supuesto de tener un modelo Post, con un campo "slug" (un slug actúa como un id significativo para un post, generado a partir de su título, lo que hace url del tipo /posts/view/el_libro_de_petete), puedes hacerlo así (este código está en la vista view del post):

Router::url(array ($post['Post']['slug']), true);

Es decir, al router nos basta pasarle dos parámetros: un array con datos sobre la URL y un flag para indicarle si queremos una URL absoluta o relativa.

La gracia del router es que es capaz de averiguar qué controlador y acción tiene que utilizar para completar la URL. En el ejemplo, el código está en la vista para la acción View del controlador Posts. Pero si estoy generando el Router en otra parte, podría pasarle

viernes, 13 de abril de 2012

ModSecurity y Ajax Upload

Al cambiar de hosting un proyecto me encontré con un problema pues dejó de funcionar mi módulo de subida de archivos.
Con la ayuda de los logs del servidor y del soporte técnico de Dinahosting localizamos que el problema tenía que ver con ModSecurity y, después de un buen rato de investigación, llegué a la conclusión de que la cusa tenía que estar en el código javascript, que en mi caso es el Valums Ajax File Uploader
La solcuión final es realmente curiosa. Hay que evitar que la petición Ajax especifique la cabecera Content-Type.

martes, 13 de marzo de 2012

Crear Models y Fixtures para Tests

Una de mis frustraciones con CakePHP es que la documentación para crear Test Unitarios no cubre bien todas las posibilidades. Encontrar algunas soluciones concretas requiere bastante investigación en el manual, API, código del core y en blogs dispersos.

Uno de estos puntos oscuros (para mí) es el Test de Behaviors. Los Behaviors, como sabrás, son extensiones a los Models. Normalmente para crear los tests necesitarás un modelo al que asociarlos y el problema es que, por lo general, este modelo debería ser independiente y específicamente creado para la ocasión, o lo bastante genérico como para no introducir factores indeseados en el proceso de test.

¿Cómo crear esos modelos? Pues esto es lo que voy a intentar contar en este artículo. Esto lo he aprendido estudiando el código de tests de Cake y, aunque no tengo claro que sea todo lo completo que desearía, al menos he conseguido que vaya funcionando para mis propósitos.

Models

Para empezar, los Model para Tests extienden la clase CakeTestModel (que a su vez desciende de Model y se ajusta para utilizar la configuración de base de datos de test por defecto),
Pero nuestros Models no deberían acceder a una base de datos "física", a fin de no introducir elementos de distorsión de los resultados del test.

Esto se consigue definiendo el esquema o estructura de datos en el propio modelo y especificando que no se use una tabla. Aquí tienes un ejemplo:

class Basic extends CakeTestModel {
    var $useTable = false;
    var $name = 'Basic';
    var $_schema = array(
        'id'=> array('type' => 'string', 'null' => '', 'default' => '1', 'length' => '36', 'key'=>'primary'),
        'title'=> array('type' => 'string', 'null' => '', 'default' => '', 'length' => '255'),
        'key'=> array('type' => 'string', 'null' => '1', 'default' => '', 'length' => '255'),
    );
}


El lenguaje de esquema de CakePHP es bastante sencillo y te permite definir los tipos de campos que necesites. En este caso, necesitaba dos campos de texto, aparte del campo id.

Pero, ¿dónde defino este modelo?

Pues se me ocurren dos ubicaciones. Si no piensas reutilizarlo, podrías definirlo en el mismo archivo que el test.

Pero si crees que puedes utilizarlo en tests de otros elementos de tu aplicación (otros Behaviors, Controllers, etc) es buena idea guardarlo en un archivo separado e incluirlo cuando sea necesario.
En mi caso, he creado un archivo models.php en la carpeta tests de la aplicación. En este archivo colecciono los modelos creados para tests. Para incluirlo, no tengo más que usar este código:

require_once(TESTS . DS . 'models.php');

Y luego crear una instancia del modelo cuando la necesite con los métodos habituales, ya sea mediante new Model o ClassRegistry.


Los datos

En la mayoría de los tests necesitaré datos en los modelos, para lo cual tendré que crear un archivo de fixtures.

En teoría debería bastar con utilizar la propiedad CakeTestFixture->import y especificar que vamos a usar el modelo creado, pero yo no he conseguido hacerlo funcionar así, por lo que he tenido que especificar los campos del modelo en el archivo de fixtures (basta con copiar y pegar el contenido de Model->_schema). Puede que sea debido al hecho de no utilizar una tabla en la base de datos, sin embargo, no deja de ser un engorro. La parte buena es que estos modelos no tienen que cambiar mucho y realmente no da tanto trabajo.

Incluimos también los registros (records) que necesitemos para empezar. Este es un ejemplo de TEST/fixtures/basic_fixture.php


class BasicFixture extends CakeTestFixture {
    var $name = 'Basic';
    var $fields = array(
        'id'=> array('type' => 'string', 'null' => '', 'default' => '1', 'length' => '36', 'key'=>'primary'),
        'title'=> array('type' => 'string', 'null' => '', 'default' => '', 'length' => '255'),
        'key'=> array('type' => 'string', 'null' => '1', 'default' => '', 'length' => '255'),
    );
   var $records = array(
        array(
            'id' => 1,
            'title' => 'Lorem ipsum dolor sit amet',
            'key' => 'lorem_ipsum_dolor_sit_amet',
        ),
    );
}
?>


En el test declaramos que vamos a usar como fixtures app.basic y así estamos listos para trabajar. Aquí tienes un ejemplo de un test que utiliza el modelo Basic.

Conclusiones

Finalmente, tener modelos para usarlos específicamente en tests unitarios es bastante fácil. Son autocontenidos, por lo que no necesitas tener una base de datos y los puedes reutilizar dentro de un mismo proyecto o en otros.

martes, 28 de febrero de 2012

Una celda de tabla que se ajusta al contenido

Estoy diseñando una interfaz de backend en la que necesito que una columna de una tabla contenga varios botones de acciones que se pueden realizar con el registro correspondiente. En cada tabla puede haber distinto número de acciones (que son enlaces) y quiero que la columna ocupe tan sólo el espacio necesario para mostrar todos los botones.


Pues bien, es bastante sencillo.
Éste es el código CSS para la celda que contiene las acciones (tiene la clase actions). Ten en cuenta que las acciones son simplemente elementos A seguidos.:


.actions {
white-space: nowrap;
width: 1px;
}

La primera propiedad white-space: nowrap; hace que no se salte automáticamente de línea por la separación de los textos cuando no hay anchura suficiente para mostrarlas todas.
La segunda width: 1px; garantiza que el ancho de la celda es menor que cualquiera posible del contenido, pero como la anterior hace que el conjunto de etiquetas A se mantenga en la misma línea la celda se ve forzada a aumentar su tamaño para abarcarlas todas (es algo propio de la propiedad display: table-cell que tienen las TD por defecto).
El resultado es que la columna para las acciones se adapta automágicamente al contenido, sin saltos de línea.

martes, 19 de abril de 2011

Custom Find DRY

Una de las cosas que más me gusta de los custom find en CakePHP es la flexibilidad que pueden proporcionar a partir de una sintaxis única y sencilla.

Recientemente me he dado cuenta de cómo usarlos para evitar duplicaciones innecesarias de código.

Como ya sabrás, un método para custom find tiene esta signatura básica:

function _findCustom($state, $query, $results = array())
Este método se llamaría públicamente así

$model->find('custom' [, $options]);

La llamada $model->find('custom') hace, a grandes rasgos, lo siguiente:

  1. Normaliza las opciones que se han pasado para crear un array, que contiene los datos necesarios para generar la query que se hará a la base de datos.
  2. Llama a _findCustom para incluir las modificaciones que éste método haga en la query.
  3. Convierte la query en SQL y obtiene el resultado de la base de datos
  4. Llama una segunda vez a _findCustom para procesar el array de resultados.

Lo interesante es que nada nos impide llamar desde otros métodos del modelo a _findCustom pasándole el parámetro $state (que puede ser 'before' o 'after', según queremos obtener el resultado de la modificación de $query o de $results, respectivamente.

¿Para qué sirve? Supongamos que tenemos un _findCustom más o menos complejo en el que especificamos varios joins, conditions, etc, para un cierto tipo de búsqueda. Supongamos también que necesitamos hacer un cierto número de variantes de esa búsqueda, cambiando tan sólo algunas condiciones.

Pues bien, en ese caso podríamos llamar internamente desde nuestro segundo método _find a _findCustom con los parámetros extra para que genere el query (o bien, obtener el query y luego modificarlo). Por ejemplo:

function _findVariant($state, $query, $array()) {
    if ($state === 'before') {
         $extraQuery = array('conditions' => array('field' => 'value'));
         $query = Set::merge($query, $extraQuery);
         $query = $this->_findCustom('before', $query);
    }
    return $results;
}

martes, 11 de enero de 2011

Custom find en Behaviors

Trabajando en un Behavior me surgió la pregunta de si es posible definir Custom Finds en un Behavior para que sean usados por el modelo.

El caso es que encontré este código de Nick Baker, en el cual se encuentra una técnica para lograrlo. Una vez vista es obvia (pero había que verla, claro). Copio el código de Nick para explicarlo. En este caso, define una búsqueda que será find('range'), para lo cual hay que crear un método _findRange() en el Behavior.

La técnica tiene dos pasos:

El primero es mapear el método del behavior con un método para el modelo, lo cual se hace con la siguiente línea en las propiedades del Behavior:


var $mapMethods = array('/^_findRange$/' => '_findRange');

En el método setup hay que añadir la una clave al array de _findMethods del modelo que se pasa:

$Model->_findMethods['range'] = true;

Finalmente, la signatura del método es un poco distinta, pues hay que considerar dos parámetros extra

public function _findSearch(&$model, $method, $state, $query, $results = array())

Es decir, CakePHP pasa automáticamente los parámetros $model y $method, antes de los parámetros habituales del find.

A partir de ahora podremos usar el método model->find('range') en el modelo al que hayamos asociado el Behavior que lo contiene.

lunes, 27 de diciembre de 2010

Parámetros de URL y paginator

Debo decir que odio tener que comenzar esta entrada con el típico: "siento haber estado tanto tiempo sin escribir", pero razones de todo tipo me han mantenido alejado del blog, no siempre de manera muy justificada. Quiero decir, que probablemente mejor me hubiera ido escribiendo aquí que dejando pasar el tiempo sin hacerlo y sin tener la oportunidad de compartir aunque sólo fuesen pequeños trozos de código como el que traigo hoy.

En fin, para volver a la buena senda, he aquí cómo me las apaño para resolver un problema típico:

Tenemos una vista de registros paginada, con la más o menos típica tabla, con sus cabeceras "clicables" para ordenar los registros y sus botones para cambiar de página.

También tenemos un problema: ¿cómo conservar todos los parámetros que vienen en la URL en los enlaces relacionados con la paginación? Pues, la verdad es que es bastante sencillo:

$options = array(
    'url' => array_merge($url, $this->params['named'], $this->params['pass']),
    'model' => $this->defaultModel
);
$this->Paginator->options($options);

Bien, lo que tenemos que hacer es pasarle a Paginator un array de opciones que contenga una clave 'url' la cual estará compuesta por una URL de CakePHP expresada como array a la que habremos añadido los parámetros adecuados.

¿Dónde vamos a encontrar esos parámetros? Muy sencillo, en la vista en la que estemos trabajando (o incluso dentro de un Helper, que es de donde he copiado el código) tendremos los parámetros de la petición presentes en la variable de clase params, estando los parámetros "directos" en la clave 'pass' y los que tienen nombre en la clave 'named'. Simplemente hacermos un array_merge de la url, y ambos arrays de parámetros.

En el código de ejemplo, $url contendrá la URL básica de la vista.

lunes, 20 de septiembre de 2010

CakePHP y Javascript

En principio, no necesitas hacer nada especial para incorporar código javascript en las vistas de tu aplicación CakePHP, aparte de escribir el código, naturalmente. Simplemente has de insertarlo tal y como lo harías en cualquier documento HTML: utilizando la etiqueta script.


<script type="text/javascript" charset="utf-8">
 // Código
</script>


Por supuesto, en algún momento necesitarás utilizar scripts que se encuentren en archivos .js, convenientemente guardados en la carpeta js del webroot de tu aplicación. Para esta tarea es buena idea recurrir a HtmlHelper en CakePHP 1.3 (o a JavascriptHelper en la versión 1.2), ya que lo enlazará correctamente incluso si tu instalación de Cake está muy personalizada.

Por ejemplo:


<?php echo $this->Html->script('jquery.form', array('inline' => false)); ?>


o bien


<?php echo $javascript->link('ckeditor/ckeditor', NULL, false);  ?>


En estos ejemplos, puedes ver que para llamar al archivo javascript no es necesario incluir la extensión y que si el script se encuentra en una subcarpeta de js, hay que incluirlo en el nombre.

Por otro lado, la opción "inline" = false (en el segundo ejemplo, se correspondería con el tercer parámetro del método link) hace que la llamada a nuestro script se incluya en la cabecera del documento. Más exactamente allí donde hayamos realizado un echo $scripts_for_layout. De no indicar nada o ponerlo en true, el script se incluirá en el lugar de la vista donde realicemos el echo.

Puesto que todo lo que acontece en Javascript está en el lado del cliente, no hay más de lo que debas preocuparte en lo que respecta a la aplicación, exceptuando, claro está, el caso de trabajar con Ajax que veremos dentro de un momento, tras un pequeño inciso.

Los helpers Ajax/Javascript han transmutado en Js

La presencia del JavascriptHelper (y de AjaxHelper) en la versión 1.2 o del JsHelper en la versión 1.3 pueden llevarte a pensar que son de algún modo necesarios para poder trabajar con Javascript en una aplicación CakePHP.

Como decía al principio, esto no es así. Realmente no los necesitas si no quieres usarlos, pero te pueden echar una mano, que para eso están. Estos Helpers están ahí para ayudarte a generar algunos bloques de código más o menos típicos y repetitivos de la manera más sencilla posible.

Con todo, en la versión 1.2 podían resultar hasta un poco estorbo, ya que, el AjaxHelper en particular está basado en la librería Prototype, mientras que mucha gente opta desde hace tiempo por otras como jQuery o MooTools.

A consecuencia de esta constatación, el equipo de CakePHP decidió unificar AjaxHelper y JavascriptHelper en el nuevo JsHelper, con la ventaja añadida de poder definir qué framework Javascript preferimos utilizar. Por otro lado, un par de funciones que eran responsabilidad de JavascriptHelper han pasado a HtmlHelper. En concreto, la JavascriptHelper->link() es ahora HtmlHelper->script(), y JavascriptHelper->codeBlock() pasa a ser HtmlHelper->codeBlock(), lo que tiene bastante lógica.

Ajax

Ajax nos puede plantear algunos problemas particulares, ya que se trata básicamente de Javascript que interacciona con nuestra aplicación en segundo plano. Y, si bien, podemos organizar las cosas de modo que aprovechemos el código, debemos tener en cuenta algunos puntos.

RequestHandler Component

Para empezar, nos interesa recurrir a este componente especializado en gestionar información sobre las peticiones que recibe la aplicación y las respuestas que envía.

En general, suelo ponerlo en AppController, pues me puede interesar su uso en muchos lugares distintos de la aplicación. Aunque para usarlo basta incluirlo en el array de components del controlador que lo necesite.

En el caso que nos ocupa, es decir, procesar peticiones que vienen a través de Ajax, RequestHandler nos permite identificarlas mediante el método isAjax(), que devuelve true en el caso que te puedes imaginar. Además, preselecciona por nosotros el layout Ajax (básicamente un layout vacío que solo mostrará el contenido de la vista que toque).

De este modo, podemos bifurcar una acción para hacer cosas distintas si ha sido solicitada a través de Ajax o no. O también podemos asegurarnos de que una acción sólo se ejecuta si se ha pedido por Ajax.

RequestHandler identifica una petición Ajax cuando viene marcada con una cabecera determinada. Esto puede ser un problema si acción no es solicitada a través del objeto XHttpRequest. Por ejemplo, si se utiliza la técnica de iframes, en principio la petición es indistinguible de una petición estándar. En este caso se pueden llevar a cabo diferentes estrategias, como pueden ser "marcar" de algún modo la petición para interpretarla correctamente en el servidor, o bien tener una acción del controlador específica para procesar esa circunstancia, acción a la que "apuntaría" nuestro iframe.

En cualquier caso, dedicar una o varias acciones a procesar peticiones Ajax puede ser un buen planteamiento aunque depende de los casos y de lo "DRY" que quieras mantener el código.

Ajax y Auth

Otro problema que puede aparecer cuando utilizamos peticiones Ajax es que se produce un problema relacionado con el funcionamiento de AuthComponent y el comportamiento de algunos navegadores.

Si una acción requiere que el usuario esté autentificado, la petición Ajax falla porque Auth no reconoce que el usuario está autentificado y redirige al login.

Para dar como autentificado a un usuario, AuthComponent se basa no sólo en los datos de login almacenado en la sesión, sino que en cada recarga comprueba que la sesión no ha sido "secuestrada" por un nuevo agente de usuario. Si observas el contenido de $_SESSION en una aplicación CakePHP verás una variable Agent que identifica al navegador, junto con las demás variables.

En varios navegadores XHttpRequest genera una nueva identificación de agente lo que provoca que CakePHP considere inválida la información de autentificación almacenada en la sesión y vuelva a solicitar al usuario que se conecte mostrando la vista de login.

Una posible solución es poner en false la variable de configuración "Session.checkAgent" en core.php. También puedes deshabilitar este chequeo de forma temporal en la propia petición sólo si esta se hace a través de Ajax y antes de que Auth entre en acción. En AppController puedes poner:


function beforeFilter() {
    parent::beforeFilter();
    if ($this->RequestHandler->isAjax()) {
        Configure::write('Session.checkAgent', false);
    }
    // debug($this->params);
    $this->Auth->allow('display');
}


Otra técnica consiste en reiniciar a mano la sesión.

Respuestas Ajax

Las respuestas Ajax también pueden necesitar un pequeño ajuste antes de ser enviadas de vuelta al cliente.

Si la respuesta generada y esperada es HTML no tendrás que hacer nada especial, pero si la respuesta es JSON es posible que debas enviar una cabecera adecuada para que el navegador se entere de que se trata de JSON y no de otra cosa. Además, debes evitar que intente descargarlo, cosa que ocurre si envías lo que parece la cabecera propia de JSON: application/json.

En mi caso, la cabecera que ha funcionado es application/javascript.

Para ello, podemos utilizar también RequestHandler, indicando que vamos a enviar Javascript.


$this->RequestHandler->respondAs('js');


(Me surge ahora la dudo de si esto es lo que se considera Literal Javascript).

Debug

En muchos casos, aunque no necesariamente en todos, tendrás que poner el modo de debug de CakePHP a cero para que la respuesta Ajax sea aceptable. De otro modo, la información de debug puede hacer irreconocible la respuesta para el lado del cliente (no suele dar problemas si es una respuesta HTML que vaya a mostrarse en un elmento), pero seguro que los dará si es JSON o XML, que son formatos bien estructurados.

Simplemente, añade la línea


Configure::write('debug',0);


en el código que desarrolla la respuesta Ajax.

En estos casos, lo mejor es que hagas un log con la información que necesites para depuración.

Herramientas

Para comprobar el buen funcionamiento de tu código Javascript asegúrate de instalar FireBug en Firefox o activar las herramientas de Desarrollo en Safari (y otros navegadores basados en Webkit, como Chrome).

En ambos podrás estudiar el funcionamiento del código Javascript (marcando puntos de parada, inspeccionando variables y objetos...), así como las solicitudes y respuestas Ajax, lo cual también te ayudará a comprender cómo funcionan las cosas.

domingo, 19 de septiembre de 2010

Javascript y Ajax para torpes (como yo)

Tengo que confesar que tengo un problema con Javascript. Me cuesta mucho entender este lenguaje y, por tanto, utilizarlo. Claro que, a día de hoy, es ya una herramienta imprescindible.

Gracias a una parte del proyecto en el que estoy ahora mismo trabajando he tenido que empezar a plantearme en serio el uso de javascript en la aplicación y como parte de ese aprendizaje voy a dejar caer por aquí algunas anotaciones al respecto.

Esta primera anotación no es un tutorial, sino más bien una clarificación de conceptos sobre el lenguaje en sí, Ajax y otros temas relacionadas. Así que vamos allá:

Javascript

Para más información consulta la entrada Javascript en Wikipedia.

Javascript es un lenguaje de scripting orientado a objetos que está integrado en navegadores web, de modo que el código reside normalmente (o es incluido desde un archivo externo) en un documento HTML.

Quizá la característica más relevante de Javascript es que el lenguaje nos da acceso a una representación del documento HTML. Esta representación es el DOM (Modelo de Documento-Objeto) y nos permite interactuar de forma directa con los elementos de la página. En otras palabras, podemos obtener sus contenidos y modificarlos.

Parte de las dificultades prácticas de la programación en Javascript residen precisamente en la mayor o menor facilidad que tenemos para acceder a un elemento o grupo de elementos con los que necesitemos trabajar. El objeto document posee el método getElementById() que nos permite obtener un elemento si sabemos su atributo id. Sin embargo, esto a veces no es suficiente, ya que nos puede interesar obtener elementos por su clase, por tipo, o por otra forma de selección.

Esto se puede conseguir en el propio lenguaje escribiendo funciones específicas, lo cual ha dado lugar a que se hayan desarrollado diversos frameworks que complementan el Javascript original.

Frameworks

Para evitar estas dificultades y añadir diversas funcionalidades generales, se han creado frameworks o bibliotecas. Algunos ejemplos son jQuery, MooTools, Prototype...

Uno de los métodos de selección que han aportado estas bibliotecas es el uso de la sintaxis de selectores CSS. Es decir, gracias a estos frameworks podemos hacer selecciones de elementos tal y como lo haríamos en CSS, lo cual nos da acceso a cualquier elemento, o familia de ellos, presente en el documento, de una forma sencilla y reutilizando un conocimiento que ya tenemos.

Además, el uso de estos frameworks proporciona una normalización de acceso a propiedades y métodos que facilita la programación.

XHttpRequest

Una de las adiciones más significativas en Javascript fue la inclusión de la clase XHttpRequest, que proporciona al lenguaje la capacidad de enviar peticiones a servidores a través del protocolo HTTP y utilizar la respuesta recibida.

Estas peticiones se hacen, por así decir, desde "dentro" la página web "programáticamente", sin necesidad de recargarla. Y esto nos lleva a Ajax.

Ajax

Más información sobre Ajax

Ajax es una técnica o conjunto de técnicas que se basan en la comunicación en segundo plano entre una página web y el servidor, enviando o recogiendo información que se utiliza para modificar partes de la propia página.

Ajax presenta varias ventajas, siendo la principal que permite una actualización de contenidos en una página en tiempo real (o cuasi-real) sin necesidad de recargar la totalidad de la página y, por tanto, sin interrumpir la actividad del usuario en la misma.

Así, por ejemplo, es posible proporcionar prestaciones como:


  • Widgets o módulos de contenido que se actualizan en tiempo real, como podría ser un módulo de usuarios conectados, algún tipo de chat, etc.
  • Formularios que realizan cálculos con los datos que va introduciendo el usuario sin que éste tenga que enviarlos (incluso cuando esos cálculos requieran consultas al servidor).
  • Formularios que se autoguardan periódicamente para que el usuario no pierda el contenido introducido.
  • En general, cualquier tipo de interacción con el servidor que deba hacerse sin actividad explícita del usuario y sin recargar la página completa.


Ventajas del uso de Ajax son:


  • No interrumpimos la actividad principal que el usuario está llevando a cabo en la página, ya que la petición y actualización se realizan en segundo plano y sin recargar la totalidad de la página.
  • Ahorramos ancho de banda en las peticiones, ya que la solicitud se hace por una información concreta y se interpreta en el navegador sólo en las partes de la página afectadas, evitando tener que recargar información o contenido que ya estaba en la página.
  • Un comportamiento más ágil de la página y más similar a la experiencia de aplicaciones de escritorio.

La respuesta Ajax

La petición enviada por Ajax nos permite obtener una respuesta del servidor. Lógicamente, nuestra aplicación web tiene que saber recibir la petición y darle una respuesta adecuada. Esta respuesta puede adoptar varios formatos, de los que destacan tres, que se utilizarían según el tipo de manipulación que necesitamos hacer en el lado del cliente:

Si necesitamos actualizar un elemento específico lo mejor sería dar una respuesta HTML, es decir, el servidor obtiene la información y la empaqueta como un fragmento HTML. El código javascript del cliente no tiene más que actualizar la propiedad html del elemento al que vaya dirigida asignándole el contenido de la respuesta. Es la solución ideal para widgets o módulos de página.

Si necesitamos post-procesar la respuesta en la página, bien porque debemos utilizar partes de la misma en distintos elementos, bien porque necesitamos operar de distintas formas con ella, la respuesta adecuada sería en formato JSON. Json es una notación de objetos javascript que puede ser utilizada directamente desde el lenguaje a través de la función eval(), aunque puede utilizarse LJS (Javascript literal) como alternativa si sólo se envían datos, de modo que no se usa eval().

Esto lleva al planteamiento de cuestiones de seguridad, ya que eval() es una coladera para todo tipo de código construido de forma maliciosa, por lo que el uso de JSON en principio debe limitarse a intercambios cliente-servidor "de confianza". En general, no habría problema en entornos de una aplicación web que genera páginas que deben interactuar vía Ajax con el propio servidor.

Por tanto, si la seguridad es una característica crítica, y en particular en interacciones entre servidores (como por ejemplo si expones APIs), la respuesta del servidor debería darse en XML.

Javascript, Ajax y CakePHP lo dejo para la próxima anotación.