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.

No hay comentarios: