lunes, 31 de marzo de 2008

Selector de minutos

Al usar el método input del FormHelper para un campo de hora y, en general, cualquier método de este Helper para generar campos de horas y/o minutos es posible especificar el intervalo en minutos entre cada opción. Por defecto, este intervalo es 1, l que produce uno de esos largos e incomodísimos desplegables con los minutos de 0 a 59.

En la mayor parte de los casos, un desplegable con opciones cada 5 ó 15 minutos funciona estupendamente. ¿Cómo se consigue?

Pasa la opción 'interval' => 15 (o el valor que necesites) en el array de opciones o atributos del campo. En el caso de un input, aquí tienes un ejemplo:

<?php echo $form->input('Evento.hora_inicio', array('timeFormat' => '24', 'empty' => true, 'interval' => 5)); ?> 


En este ejemplo, para un sitio sobre actividades y eventos diversos, hemos usado un intervalo de 5 minutos, que da 12 opciones.

miércoles, 26 de marzo de 2008

Personalizando Find y adelgazando el Controller

Aprendí esta técnica gracias a un post de Cakebaker que, a su vez, se basa en uno de Nate Abele más general sobre buenas prácticas en programación MVC con CakePHP.

La idea, una vez conocida, parece obvia. Desde la última beta de CakePHP se ha cambiado la sintaxis del método Model->find, de modo que ahora se llama con dos parámetros, uno indicando el tipo de búsqueda y otro con un array de opciones, que incluye, entre otros, las condiciones de búsqueda, límites, ordenación, etc.

Esta sintaxis a mi me ha resultado muy práctica, ya que no tienes que andar pendiente de si pasas los parámetros en el orden adecuado, etc.

Pero la técnica consiste en ir un poco más allá y reescribir el método a fin de disponer de más tipos de búsquedas personalizadas para nuestras necesidades en cada modelo. La reescritura sería más o menos así:

function find ($type, $options = array()) {
switch ($type) {
case 'tipoPersonalizado':
// Código
break;
default:
return parent::find($type, $options);
break;
}
}


Fíjate que para seguir utilizando los tipos predefinidos por Cake no tienes más que llamar a parent::find con los parámetros adecuados.

Hasta ahora podías (y se sigue pudiendo, claro) escribir métodos del modelo especializados en buscar resultados encapsulando condiciones. Por ejemplo, en un modelo Post, podríamos tener métodos como recientes(), valorados(), destacados(). (Espero que te hagas una idea de cuál sería su funcionamiento).

Con la nueva sintaxis los podríamos reescribir así: find('recientes'), find('valorados'), find('destacados').

Superficialmente esta reescritura no ofrece ninguna ventaja sobre esta forma de trabajar, pero si lo piensas un poco te darás cuenta que puede ser muy útil en ciertas situaciones.

En particular, cuando los diversos tipos de búsqueda comparten ciertas condiciones por defecto. En el ejemplo, podría ser que tu modelo Post sólo deba devolver registros que tengan el campo "publicar" en 1, y las fechas de "publicación" y "caducidad" adecuadas. Evitarías tener que repetir esas condiciones comunes en cada método, centrándote sólo en las específicas .

Además, puedes usar el parámetro $options para pasar opciones que no están contempladas por CakePHP. Por ejemplo, yo estoy usando esta técnica para pasar a find, un rango de fechas con las que seleccionar eventos de un calendario. Pero también podría ser el usuario actual para seleccionar Posts a los que tiene derecho a acceder y un largo etcétera de posibilidades.

Acabo de utilizar esta técnica, como he comentado, con una aplicación de Agenda o Calendario, en la que he movido toda la lógica de búsqueda de eventos al modelo. He añadido hasta siete nuevos tipos de búsqueda (días sueltos, esta semana, hoy, rango de fechas...). Ahora el modelo es un auténtico "fat model", mientras que las acciones del controlador han adelgazado enormemente y son mucho más comprensibles.

En la mayor parte de los casos, los nuevos tipos que he definido lo único que hacen es preparar condiciones y opciones específicas para llamar finalmente a parent::find('all', $options). Es decir, no es que haya que reescribir el método find por completo o construir querys personalizadas, pero podemos simplificar muchísimo la llamada desde el Controller, incluso definiendo nuevas opciones más significativas.

Por ejemplo, en mi modelo Evento he definido un tipo de búsqueda que he denominado 'week', y que sirve para obtener los eventos activos desde hoy hasta dentro de 7 días. En el método personalizado lo único que hago es determinar la fecha de hoy y la fecha de 7 más adelante:

case 'week':
$options['to'] = date('Y-m-d', strtotime( "+ 7 Day"));
$options['from'] = date('Y-m-d');
break;


Luego, inserto esos valores en el array de condiciones que voy a enviar al método parent::find('all')

$conditions = array(
'Evento.fecha_inicio' => '<= '.$options['to'],
'or' => array(
'Evento.fecha_fin' => '>= '.$options['from'],
'and' => array(
'Evento.fecha_fin' => null,
'Evento.fecha_inicio' => '>= '.$options['from']
)
),
'Evento.publicar' => 1
);


if (!isset($options['conditions'])) {
$options['conditions'] = $conditions;
} else {
$options['conditions'] = array_merge($options['conditions'], $conditions);
}

return parent::find('all', $options);


En este caso, te hago notar que no hago la búsqueda en el "case", sólo establezco los valores límite de las fechas, pero podría perfectamente hacer una llamada a parent::find si ya tengo todo lo que necesito.

Otros tipos de búsqueda hacen algo similar, partiendo de algunas opciones que paso (o que no paso, por lo que toman ciertos valores por defecto), genero las fechas límite en las que busco los eventos.

Espero haberme explicado. De todos modos, lo mejor es leer los artículos originales que enlazo al principio.

Ahora estaría bien si Controller-->paginate() llega a soportar esta sintaxis. De momento es un punto un poco oscuro para mí, coordinar paginación y búsquedas.

lunes, 10 de marzo de 2008

Logs específicos para la aplicación

Todas las clases de CakePHP descendiente de Object cuentan con el método log para registrar los mensajes que necesitemos en archivos de log. Basta llamarlo con el contenido del mensaje y un indicador, opcional, del tipo de mensaje. Los archivos de los están en la carpeta APP/tmp/logs

Cake PHP soporta varios tipos de mensajes, definidos mediante alguna de estas constantes, que es bastante autodescriptiva:

Van al archivo debug.log

LOG_NOTICE
LOG_INFO
LOG_DEBUG

Van al archivo error.log
LOG_ERR
LOG_ERROR
LOG_WARNING

Es decir, puedes registrar un determinado hecho escribiendo una línea como:

$this->log('mensaje', $tipo)

Siendo $tipo una de las constantes indicadas. Cake guardará el mensaje en el archivo que corresponda a cada tipo. Aparte, Cake por sí mismo va anotando diversos errores y problemas durante la ejecución.

Ahora bien, supongamos que nos gustaría tener el registro de actividades de los usuarios de una aplicación en un archivo separado de los anteriores para que sea más fácil saber lo que ocurre y no mezclarlo con la información de errores o de depuración de PHP.

Pues basta con indicar un nombre personalizado para el log y CakePHP se encargará tanto de crear el archivo de log y de escribir en él los mensaje que le dirijamos. Por ejemplo:

$this->log('mensaje', 'miLog');

Esto nos permite unas cuantas cosas interesantes. Podríamos, por ejemplo, crear un archivo de log por usuario, por controlador, por modelo, por fecha o por cualquier criterio que nos parezca. Lo único que tenemos que proporcionar es un identificador o nombre para el archivo.

Por ejemplo, para un log diario el nombre podría ser date('Ymd'), el cual nos daría archivos como 20080310.log y sucesivos. date('Ym') valdría para un log mensual.

Puede ser buena idea sobreescribir el método log para personalizarlo en clases particulares, por ejemplo, para asegurarnos de que los logs de un controlador se escriben en un archivo determinado, dar un formato a los mensajes, etc. Y llamar luego al método "padre".

Lo siguiente es justamente un ejemplo de cómo se podría personalizar. En este caso, se comprueba si hay un usuario autentificado mediante el component Authentication y, si lo hay, se añade su nombre e id al mensaje y luego se anota. La intención es saber qué usuario estaba haciendo qué cosa para el caso de que ocurra el algún comportamiento extraño de la aplicación.


function log($message) {
if ($usuario = $this->Auth->user()) {
$message = $usuario['Usuario']['usuario'].' (id: '.$usuario['Usuario']['id'].') '.$message;
} else {
$message = '(Anón) '.$message;
}
parent::log($message, 'miLog');
}

lunes, 18 de febrero de 2008

Safari, AjaxHelper y las codificaciones

Safari es un buen navegador, pero a veces tiene sus "cositas".

La última que he descubierto es un problema con las codificaciones y las llamadas Ajax. El caso es que éstas llamadas me volvían con caracteres mal codificados. Sin embargo, hasta que no lo probé en Firefox no caí que era un problema de Safari, y no de mi forma de trabajar con el Ajax Helper de CakePHP.

El problema concreto lo explican en este artículo, y tiene que ver con un fallo de la configuración del servidor donde alojemos la aplicación, combinado con un fallo de Safari a la hora de determinar cómo se codifica el contenido que se devuelve a una petición Ajax.

Sencillamente, si el servidor no tiene como juego de caracteres por defecto UTF-8, Safari tampoco sabe cómo manejar esa situación correctamente. Resultado: la codificación sale mal.

La solución:

Si tienes acceso a la configuración del servidor, o al htaccess de la raíz de la aplicación, añade esta línea:

AddDefaultCharset UTF-8


Si no, pide al adminsitrador del servidor que lo haga o que te prepare el servidor para servir contenido UTF-8.

Y con esto funciona estupendamente. (Hay pseudosoluciones a base de enviar cabeceras desde la aplicación, pero a mí no me ha funcionado ninguna de ellas).

domingo, 3 de febrero de 2008

Temas para las aplicaciones Cake

Como estoy manteniendo un desarrollo que voy a usar en diferentes proyectos, me interesaba estudiar el asunto de crear temas para cada uno de ellos.

Por si no tienes claro de qué estoy hablando, los temas tienen que ver con el aspecto visual de las aplicaciones. A mí me interesa sobre todo, poder tener un tema diferente para cada proyecto concreto, aunque compartan buena parte de la funcionalidad y el código de la mayoría de las vistas.

La mejor referencia que he encontrado sobre el particular es este artículo de Tarique Sani y es tan sencillo que apenas lleva un par de minutos preparar una aplicación para dar un soporte básico de los temas. Reproduzco los pasos básicos

En el código

En tu app_controller añade la siguiente línea

var $view = 'Theme';


Básicamente la línea anterior le dice a Cake que vamos a usar temas en la aplicación.

En beforeFilter o beforeRender de los Controllers adecuados (o en el app_controller) añade una línea para indicarle a CakePHP el nombre del tema que vas a utilizar

$this->theme = 'mitema'


El nombre lo puedes aportar directamente, o bien leyendo una variable de configuración, o un ajuste de usuario guardado en la sesión... de dónde quieras.

Cómo se hacen los temas

Como se supone que ya sabes hacer vistas y hojas de estilo y demás, ya sabes hacer temas. Para empezar a crear temas para la aplicación Cake lo primero que tienes que hacer es crear una carpeta "themed" dentro de "views" y otra "themed" dentro de webroot.

Dentro de cada una de ellas crearás una carpeta por cada uno de los temas que vayas a crear. Por ejemplo, "mitema".

Así, la ruta quedará así

/app/views/themed/mitema


/app/webroot/themed/mitema


¿Y qué habremos de colocar ahí? Pues la versión "tematizada" de nuestras vistas, hojas de estilo, javascripts o imágenes que necesitemos, de manera que la estructura habitual de views y webroot quede reproducida bajo la carpeta del tema.

Así, por ejemplo, si necesitamos una vista "tematizada" para la acción /posts/index crearemos el archivo

/app/views/themed/mitema/posts/index.ctp


Y si, por otro lado, queremos un hoja de estilo específica para un tema, usaremos

/app/webroot/themed/mitema/css/estilos.css


Sin embargo, no necesitas reescribir todas tus vistas ni recursos.

Fall-back

Lo mejor de todo es que Cake tiene un sistema de "red de seguridad" para los temas. Esto es, cuando usamos temas, CakePHP buscará layouts, vistas y demás recursos en la carpeta del tema, y si no lo encuentra, buscará en la carpeta básica.

De este modo, puedes escribir tus vistas, hojas de estilo, recursos web comunes y luego tematizar únicamente aquellos que realmente lo necesiten. Por ejemplo, a lo mejor sólo tienes que tocar unas hojas de estilo y un layout. O alguna vista para una acción en particular.

lunes, 28 de enero de 2008

tinyint(1) es un Bit

Pues eso. Que tinyint(1) es un Bit.

¿Y qué significa? Significa que si defines un campo de una tabla de Mysql como Tinyint con un tamaño de 1, CakePHP lo considerará como un bit y cualquier valor que le intentes poner que no sea 1 ó 0 será convertido a 1.

En Mysql 5 ocurre exactamente eso (en versiones anteriores creo que no).

Es un poco "contraintuitivo" ya que la definición de tinyint es básicamente un byte (de 0 a 255).

En fin, yendo a lo práctico, si necesitas tener un campo tinyint para almacenar valores numéricos pequeños, algo normal para flags de estado e indicadores de tipos con pocas opciones, asígnale tamaño 2.

Esto lo he aprendido hoy, tras una hora y pico de desconcierto con un modelo que tiene un campo de "estado" que puede tomar valores 0, 1 y 2, y que siempre se guardaba como 1.

martes, 22 de enero de 2008

Más diversión con el Form Helper: campos de fecha (actualización)

El método input de FormHelper está muy bien para resolver los problemas generales de creación de formularios, pero si por alguna razón nos resulta insuficiente tenemos más recursos a nuestra disposición.

Por ejemplo. Cuando se trata de un campo para fechas, el método no resulta muy flexible (al menos en su versión actual) ya que siempre nos fuerza a un determinado formato. FormHelper tiene un método dateTime que se usa precisamente para crear este tipo de campos y que de hecho es llamado desde input. Sin embargo, si lo usamos nosotros directamente podemos personalizarlo a gusto.

Lo único que tenemos que saber es cómo trabaja Input para generar el código y reproducirlo con nuestras preferencias.

$campoPublicacion = $form->label ('Circular.publicacion', 'Fecha de publicación');
$campoPublicacion .= $form->datetime ('Circular.publicacion', 'DMY', null, null, null, true);
echo $html->div('input', $campoPublicacion);


La explicación del código anterior es bastante sencilla: lo primero que hacemos es generar la etiqueta del campo (label). Después, generamos el control mediante el método dateTime, indicando el formato y como queremos mostrarlo por defecto. Finalmente, empaquetamos eso en un DIV con la clase input.

El resultado es el mismo código que se generaría con input. Sólo que esta vez con el formato deseado. Puedes consultar la API del método dateTime, que es bastante clarita para saber cómo pasar los argumentos.

Actualización 22-1-2008

Con la salida de la versión 1.2 beta ya se ha corregido el asunto del formato de los campos de fecha y es posible pasar un formato en el método input, lo anterior se puede escribir así:


echo $html->input('Circular.publicacion', array('label' => 'Fecha de publicación', 'dateFormat' => 'DMY'));

lunes, 21 de enero de 2008

Buscando en relaciones habtm

Supongamos un típico ejemplo de CMS con su modelo de Post y Tags, los cuales se asocian entre sí mediante una relación de tipo HasAndBelongsToMany (o muchos a muchos en otras terminologías).

Este tipo de asociaciones require una tabla íntermedia de unión (join table) que siguiendo las convenciones de CakePHP nombraríamos en este caso posts_tags. Ya sabes.

Post habtm Tag a través de posts_tags

Cuando hacemos un búsqueda en Post, CakePHP automágicamente incluye en los resultados las Tag asociados a cada Post. Pero, ¿podemos buscar los Post que estén asociados a una o varias Tag además de otras condiciones?

La respuesta es que directamente no podemos hacerlo ya que en las relaciones habtm no se hace el "join" automático de las tablas (al contrario de lo que ocurre en las relaciones hasMany o belongsTo).

Crear una query en SQL para utilizar con Model::query() es una solución, pero tiene el inconveniente de que perdemos algunas de las facilidades propias de CakePHP (básicamente los callbacks como afterFind o beforeFind) y otras ventajas.

En CakePHP 1.2 es posible hacer esto de una manera bastante sencilla aunque require "modelizar" la tabla intermedia. Primero, tenemos que crear un modelo para la tabla posts_tags y especificar sus relaciones con las tablas Post y Tag. Siguiendo las convenciones de CakePHP nos queda así:

<?php

class PostsTag extends AppModel {
var $belongsTo = array(
'Tag',
'Post');
}

?>


En el modelo Post tenemos que especificar que la relación con Tag se va a hacer "con" (with) ese modelo intermedio.

var $hasAndBelongsToMany = array (
'Tag' => array(
'className' => 'Tag',
'with' => 'PostsTag'
)
);


Hasta aquí el trabajo preparatorio. Ahora veamos un ejemplo de su uso. En el model Post tengo un método "publicados" que me devuelve los Post cuya fecha de publicación es anterior a la actual, cuyo flag de publicar está a 1 y que están etiquetados con una tag que le pasamos al método.

function publicados ($tag = false) {
$now = date ('Y-m-d');
$conditions = array (
'Tag.tag' => $tag,
'Post.publicar' => '1',
'Post.publicacion' => "<= $now"
);
return $this->PostsTag->find('all', array('conditions' => $conditions));
}


Espero que se entienda. Las condiciones no deberían tener mucha dificultad. Toda la clave es el find que hacemos sobre el modelo intermedio (PostsTag) asociado a Post, no directamente a Post. (Nota: he suprimido la parte del código que gestiona lo que hay que hacer si no se pasa ninguna $tag).

Algunas referencias que me sirvieron de punto de partida:

Modelizing HABTM join tables in CakePHP 1.2: with and auto-with models de Mariano Iglesias

Modeling relationships in CakePHP (faking Rails’ ThroughAssociation) de Felix Geisendörfer

lunes, 14 de enero de 2008

El truco del espacio tras el tag de php

No recuerdo de dónde saqué la información. Un problema típico de las vistas (los archivos .ctp o thml de CakePHP en general) es que si usas el tag de php la página generada "se come" el retorno de carro que hubiera (o hubiese) chafando lo que de otro modo sería un limpio y ordenado HTML (o texto puro para un email).

La solución consiste en añadir un espacio después del cierre del tag.

viernes, 11 de enero de 2008

Claves memorizables en PHP (y razonablemente seguras)

Bueno, ya veo que hace más de dos meses que no asomo el pelo por aquí. El caso es que he estado ocupado y después de una temporada, por fin puedo volver a CakePHP un rato.

Estoy trabajando en la parte de gestión de usuarios de una aplicación y se me ocurrió que necesitaba un generador de contraseñas para automatizar la creación de las mismas al dar de alta manual o automáticamente a usuarios (no creo que esta aplicación lleve autorregistro).

Mis usuarios odian las contraseñas puramente aleatorias del tipo ajku87hsdf6, así que pensé en utilizar uno de esos generadores de contraseñas legibles o memorizables. No encontré ninguno que me gustase, así que acabé escribiéndolo y esta es la segunda versión (muy difícil no es).

Lo de las contraseñas memorizables es bastante sencillo. Se trata de series de letras construidas de tal modo que parecen palabras y son bastante fáciles de pronunciar y memorizar por humanos. Por ejemplo: hanicu, corchuela o gafimocho.

La base del generador es la construcción de sílabas válidas en español (en este caso), combinando una consonante (o grupo consonántico) con una vocal y opcionalmente con otra consonante válida para finalizar una sílaba.

He añadido un factor de fortaleza que afecta a todo el proceso de generación de la contraseña aumentando la probabilidad de que se generen sílabas con más letras. Esto se traduce en claves más largas, con más posibilidad de repetición de caracteres, etc.

Algunas mejoras posibles serían la posibilidad de convertir al azar algunas letras en mayúsculas o incorporar más caracteres (por ejemplo los acentuados).

El uso de la función es muy simple:

$nuevaClave = claveLegible ();


Lo anterior nos daría claves de fortaleza media

o indicando la fortaleza deseada:

// baja, pasar 'l' de 'low'

$nuevaClave = claveLegible('l');

//alta, pasar 'h', de 'high'

$nuevaClave = claveLegible('h');



<?php

/**
* Genera una clavve legible que se puede memorizar por humanos con mayor facilidad
* sin que por eso se pierda demasiada seguridad
* @param $fortaleza: 'l' (baja), 'm' (media), 'h' (alta). Por defecto es 'm'. Indica
* la solidez teórica de la contraseña generada (más larga y con palabras más difíciles
* de localizar en un diccionario0)
*
* @return string La clave generada
* @author Frankie
**/
function claveLegible($fortaleza = 'm') {
// Preparamos los parámetros de la generación a partir de la indicación de fortaleza
$fortaleza = strtolower($fortaleza);
if ($fortaleza == 'h') {
$factor = 0;
$numeroSilabas = 5;
} elseif ($fortaleza == 'l' ) {
$factor = 4;
$numeroSilabas = 3;
} else {
$factor = 2;
$numeroSilabas = 4;
}
// Fuentes de los caracteres, si quieres modificar la probabilidad de cada uno, añade los que desees
$consonantes = array('b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'ñ', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z', ' ');
$grupos = array('b', 'bl', 'br', 'c', 'ch', 'cl', 'cr', 'd', 'f', 'fl', 'fr', 'g', 'h', 'j', 'k', 'l', 'll', 'm', 'n', 'ñ', 'p', 'pr', 'pl', 'q', 'r', 's', 't', 'tr', 'v', 'w', 'x', 'y', 'z', ' ');
$vocales = array('a', 'e', 'i', 'o', 'u');
$diptongos = array('a', 'e', 'i', 'o', 'u', 'ai', 'au', 'ei', 'eu', 'ia', 'ie', 'io', 'oi', 'ou', 'ua', 'ue', 'uo');
$finales = array(' ', 'n', 'l', 's', 'r', 'd');
// Generación de la contraseña. Cada sílaba se construye con una consontante o grupo inicial, una vocal y una consonante final. Se introduce un factor de aleatoriedad regulado por la fortaleza para generar sílabas más o menos simples.
$clave = '';
for ($i=0; $i < $numeroSilabas; $i++) {
$consonante = rand(0,$factor) ? $consonantes[rand(0, count($consonantes)-1)] : $grupos[rand(0, count($grupos)-1)] ;
$vocal = rand(0, 2*$factor) ? $vocales[rand(0, count($vocales)-1)] : $diptongos[rand(0, count($diptongos)-1)];
$final = rand(0, 4*$factor) ? '' : $finales[rand(0, count($finales)-1)];
$clave .= trim($consonante . $vocal . $final);
}
return $clave;

}

// Test
//
// for ($i=0; $i < 20; $i++) {
// echo claveLegible('m') . chr(10);
// }

?>


Addenda: Uso en CakePHP

Yo he creado una carpeta Utilidades bajo Vendors y he puesto la función en un archivo clave_legible.php.

Cuando necesites usarla en Cake, no tienes más que cargarla con una llamada

vendor('utilidades/clave_legible')