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');
}