domingo, 25 de enero de 2009

URL mejoradas con rutas personalizadas

La idea es la siguiente. A veces la estructura /controller/action/arguments de algunas URL de Cake son poco significativas para los usuarios. Es un detalle que a veces me resulta un poco molesto y en particular para un proyecto que tengo entre manos. Por ejemplo. Supongamos que tengo un CMS y quiero mostrar los comentarios a un post. Una posible URL sería:

/posts/show_comments/slug_del_post

Una URL mucho mejor sería

/posts/slug_del_post/show_comments

Esto se puede conseguir definiendo una ruta de la siguiente manera:

Router::connect('/posts/:slug/:action/*', array('controller' => 'posts'), array('pass' => array('slug')));

Ahora, la segunda URL nos dispara la acción show_comments del Posts controller y como extra, le pasa el argumento slug, de modo que la puedes definir así:

function show_comments ($slug) {...}

Todo esto sacado del manual

viernes, 28 de noviembre de 2008

Llamar a papá

De la serie de fallos estúpidos cometidos por Fran nos llega esta nueva entrega, con varias espectaculares meteduras de pata. Digno de un premio en el "pifias de primera".

El problema

El CMS que estoy escribiendo tiene soporte para múltiples blogs en un site. Cada blog puede tener su theme asociado. Para saber qué blog se está viendo actualmente se guardan sus datos en la sesión siempre que la URL solicitada lo implique (básicamente ver la página principal de un blog o uno de sus posts).

Simplemente hay que consultar la variable de sesión y obtener el theme del blog en cuestión. Lo mejor es hacerlo en el AppController.

Fallo 1: poner ese código en el beforeFilter.

Es un fallo porque el blog actual puede cambiar durante la acción y la sesión ya no la volvemos a leer hasta el próximo clic del usuario.

Y lo peor es que esa acción puede suponer que si ese siguiente clic nos lleva a otro blog, éste coge el tema del blog que acabamos de abandonar y nos hace alucinar en colores.

Consejo: Si prepara cosas para la acción, ponlo en beforeFilter, si utiliza cosas generadas por la acción, ponlo en beforeRender.

Fallo 2: no llamar a papá

El sitio lógico para poner el cambio de theme es beforeRender porque así se ha ejecutado la acción y si ha cambiado el blog actual el código puede enterarse. Como beforeRender se ejecuta justo antes de empezar a dibujar la vista, ocurre que ésta ya aplica el nuevo theme.

Pero si los Controllers tienen su propio beforeRender debes asegurarte de llamar en ellos al parent::beforeRender() porque si no te saltas el código encargado de determinar si hay que cambiar el theme para las vistas.

Consejo general: siempre que escribas un callback en un controller llama a la función equivalente en parent, o por lo menos, piensa si tendrías que llamarla.

Fallo extra. Controla los nombres de variables

Este fallo me ha tenido varios días detenido. En un behavior utilizaba una variable cuyo nombre era igual que el del parámetro por el que CakePHP pasa la referencia del modelo. Por supuesto, se iba todo a tomar por el saco.

La cuestión curiosa es que ese código funciona bien sobre PHP 5, pero provoca un error fatal en PHP4.

miércoles, 12 de noviembre de 2008

$ajax->div sirve para algo

Trabajando hoy en el tema de Ajax me he dado cuenta de que si no utilizas $ajax->div('identificador') para crear los elementos que se han de actualizar mediante peticiones Ajax, las cosas no funcionan bien y se carga la vista entera, con layout y todo, aún con el RequestHandler.

Basta utilizar este método y cerrar con $ajax->divEnd('identificador') para que funcione perfectamente.

miércoles, 22 de octubre de 2008

Duplicable behavior

Este behavior facilita la tarea de duplicar registros de un modelo. Puedes tomar un modelo guardado como plantilla y aplicar automáticamente algunos cambios a sus campos, así como duplicar los modelos relacionados si los tiene, etc.

Uso básico:

añade una entrada a tu lista de behaviors:

'duplicable' => array(
'cascade' => true; // Duplicar asociados también
'changeFields' => array(), // List of fields to change their content ...
'changeString' => '%s dup.', // ... applying this format string (%s is a container for the original)
'whitelist' => array(), // List of fields to change (empty will duplicate all). Fields not in list will be empty or default
'associations' => array() // List of associations to duplicate (with cascade = true)
)

lunes, 20 de octubre de 2008

AclUpdate Behavior

Un problema de Acl Behavior es que si cambias el "padre" de un modelo (por ejemplo, si cambias el grupo al que pertenece un usuario), el Nodo Acl correspondiente no se actualiza para reflejar la nueva situación.

Este Behavior sirve precisamente para poder superar este problema. Lo he puesto en el bin.cakephp.org

AclUpdateBehavior

Posiblemente se pueda mejorar bastante.

jueves, 16 de octubre de 2008

Media Player Helper, actualización

Act: Tomy Muliady encontró un error en este Helper. Lo he modificado y ahora debería funcionar bien.

He escrito un Helper para insertar el JW FLV Media Player en las Views. Lo he colgado del bin.cakephp.org, Las instrucciones están en el interior.

martes, 7 de octubre de 2008

La encuesta

Bueno, los resultados de la (nada) científica encuesta (pocos votos) evidencian que si se pasan por aquí los teóricos del patrón MVC nos corren a gorrazos y nos echan al pilón por herejes.

Ha salido ganadora la opción de dejar a la vista lidiar con las diferencias en estructuras de datos.

Yo soy más partidario de la segunda opción, hacer que el controller elija la vista aunque él mismo obtenga estructuras de datos un poco diferentes.

La otra opción muchas veces produce métodos demasiado parecidos que atentan bastante contra el principio DRY ("no te repitas"). Tampoco es que haya que respetar estos principios hasta la última consecuencia (en último término te bloquean), sino tomarlo como guía. En el ejemplo que yo proponía adopté finalmente esta opción.

A medida que progreso en el aprendizaje de CakePHP me voy dando cuenta de que trato de eliminar código PHP de las vistas. Es decir, cada vez creo más en las vistas "tontas", que sólo saben recibir datos y mostrarlos, de modo que el código PHP que haya en ellas sólo sirva para convertir formatos o preparar información para helpers o elements. Pero casa vista debe saber recibir un tipo de datos y mostrarlo de una única manera.

Al Controller intento dejarle algo más de inteligencia, pero no demasiada. En ocasiones, como es el caso que proponía, las diferencias son lo bastante pequeñas como para darle al Controller cierto margen de decisión, en este caso, escoger la View, o incluso el layout, en función de ciertos parámetros.

Upload behavior: para subir archivos (CADUCADO)

CADUCADO: La información de este post apesta por lo vieja. Es posible que ya no sea válida con las versiones más recientes de CakePHP. Se mantiene público  para vergüenza y escarnio del autor.

Aunque no está del todo terminado, lo cierto es que este behavior funciona bastante bien. Básicamente sirve para gestionar la subida de archivos por un formulario. Para usarlo, en el modelo tenemos que poner:

var $actsAs = array ('Upload' => array ('imagen' => array ('ruta' => 'test')));

Upload es, por supuesto, el nombre del behavior.

Imagen, en este caso, es el nombre del campo en el que se sube el archivo. Si vamos a poner varios campos para subir archivos, tendríamos que indicarlos como pares "campo" => "opciones". En el ejemplo, la opción ruta nos permite especificar una ruta específica (bajo el directorio base por defecto) para guardar los archivos subidos mediante ese campo.

Las opciones para cada campo son varias y vienen explicadas en el código, en la definición de var $opcionesDefecto y también un poco más abajo.

No tienes que hacer nada más. El behavior se dispara en el beforeSave. CakePHP pone el array de datos de un archivo recién subido en el campo del modelo. Upload behavior mira a ver si hay algún campo del modelo que contenga ese tipo de estructura y en caso afirmativo lo procesa. Si hay otro tipo de información, simplemente lo ignora.

Una cosa importante es que este behavior puede alterar el nombre del archivo. Si éste contiene caracteres que puedan dar problemas según los servidores, los "sanea". De este modo se previenen problemas posteriores de archivos correctamente subidos que luego no se pueden descargar y similares.

Opciones por archivo subido

Que conste que el código parece complicado porque intento poder gestionar muchas cosas:

  • modo: distintos modos de trabajar con el archivo recién subido. Los modos son : url para indicar que se sube el archivo a una ubicación y el campo se trata como una url para acceder a ese archivo (ideal para imágenes o descargas); ruta para indicar que se sube un archivo y el campo se trata como una ruta del sistema de archivos; contenido es para copiar el contenido del archivo en un campo especificado del modelo. En el futuro podría haber soporte para un modo más: bd, que serviría para almacenar la información en una base de datos, pero para eso hay que crear el modelo y algunas cosas más.
  • ruta: especificar una ruta específica para guardar el archivo que hemos subido.
  • mime_permitidos: controlar qué archivos se pueden subir por su tipo mime general o específico (p.ej, podríamos usar image, o bien image/jpeg). Si no se especifica nada, entonces no se controla este tema.
  • ext_permitidas: controlar qué archivos se pueden subir por su extensión. Si no se especifica, se admite cualquiera.
  • sobreescribir: si existe un archivo con el mismo nombre: true lo machaca.
  • crear_ruta: crea las carpetas necesarias.
  • campo_contenido: copia el contenido del archivo al campo especificado.

El código

Para pegar en /app/models/behaviors/upload.php
Bueno, he cambiado el código y lo he puesto aquí, para que sea más fácil leerlo y descargarlo y también más fácil de mantener actualizado.

Procesa feeds

Hay cosas en CakePHP que consiguen asombrarme. Aquí tienes cómo procesar un feed:
$url = "http://cakephpilia.blogspot.com/feeds/posts/default";
App::import('Xml');
$x = new Xml($url);
$data = Set::reverse($x);
debug($data);
¿Dónde colocar este código? Bueno, tal cual está funciona en una View (¡detente, pecador!) o en un Controller, que quizá sea mejor sitio. Depende un poco de lo que estés haciendo, ya que podrías escribir un Model para procesar feeds, llamarlo desde el controller y pasárselo a la vista.

Hay un ejemplo "antiguo" de Felix Geisendörfer en el que define un modelo RSS dentro de una familia de "webModels" para procesar este tipo de cosas. Yo había empezado a utilizarlo, pero leyendo este otro artículo de Fahad Ibnay Heylaal sobre cómo parsear XML y la API de la clase XML me di cuenta de que se podían simplificar bastante las cosas. Y tanto. De dos modelos a 3 líneas de código.

Ojo: funciona mal con feeds redireccionados. O directamente no funciona.

Otra cosa importante, tienes que detectar el formato (es fácil), según sea atom o rss2, ya que  la estructura es diferente. Eso sugiere hacerlo en un modelo (o idealmente un dataSource, pero yo no sé como hacerlo) que pudiese tomar los feeds y normalizar los datos a una estructura, como por ejemplo, el array de datos típico de los modelos en CakePHP.

Esa tarea la dejo para otra ocasión, o como suele decirse, lo dejo como ejercicio para el lector.



miércoles, 1 de octubre de 2008

Notas sobre Localización

¿De dónde saca CakePHP el idioma para localizar e internacionalizar?

Según mi lectura del código de L10n y I18n, la cosa es más o menos así:

L10n::get(), si no se indica ningún argumento, tratará de averiguarlo mediante la información procedente del navegador o bien utilizar el contenido de la constante DEFAULT_LANGUAGE, que se podría definir en bootstrap.php.

Además, se supone que L10n::get() hace un Configure::write('Config.language') para establecer una preferencia de la aplicación.

Por otro lado, I18n::translate(), que es la base de las funciones del tipo __(), que se usan para tener las cadenas de texto localizables (o traducibles) obtiene el idioma a través de la variable de sesión Config.language, y si no hay una establecida, lo hace a partir de Configure::read('Config.language).

En conclusión, una vez que determinas qué idioma hay que servir, hay que registrarlo en la sesión y/o en la preferencia Config.language.

Curiosamente yo no consigo que L10n::get() me funcione bien para fijar el idioma preferido como se menciona en muchos tutoriales  al respecto. De hecho, estoy trabajando en una aplicación que gestiona todo eso sin recurrir a esta clase (básicamente con Configure y con Session) y me va bien.