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.

jueves, 25 de septiembre de 2008

Selects dependientes en CakePHP: la manera bruta, toma 1

Vaya por delante que soy malísimo con Javascript. Sé que hay mejores maneras de hacer esto de los selects dependientes (de hecho, ya se me está ocurriendo alguna).

Allá va:

Básicamente se trata de tener una acción en un Controller que obtenga los datos necesarios para el select "dependiente" y cuya vista sea (redoble de tambores)... el nuevo campo select con los valores adecuados y llamarla mediante Ajax.

Pondré unos ejemplos de un proyecto en que estoy ahora. (Para ponerte en contexto se trata de un sistema de publicación. Cuando estás editando un post puedes asignarlo a un Blog y, a su vez, puedes asignarlo a una Serie dentro de ese Blog (en ambos casos con selects). Cada Blog tiene distintas Series, por lo que al escoger un Blog en su select, el select de Series tiene que cambiar. Espero haberme explicado.)

Bien, en mi caso la acción que genera el select será

/series/select


La vista padre es el archivo /views/post/admin_edit.ctp en el que tengo...


echo $form->input('blog_id', array(
'label' => __('Blog to publish this post', true),
'empty' => __('select a blog', true),
)
);
if (isset($series)) {
echo $form->input('series_id', array(
'label' => __('Is this post part of a series?', true),
'empty' => __('No', true),
'div' => array(
'id' => 'PostSeriesDiv'
)
)
);
}
echo $ajax->observeField('PostBlogId', array(
'frequency' => '1',
'update' => 'PostSeriesDiv',
'url' => array(
Configure::read('Routing.admin') => false,
'controller' => 'series',
'action' => 'select'
)
)
);
En el fragmento anterior se pueden ver los dos selects implicados y el uso de $ajax->observeField para generar un script cuya función es controlar los cambios del primer select. Por supuesto, al principio de la vista me aseguro de cargar la biblioteca prototype.js

echo $javascript->link('prototype', false);

En la vista "padre" (el formulario donde están los selects) necesitas un poco de Javascript para llamar a esa acción. Yo lo he hecho con observeField. Este método del Ajax Helper pide como parámetros una frequency en segundos para observar el campo, una url de la acción destino, un update, para saber dónde colocar el nuevo contenido y un último parámetro with para indicar los parámetros que se han de pasar en la petición.

(aquí es dónde creo que se podría hacer algo mejor con un evento onChange, pero de momento no me pidas más, lo dejaré para la toma 2)

¿Cómo se pasa el valor del Select "padre" a la acción para poder obtener las opciones? Muy sencillo: observeField por defecto serializa el campo observado en el parámetro with y tú puedes recoger esos parámetros en la acción destino en el array $this->data['Model']['field']. Si, exactamente así. Esta es mi acción receptora, tal como la tengo en /controllers/series_controller.php

function select() {
$this->set('series', $this->Series->getByBlog($this->data['Post']['blog_id']));
}


Debido a la forma en que se actualizan los elementos, hay que tomar unas precauciones con la construcción de los selects.

En primer lugar al select en la vista maestra hay que ponerlo en una DIV con un id único pues el elemento select en sí no se puede actualizar con una petición Ajax. Suponiendo que uses $form->input() para generar el campo, basta con poner el parámetro "div" en las opciones y ponerle un "id".

En la vista devuelta por Ajax, tienes que poner el mismo parámetro "div" en null, para que no se nos duplique. Tal que así:

/views/series/select.ctp
<?php
echo $form->input('Post.series_id', array(
'options' => $series,
'div' => null, // Esto elimina el div contenedor
'label' => __('Is this post part of a series?', true),
'empty' => __('No', true)
)
);
?>

miércoles, 17 de septiembre de 2008

Encuesta chorra (o no)

Aprovechando que el Blogger ahora admite encuestas (ver arriba), se me ha ocurrido la siguiente pregunta. Supongamos que tienes un sistema de blogs con los modelos Blog y Post, asociados de modo que Blog hasMany Post.

Ahora supongamos que la raíz del sitio (/) saca una lista de todos los Post recientes, con independencia del Blog al que estén asociados. Es decir, una accion como /posts/index.

Por supuesto, cada Blog tiene su página principal, que muestra una lista de posts. Mi primera tendencia fue a crear una accion /blogs/view/blog_id, pero me dije: "Un momento, esto no es más que un /posts/index/blog_id. Puedo hacer que /posts/index maneje la situación de listar sólo los post de un blog determinado".

Así que en principio creé una acción que modificaba la búsqueda de datos en función de si se pasaba un parámetro para indicar un blog. Luego en la vista, incluí una lógica para actuar de manera ligeramente distinta si se listaban los post de todo el sitio o si eran sólo los del blog indicado. Como tampoco me gustaba, preparé dos vistas y dejé que el controller eligiese la adecuada.

Sin embargo, esta solución tampoco me convence. Finalmente he decidido crear dos acciones en el PostsController (index y blog), cada una con su vista. El Controller ahora no toma tantas decisiones, y la vista tampoco. Pero ¿Qué opinas tú?

Cache de todo

La clase Cache aún no parece muy documentada, pero hoy estaba buscando una manera sencilla de hacer cache y acabé encontrando una referencia indirecta.

La cosa es que con la clase estática Cache puedes almacenar lo que quieras usando cualquiera de los motores de Cache de CakePHP. Esto es útil para evitar cargas excesivas en los servidores pidiendo los mismos datos. CakePHP tiene sistemas integrados de cache para las vistas, y una función cache (deprecated).

¿Quieres guardar algo en la cache por defecto?

Pues simplemente es:

Cache::write('clave', $datos);

siendo clave un nombre que te permita identificar los datos guardados. Por supuesto, $datos son los datos que quieres guardar (cualquier cosa menos recursos). Opcionalmente puedes indicar una duración.

¿Necesitas recuperar algo de la cache?

Cache::read('clave');

¿Que lo que hay en cache ya no vale y hay que borrarlo?

Cache::delete('clave');

No he visto un método Cache::check() para comprobar si existe una clave en cache, pero lo puedes suplantar con Cache::read('clave') ya que si no existe esa clave, el método devuelve false.

jueves, 4 de septiembre de 2008

Validation CheatSheet 1.1

Acabo de hacer una chuleta (cheatsheet) sobre el tema de la validación en CakePHP. Puedes descargarla si crees que te puede resultar útil. Está basada en la página del manual correspondiente


29/8/2008: corregido el typo señalado por Kejk (Thanks)
4/9/2008: corregido y aumentado el apartado de Custom Validation
10/2/2014: Cambio del enlace

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)

miércoles, 2 de abril de 2008

Malditos errores: CakePHP y PHP 4

Vale, pues resulta que acabo de subir una aplicación al servidor del trabajo para las primeras pruebas "reales" de funcionamiento. Resulta que la máquina de desarrollo tiene PHP 5 y la de producción 4 (las actualizaciones no son lo mío...). Después de algunos ajustes básicos del Apache la aplicación empieza a funcionar excepto algunas partes... Hum...

Básicamente, me fallaba no haber añadido el "apaño" que hace CakePHP para compatibilidad con PHP 4 de añadir la variable $name a los modelos. Es decir, en mi caso:

class RolesTarea extends AppModel {
var $name = 'RolesTarea';
...
}


El problema principal es que este tipo de fallos son muy desconcertantes. De hecho, CakePHP no conseguía siquiera mostrar un error.

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