sábado, 30 de agosto de 2008

BreadCrumbs, miguitas para navegar

La verdad es que no estoy muy seguro de si  he entendido bien el concepto de las BreadCrumbs en CakePHP. Pero vayamos por partes.

En muchos sitios web se puede ver una línea de enlaces que nos permite "subir" dentro de la estructura de contenido hacia las páginas que "contenedoras" de la que estamos viendo. 

Para hacer esto en CakePHP de manera fácil se puede usar el HTMLHelper, el cual dispone de los métodos addCrumb($label, $url) y getCrumbs(), el primero para añadir un enlace y el segundo para obtener la cadena e incluirla en la página.

La forma en que lo he usado es la siguiente:

1. En la View actual añado tantas "crumbs" como necesite desde la página principal hasta la actual.

$html->addCrumb($label, $url);

$label sería el texto que quiero que figure y $url la dirección a la que apunta, que puede expresarse en forma de array. Si $url es false, entonces no le añade enlace.

2. En el Layout que corresponda, utilizo getCrumbs() para sacar la cadena.

echo $html->getCrumbs();

Es posible personalizar los separadores. Consulta el API para más información.

jueves, 28 de agosto de 2008

Paginación con relaciones HABTM

Bueno, el artículo de Cakebaker que puedes leer pinchando en el título o aquí, me ha servido para resolver un problema que me lleva ocupando todo el día. En fin, Daniel Hofstetter es un capo en este de CakePHP.

La cosa es más o menos así:

Tengo un model Blog y un model User, de forma que Blog hasAndBelongsToMany User. Es decir, que un blog puede estar asociado a muchos usuarios y cada uno de estos estar asociado a varios blogs.

Yo quería identificar que algunos de esos usuarios son Administradores del Blog y otros son sólo Escritores. La idea es que cuando un usuario haga login y acceda a administrar sus blogs salgan sólo aquellos de los que es administrador (como es lógico, ¿no?).

Bien, para esto tenemos que "modelizar" la relación, o lo que es lo mismo, utilizar la join table como un modelo más. Es decir, si añadimos unos campos a la join table blogs_users podemos describir características de la relación entre cada usuario y cada blog. Por ejemplo, un campo admin, para indicar si el usuario es administrador o no del blog relacionado.

Por otro lado, y según el artículo de Cakebaker, también tenemos que modelizar la relación para conseguir la paginación de los resultados.

¿Cómo se hace esto?

Bien. Para crear el modelo tenemos que añadir un archivo app/models/blogs_user.php que contenga la definición básica (observa el nombre del modelo: viene del nombre de la tabla blogs_users, es CamelCased, empieza en mayúsculas y var singular):


En las definiciones de Blog y User tenemos que añadir la clave 'with' para indicar que las relaciones se harán mediante ese modelo. Así en app/models/blog.php

var $hasAndBelongsToMany = array(
'User' =>; array(
'className' => 'User',
'joinTable' => 'blogs_users',
'foreignKey' => 'blog_id',
'associationForeignKey' => 'user_id',
'unique' => true,
'with' => 'BlogsUser'
)
);


Y en app/models/user.php tres cuartos de lo mismo. Añade la clave 'with'.

Con esto ya tenemos la base del problema resuelto. Para hacer la paginación lo que tenemos que especificar en Controller::paginate() es que la haga usando la relación modelizada:

$this->paginate('BlogsUser')


He aquí un ejemplo, que estaría en app/controllers/blogs_controller.php:

function admin_index() {
if ($this->Auth->user('root') == 1) {
$this->set('blogs', $this->paginate());
} else {
$options = array(
'BlogsUser.admin' => 1,
'BlogsUser.user_id' => $this->Auth->user('id')
);
$this->set('blogs', $this->paginate('BlogsUser', $options) );
}
}

Hay que decir, antes de nada, que esta aplicación usa el Auth Componente para autentificar usuarios, de modo que podemos saber qué usuario está actualmente conectado llamando a $this->Auth->user().

En este caso, primero comprobamos si el usuario autentificado se trata de un root, en cuyo caso simplemente buscamos todos los Blogs sin ninguna condición en especial. 

Si no es así, establecemos las condiciones para localizar los blogs administrados por el usuario autentificado, datos que están en BlogsUser. Luego hacemos el paginate indicando que use el modelo BlogsUser. Podemos añadir las condiciones que deseemos en cualquiera de los tres modelos implicados.

Usar condiciones al definir asociaciones de modelos

En todo el tiempo que llevo usando CakePHP nunca había probado hasta hoy el introducir condiciones en las relaciones entre Models. Es decir, al definir una relación (del tipo que sea) podemos poner condiciones que ha de cumplir el Model relacionado.

De este modo puedes tener dos Modelos relacionados de distintas formas y obtener datos de un modo incluso más fácil que pasando condiciones a un método find. Aunque no he explorado ni mucho menos las posibilidades de esta forma de trabajar, me voy a permitir poner un ejemplo útil de cómo funciona.

Supongamos dos modelos Autor y Post, de modo que Author hasMany Post (y Post belongsTo Author). Ahora supongamos también que Post tiene un campo published para indicar el estado de publicación del Post.

La relación "normal" en Author la definiríamos (minimalísticamente si seguimos todas las convenciones de CakePHP) como:

var $hasMany = array('Post')


Pero ahora, supongamos que nos interesa poder acceder a todos los Post de Author que estén publicados. De acuerdo, podríamos utilizar un find, pero ¿y si definimos una relación basada en esa condición?

'PostPublished' => array(
'className' => 'Post',
'joinTable' => 'posts_authors',
'foreignKey' => 'author_id',
'associationForeignKey' => 'post_id',
'conditions' => 'Posts.published = 1',
)


Pues lo que ocurre es que al hacer una búsqueda de Author, nos devolverá registros relacionados tanto en Post, como en PostPublished. En este caso, todos los Post y todos los Post publicados. En cierto modo, PostPublished es un modelo relacionado "virtual" que podemos manejar igual que uno definido realmente.

En este caso, debemos especificar las claves de la relación para asegurarnos de que se establece correctamente, ya que el nombre de la misma no respeta las convenciones.

(Nota: un uso de esta técnica es una típica página "home" de usuario, en la que mostremos todos sus objetos que cumplan cierta condición. Por ejemplo, tareas pendientes, posts sin publicar, etc.)

Combinado con bindModel y unbindModel o el Containable behavior (para restringir las relaciones y optimizar las query) puedes recabar con facilidad datos sin necesidad de recurrir a condiciones de búsqueda triviales o incluso complejas si las intentamos hacer en asociaciones habtm.

Precisamente ahora estoy estudiando un caso en el que las condiciones de búsqueda resultan bastante complicadas de establecer y los resultados no son los deseados y parece que aplicando esta idea, el problema podría quedar fácilmente resuelto. Ya contaré qué tal me va.

martes, 26 de agosto de 2008

ACL: colgando ACOs de un nodo raíz con ACL Behavior

Si el título de esta entrada no te sugiere nada, casi mejor que no sigas leyendo.

Llevo toda la tarde buscando información para hacer esto. Al final, encuentro la buena pista en un ticket del trac.

Pues nada, hay que hacer que el Model::parentNode() devuelva el alias del nodo (ACO en este caso, pero lo mismo para ARO) raíz en el que queremos colgarlo.

Tal que así:

function parentNode() {
return 'contents';
}


Me fui a añadirlo al CookBook, a ver si me lo aceptan. (Por cierto, acabo de descubrir que este blog sale listado allí...)

lunes, 25 de agosto de 2008

Paginate y Find personalizado

Soy un "fan" de la sintaxis "nuevo estilo" del método Model::find($type, $options), que permite crear búsquedas personalizadas. Personalmente me resulta muy útil y cómoda.

Lo que me tenía preocupado hasta ahora era la posibilidad de combinar esto con Controller::paginate, como forma de descargar el Controller de la preparación de condiciones de búsqueda cuando no quieres un findAll puro y duro de los datos.

Estudiando el código en el api resulta que sí es posible indicarle a Paginate que utilice una de estas personalizaciones, aunque para lograr que funcione el código que voy a presentar tienes que bajarte una nighty build o bien el código de controller.php más reciente. Me parece que la cosa está un pelín verde todavía, pero a mí me ha funcionado de esta manera:


function admin_index($mode = 'all') {
$this->User->recursive = 0;
switch ($mode) {
case 'pending':
$this->paginate['User'][0] = 'pending';
break;

default:
# code...
break;
}
$this->set('users', $this->paginate());
}


La clave está en la asignación de un valor al variable $this->paginate['nombre_del_modelo'][0] = 'find_personalizado'. Indicándolo así, Controller::paginate recabará los datos usando esa búsqueda en lugar de findAll.

Recuerda que la versión RC2 todavía no soporta esta capacidad.

Aprendiendo de un refactoring

He comenzado a reescribir algún código procedente de un proyecto anterior para el nuevo que estoy emprendiendo. En concreto un helper para genera tablas de administración de registros más flexibles y configurables que las generadas por Bake.

Cuando tu propio código ha envejecido unos meses puede llegar a hacerse bastante complicado de leer. De hecho, para ser sinceros, no había por dónde cogerlo. En otras palabras, no sabía por dónde empezar. Recordaba vagamente no estar muy satisfecho con la forma en que había escrito ese helper ya que, aunque funcionaba bien, no me atrevía normalmente a tocarlo mucho por si acaso.

Entonces se me ocurrió que ya que no veía como mejorar aquello, por lo menos intentaría hacerlo más legible. En cuanto empecé a conseguir legibilidad me fue fácil darme cuenta de dónde estaba metiendo la pata (en casi todo, pero esa es otra historia).

Volver pronto a casa

Por ejemplo, tengo (tenía) mucha tendencia a escribir código así:

if ($hay_que_procesar_esto == true) {
procesar...
procesar...
procesar
} else {
return false;
}
return $resultado_del_proceso;


Lo cual en su momento me parecía de lo más elegante y tal y cual. Sin embargo, tras leer este artículo de Felix Geisendörfer, caí en la cuenta de que la solución "Vuelve pronto a casa" es bastante más legible;

if (!$hay_que_procesar_esto) {
return false;
}

procesar...
procesar...
procesar...

return $resultado_del_proceso;


La lógica que apoya esta forma de organizar el código es muy convincente. Si escribes una función o un método, primero intenta lidiar con las excepciones y regresa. Deja para el final el algoritmo general, que podrá ejecutarse sin problemas y será más fácil de depurar llegado el caso.

Normaliza los datos

En mi caso, el helper podría lidiar con un array de registros resultado de una búsqueda con un model::find(). Pero ¿qué ocurre si quiero usar registros relacionados? La estructura del array es ligeramente distinta ¿no? Y solo requiere un sencillo bucle pasar de una a otra.

Pues nada, lo lógico (yo no lo hacía en el método que estaba reescribiendo) es chequear si los datos se ajustan a una estructura dada y, si no es así, normalizarlos antes de empezar.

De otro modo, lo que consigues es tener que crear un algoritmo o un proceso distinto para cada tipo de estructura posible y, si bien puede haber casos en que sea necesario hacerlo así, lo más seguro es que la modificación sea fácil.

No te repitas

Una de las cosas que más me llamó la atención fue comprobar que mi código hacía un montón de tareas duplicadas. A veces con los mismos datos de forma sucesiva (lo que supone el doble de tiempo de ejecución). Eso no era evidente en la primera versión, pero al reescribir esas situaciones empezaron a manifestarse con claridad.

La repetición de código no se refiere solo a ejemplos tan burdos como el mío, sino incluso al problema más sutil de los métodos que son muy similares entre sí y que tal vez sólo se diferencian en el ámbito de los datos que recuperan. Por ejemplo, un método index en un controlador que devuelve todos los registros existentes y otro método populares que devuelve todos los registros que tienen un mayor número de peticiones. La única diferencia entre ambos métodos sería una condición de búsqueda.

Como seguramente sabrás, el principio DRY (Don't Repeat Yourself) aboga por un código en el que se eviten al máximo las repeticiones. En el artículo de Nate Abele Art, MVC and CakePHP puedes encontrar un estudio muy interesante en el que se explica la aplicación de este principio. Es un clásico.

jueves, 21 de agosto de 2008

Cocinando Plugins: usando Bake para crear un plugin

No he sido capaz de encontrar mucha información sobre el cocinado de Plugins con Bake (aparte de la creación de la estructura del plugin), pero resumo aquí mis hallazgos:

Estructura del Plugin

Bueno, para esto llega con:

cake bake plugin nombre


Siendo "nombre" el nombre del Plugin.

Lo que nos creará una estructura de partida sobre la que trabajar, con las carpetas para models, controllers y views y subcarpetas adecuadas (behaviors, componentes y helpers).

Models, Controllers, y demás

Pues se hace básicamente igual que con sus equivalentes no plugin, sólo hay que especificar que se trata de un plugin y decir de cuál:

cake bake plugin nombre model


Con el comando anterior se entra a la generación interactiva de modelo, pudiendo elegir como es habitual la conexión de base de datos, el modelo, si le añadimos validación, etc.

Si especificamos un nombre de modelo, se crea de forma directa.

Para generar Controllers y Views se hace exactamente lo mismo:

cake bake plugin nombre controller


cake bake plugin nombre view


Actualización

Dos minutos después de publicar la entrada me encuentro con que estaba todo aquí

Baking pulgins (de Cakebaker, claro)