jueves, 24 de diciembre de 2009

Poblar variables de clase a partir de un array

Este código nos permite poblar de valores las propiedades de una clase a partir de un array asociativo cuyas claves tengan el mismo nombre. Es algo así como un extract.

Puede ser útil al pasar parámetros en forma de array asociativo, una práctica habitual en CakePHP.

<?php

class test {

var $prueba;
var $control;

function __construct() {

$array = array(
'prueba' => 'Hola',
'control' => 'amigos'
);
foreach ($array as $key => $value) {
$this->{$key} = $value;
}
}
}

$test = new test();
print_r($test);
?>

martes, 22 de diciembre de 2009

Comprobar que un array no es asociativo

Se me ha ocurrido esta función para comprobar que un array no sea asociativo:



function isList($array) {
if (!is_array($array) || count($array) == 0) {
return false;
}
return array_sum(array_keys($array)) > 0;
}
?>

Devuelve true si el array es numérico, y false si no es un array o es asociativo.

En CakePHP puede ser interesante usarlo cuando queremos diferenciar entre los datos devueltos por un read y un find('all'). En el segundo caso, el array es numérico (una clave numérica por cada registro del modelo).

Si los datos vienen de un read (o un find('first')) podemos convertirlos en array numérico con un simple $datos = array($datos), y luego procesar con un foreach. La ventaja es que el mismo código nos vale luego para ambos casos.

Yo voy a usar esta idea para un uploadable behavior en el que estoy trabajando.

viernes, 11 de diciembre de 2009

Querys CakePHP con operaciones bitwise

Otro de esos títulos "sólo para iniciados"...

Hoy me he encontrado con un problemilla curioso. Necesitaba definir una condición para un find en la que hay una operación bitwise en la base de datos. En concreto, un & de una máscara binaria contra un campo del modelo.

Bueno, pues la forma en que he conseguido que funcione es algo así:

...
array('conditions' => array('12 & Rule.precedence'):
...

Que debe dar un WHERE más o menos así:

WHERE 12 & `Rule`.`precedence`

Es decir, primero pongo el valor de la máscara, luego la operación binaria y luego el campo.

Si pongo primero el campo, CakePHP se empeña en hacer no sé qué y no aparece la comparación en el WHERE.

martes, 8 de diciembre de 2009

Obtener el prefijo de una acción (o dividir una cadena)

Esta es una de esas cosas que siempre se me olvidan, así que voy a anotarla.

He aquí una operación típica para romper un string en dos partes, sabiendo que están separadas por un carácter concreto, en este caso el socorrido "underscore".

list($part1, $part2) = explode('_', $string);

Aplicándolo a CakePHP, vamos a imaginar que tenemos el nombre de una acción y queremos saber si tiene prefijo y cuál es. Así que en algún sitio de nuestro Controller (probablemente en el beforeFilter) hacemos:

$action = $this->params['action'];
list($prefix, $actionName) = explode('_', $action);

Sencillo, ¿verdad?

Pero, ¿qué pasa si $action no tiene prefijo y por tanto no hay separador en la cadena?

Bueno, pues en ese caso, $prefix toma el valor de la cadena y $actionName queda vacío, por lo que necesitaríamos el siguiente código para controlar la situación:

$action = $this->params['action'];
list($prefix, $actionName) = explode('_', $action);
if(!$actionName) {
    $actionName = $prefix;
   $prefix = '';
}

viernes, 4 de diciembre de 2009

Bootstrap para plugins

El archivo APP/config/bootstrap.php de una aplicación CakePHP nos sirve como lugar donde cargar funciones, cargar configuraciones, iniciar variables o definir constantes globales para nuestra aplicación.

Estos días me encontré con la necesidad de disponer de una funcionalidad parecida para los plugins. La idea es interesante, se trata de poder disponer, entre otras cosas, de ajustes de configuración y constantes definidas en los plugins pero que sean accesibles desde otras partes de la aplicación.

La cuestión es cómo hacer ese "bootstrapping" para plugins de forma "automágica".

A mí se me ha ocurrido usar este fragmento de código en el bootstrap de la aplicación

<?php
App::import('Core', 'Folder');
$folder =& new Folder();
$folder->cd(APP . 'plugins');
$files = $folder->findRecursive('bootstrap\.php');
foreach ($files as $file) {
include_once($file);
}
?>

La explicación es bastante sencilla. La clase Folder nos permite apuntar a un directorio, en este caso plugins, y obtener todos los archivos cuyo nombre se ajuste a un patrón grep, cuyos paths se obtienen en un array. Luego no tenemos más que hacer include de cada uno de los archivos y ya tenemos nuestros bootstrapping.

lunes, 23 de noviembre de 2009

Nota mental: Controller necesita $name

Pues sí, los controladores necesitan tener la propiedad $name, al menos para funcionar en los test, de otro modo se rompe todo. Incluso con PHP 5.

miércoles, 18 de noviembre de 2009

Cosas que hago mal

Dejando aparte errores de sintaxis y algunos fallos lógicos, más o menos normales, he estado pensando que hago algunas cosas mal en mi "estrategia" (por decir algo) de desarrollo.

Una de las principales es que tiendo a centrarme demasiado en lo periférico, con lo cual avanzo relativamente poco en mis tareas, aunque teclee innumerables líneas de código.

Por ejemplo, llevo unos meses trabajando (un poco irregularmente) en el desarrollo de un proyecto que, en principio, será un CMS y la base para una aplicación de Intranet para el colegio en el que trabajo.

Pues bien, he dejado la parte de CMS para el final, y mientras tanto he estado desarrollando módulos auxiliares que, si bien son importantes, no son el objetivo principal de la cuestión. Sólo últimamente he empezado a trabajar en el módulo de contenidos, el cual, en muchos aspectos debería haber sido el primero (o uno de los primeros, ya que también me resulta muy importante el de gestión de usuarios y autorización).

Con todo, no he estado parado, sino que he desarrollado una serie de módulos que aportan gran cantidad de funcionalidad: comentarios, etiquetas, licencias, colecciones, uploads..., pero todavía no hay una aplicación real que esté funcionando. Lo que resulta frustrante.

La cuestión es que desde un punto de vista más "pragmático" lo lógico hubiera sido empezar a desarrollar la parte de gestion de contenidos (dejaré fuera de la cuestión la parte de acceso, que estoy revisitando ahora mismo) y seguramente estos otros elementos hubieran ido surgiendo de forma natural, aportando funcionalidad "avanzada" a la básica.

Mi CMS se estructura en torno a los modelos Channels e Items. Los Channels son canales, que pueden ser blogs, podcasts, photologs o similares. Los Items son los posts, episodios, etc. Si conoces la estructura de los RSS entenderás en qué me he inspirado.

La idea que intento explicar es que probablemente es más eficaz y mejor práctica comenzar por desarrollar una funcionalidad básica "gruesa", sin preocuparse de detalles. Es decir, en este caso, poner a funcionar mis channels e items, perfeccionando luego, de forma iterativa, el sistema añadiendo nuevas prestaciones.

Probablemente eso ayudaría también a mantener una buena motivación a lo largo del trabajo, cosa que en algunos momentos hecho de menos.

jueves, 17 de septiembre de 2009

DRY, pero no tan DRY

A la metodología de programación que busca no repetir innecesariamente código se la conoce como DRY (Don't Repeaty Yourself, No Te Repitas).


Hace unos posts escribía sobre una técnica que consistía en escribir algunas acciones básicas en el AppController, a modo de scaffolding, de modo que no necesitase escribir otra vez ese código en los nuevos controladores que iba creando.

A este post me respondía AD7six, del que había tomado la idea en un post suyo antiguo. En su comentario me hacía ver que, a la larga, me iba a encontrar con problemas a medida que los controladores empezasen a tener excepciones y que acabaría reescribiendo todo.

Por supuesto, AD7six tiene toda la razón al indicar esto, aunque yo creo que la técnica puede seguir siendo válida para  partes de las aplicaciones que sólo necesiten acciones CRUD muy básicas (como mantenimiento de listas de opciones, categorías, etc). En realidad, también se podría usar el scaffolding.

¿Qué solución tenemos a esta situación en la que el código es repetitivo y sin embargo no debería dejarse en un método genérico?

Pues la respuesta es Bake, la utilidad de líneas de comandos para generar Modelos, Controladores y Vistas en CakePHP.

¿Y si no te gusta el código que genera? Pues a aprender a crear tus propias task para montar controladores, y modelos. Y plantillas para las vistas.

De este modo, podemos combinar lo mejor de ambos mundos: no tener que escribir 20 veces el mismo código (lo hace bake) y poder adaptarlo según sea necesario.

Sobrecargando el campo upload

Repasemos un momento.

En PHP, cuando tienes uno o varios campos tipo "file" para subir archivos en un formulario y éste es enviado al pulsarse un botón Submit, PHP coloca los datos de los archivos subidos en el array $FILES. No tenemos más que ir a buscar allí los datos, hacer las comprobaciones necesarias y usar move_uploaded_file() para poner los archivos en los lugares adecuados.

CakePHP va un paso más allá y coloca esos datos en los campos adecuados del array de datos que viene del formulario, lo que hace aún más fácil usarlos. En concreto, el array asociativo que nos da la información de cada archivo tiene estas claves:

tmp_name
name
type
size
error

Y ahora, el truco:

Supongamos que tenemos un Modelo con un campo file con el que vamos a subir un archivo. Por tanto, en la vista en la que construimos el formulario ponemos algo así como:


echo $form->input('Modelo.file', array('type' => 'file'));


Resulta que si ponemos un campo del estilo de:


echo $form->input('Modelo.file.extra');


Cuando hagamos el proceso del archivo subido nos vamos a encontrar más claves en el array de información, exactamente tantas como campos Modelo.file.* hayamos creado:

extra

tmp_name
name
type
size
error


¿Útil? Puede que sí. Gracias a este pequeño truco he podido crear un formulario en el que el usuario puede marcar que sea borrado un archivo ya existente, si no quiere subir otro para que lo sustituya.

domingo, 13 de septiembre de 2009

Unit-testing y yo

Hace unos pocos posts comenté que una de las estrategias que estaba adopotando en mi desarrollo, junto a la modularización por plugins, es el unit-test y el desarrollo dirigido por tests.

Como programador autodidacta uno de los grandes problemas que encuentro es aprender de una forma sistemática y fundamentada. Uno no dispone de la estructuración que una formación más reglada te puede proporcionar. De este modo, temas como los patrones de programación o el uso de los test en el proceso de desarrollo no te resultan evidentes al principio, a veces, ni siquiera comprensibles.

Bueno, que me enrollo. Yo quería hablar de cómo llegué al punto en el que efectivamente uso los tests para probar mi software y cada vez más sigo una metodología dirigida por ellos. Pero quizá convenga empezar por explicar que es eso de los tests.

Test: probando el software

Al principio uno escribe trozos de código y ejecuta el programa para ver qué pasa. Es una manera básica de probar un software. De hecho, eso es básicamente un test de software: ejecutarlo y ver si ofrece los resultados esperados.

Cuando empecé con CakePHP, gracias a que proporciona una base completa para una aplicación, hacía más o menos eso mismo.

Estos tests "a ojímetro" no funcionan tan mal en situaciones sencillas, pero en cuanto las cosas empiezan a complicarse se vuelven ineficientes en progresión geométrica. En un momento dado resultan inútiles.

Cuando aparece un error en la aplicación, puede que haya fallado dentro de una función o método, pero es muy posible que la causa del error esté muy lejos en la pila de llamadas. Es decir, que el error real esté en un lugar y con la información disponible no tengamos forma de encontrarlo sin revisar toda la aplicación.

Eso sin mencionar la dificultad, por ejemplo, de probar los múltiples escenarios en que una aplicación puede funcionar. ¿De qué manera puedes saber que has probado el efecto de introducir ciertos datos de diferentes maneras? ¿Alguno de ellos puede generar un error?

Y, finalmente, si realizas un cambio de código, ¿cómo vuelves a probar todo otra vez para asegurarte de que el cambio no tiene efectos indeseados en otra parte?

Se hace necesario utilizar una metodología más sistemática y eficaz, que permita replicar las pruebas las veces que haga falta en multiplicidad de condiciones. Aquí es donde entra el Unit-testing.

Unit-rest: prueba de unidades de software

Como dice el título del apartado, el unit-test es una metodología de prueba de software que se basa en la prueba aislada de las unidades mínimas en que podemos dividir nuestro código. En el caso de CakePHP, que es un framework orientado a objetos, esas unidades son los métodos de las diferentes clases que componen la aplicación.

Por otro lado, los test serían programas que se encargan de llamar a las distintas unidades con diferentes condiciones (parámetros que se pasan, constantes globales, etc.) y comparar el resultado que ofrecen con los resultados que esperamos. Por ejemplo, si una función calcula el doble de un número, el test consistiría en algo así como:

$resultado = dobleDe(100);
    $esperado = 200;
if($resultado == $esperado) {
    echo 'OK';
} else {
    echo 'algo falla en dobleDe';
}

Al ser un programa podemos repetirlo cuantas veces queramos, en especial si hacemos algún cambio en la función dobleDe(), lo que nos diría si nuesto cambio o refactor está afectando al funcionamiento del código.

También podríamos probar multitud de valores para asegurarnos de que la función devuelve los valores correctos, sobre todo en ciertos puntos críticos. Por ejemplo, una función para calcular el precio con descuento por volumen en una tienda podría tener los intervalos:

Unidades                  descuento
menos de 10 ud.       0 %
entre 10 y 20 ud       3% de descuento
más de 20 ud            5% de descuento

Aquí tendríamos que probar al menos los siguientes valores de unidades:

<10 =10 >20 y <20 =20 >20

Es decir, tendríamos que escribir al menos 5 test variando las unidades de producto para ver si la función nos devuelve el precio correcto para una combinación de producto y cantidades.

Ayudas al Unit-Test

Por supuesto, escribir los tests "a pelo" es un trabajo considerable. Para ayudar en la tarea existen bibliotecas como SimpleTest, en la cual se basa el Test Suite de CakePHP.

En conjunto la Test Suite nos proporciona un entorno para probar las clases que escribimos para nuestra aplicación, garantizándonos el mínimo de funcionalidad que necesitamos para que nuestros modelos, controladores y vistas puedan ser probados, así como funciones específicas para hacer los tests.

Un concepto básico son las aserciones o asserts. Se trata de afirmaciones que hacemos sobre el resultado de una unidad. Por ejemplo, que el resultado va a ser igual a cierto valor, que será cierto o falso, o que se coincidirá con una determinada expresión regular.

CakePHP tiene una clase CakeTestCase que incorpora la mayoría de asserts que podemos necesitar, así, podremos escribir un test como el siguiente:

$result = $this->Post->find('count');
$this->assertEqual($result, 5):

Que básicamente quiere decir que el find('count') debería encontrar 5 registros en la base de datos que estamos usando de prueba.

Otra ayuda importante son los Mock Objects. Estos objetos nos permiten imitar el comportamiento de objetos de nuestra aplicación, pero sin que ejecuten su código real, sino que ofrecen la misma intefaz y podemos programarlos para que devuelvan ciertos resultados que nos interesen.

Es decir, que si el método que estamos probando llama a un objeto "mockeado", no se ejecutará el código del objeto original, sino que el "mock" nos devolverá el valor que le hemos configurado que devuelva.

Eso nos permite aislar el código que estamos probando del resto de la aplicación, lo que hace más fiable el test (ejecuta sólo el código que probamos) y nos permite jugar con diferentes escenarios.

Un ejemplo típico es hacer un Mock del EmailComponent. Puede que en nuestra máquina de test no podamos enviar correo usando el EmailComponent, pero haciendo un Mock podemos simular que lo ha enviado y basarnos en eso para probar una parte de la aplicación. O también podemos probar la condición de que no funciona y ver cómo la supera nuestra aplicación.

También es posible probar condiciones de error. Es posible utilizar la función expectError para detectar que nuestro código produce un error. Por ejemplo, cuando lanzas un error desde el código si los datos que llegan a un método son inválidos.

La asserts, además, permiten a la Test Suite realizar algunas estadísticas con tus test. De este modo, puedes tener un número de tests sobre una clase y saber cuántos pasan, cuántos fallan y si se han provocado excepciones.

Desarrollo dirigido por tests

El desarrollo dirigido por tests es una metodología en que usas el Unit-testing como base para desarrollar tus aplicaciones. Se trata de escribir los tests antes que el código de las unidades.

¿Cómo?

Sí, al principio me costó mucho entender esta idea, que ahora me parece de lo más evidente.

En el fondo, escribir un test para una unidad de software es definir de una manera formal sus especificaciones e interfaz: qué parámetros debe recibir y qué resultados ha de proporcionar y en qué formato.

Esto puede hacerse antes de escribir el código, por supuesto. Tú sabes lo que quieres que haga un método antes de escribirlo. En realidad, en un equipo de desarrollo, ni siquiera tendría que ser la misma persona la que prepara los test y la que realiza el código.

Preparar el test te hace pensar muy a fondo en la interfaz del método. Y escribir código para cumplir el test te obliga a estar muy enfocado en lo que estás haciendo. Y, sobre todo, te proporciona una red de seguridad para el futuro.

Tests y refactor

En un momento dado te plantearás refactorizar el código. Los tests te ayudan a garantizar que no se rompe nada. Es genial, en serio: incorporas unas modificaciones y pruebas, si falla, revisas de nuevo y reescribes, vuelves a probar, y así sucesivamente hasta que vuelves a pasar el test. Y, si no, puedes volver a la revisión anterior que sí funcionaba.

Si se trata de añadir funcionalidades nuevas, los test también te ayudan. Por un lado, los tests originales te garantizan que el nuevo código no rompe la funcionalidad original. Por otro lado, debes añadir tests que prueben las nuevas características.

Unit-test y calidad de vida

Pues mejora mucho. Me costó llegar a realizar tests para las diferentes clases. Tiene su complicación probar un controlador por ejemplo, o un behavior. Sin embargo, superadas esas dificultades (lo que a su vez me permitió aprender mucho acerca de cómo funciona CakePHP), el resultado no puede ser mejor.

Mi código está mejor escrito y más pensado. Mi trabajo es más focalizado en objetivos concretos. Además, debido a que con frecuencia tengo que interrumpir el desarrollo para dedicarme a otras actividades, me resulta mucho más fácil retomar el trabajo después de un tiempo. También me permite, por ejemplo, dedicar ratos sueltos a resolver pequeños problemas y avanzar en los proyectos, tomando algún problema detectado o algún test fallido y viendo cómo resolverlo.

Al poder trabajar con partes aisladas, no tienes miedo a romper la aplicación y tener muchos frentes abiertos. Te centras en una tarea y la resuelves, luego otra y luego otra.

La inversión de tiempo en aprender a usar los test y en crearlos realmente merece la pena.

Saber si haces buenos tests

El Test Suite incluye soporte para analizar la cobertura de código de tus test mediante Xdebug. Me costó un poco preparar mi sistema para poder utilizarlo y ha sido una especie de revelación cuando lo conseguí. Además, Xdebug mejora la información que te devuelve la aplicación cuando hay errores de PHP, mostrándote la pila de llamadas y diversa información.

La cobertura de código te indica qué porcentaje del código probado es ejecutado en realidad. Te ayuda a descubrir partes del código que no se ejecutan, condiciones que no has probado y otros muchos detalles, pues te muestra los fragmentos no ejecutados.

Al disponer de esta información es más fácil hacer tests para todo tipo de condiciones, o saber si tienes que crear nuevos tests aunque cubras el 100% del código con los que tienes, al añadir nuevas funcionalidades o hacer refactor de un método.

Los límites de los test

Los test no son la solución para garantizar un programa perfectamente libre de errores. Los test te informan de que una unidad de software hace lo que esperas que haga, suponiendo que has cubierto todos los escenarios posibles con los tests.

Sin embargo, la tranquilidad y seguridad que te proporcionan, la focalización que te aportan y la objetividad y claridad que te dan a la hora de definir tus tareas de programación, tienen un valor que compensan claramente estos límites.

domingo, 23 de agosto de 2009

Obtener el nombre de un modelo a partir del array de datos

Actualizado:

José Lorenzo recomienda una forma más compacta en los comentarios:

$modelName = key($model)

No había caído en la función key, que extrae la clave del elemento actual del array, si no hemos hecho nada con él que haya podido variar su puntero interno, el elemento será el primero y el resultado será el mismo que con el otro método.

El método alternativo

Dejo aquí el post y el método original. La diferencia es que éste siempre devolverá la primera clave, con independencia de dónde esté el puntero del array.

Esta semana estoy trabajando en un sistema de autorización basado en reglas que me está trayendo de cabeza. Mientras tanto, este pequeño snippet me está resultando muy útil, he debido leerlo en algún sitio porque a mí solo no se puede ocurrir, pero no recuerdo dónde:

$modelName = array_shift(array_keys($model));

Extrae la clave del array de modelo, si tienes un array de datos de un modelo sin que tengas referencia a la clase ni forma de obtnerla. Algo del tipo:

$model = array('Modelo' => array('id' => 12312, 'name' => 'fede'));

El código extraerá correctamente 'Modelo' en $modelName, y ya lo puedes utilizar para lo que quieras.


jueves, 20 de agosto de 2009

Plantillas para generar vistas con Bake

Una de las estrategias que puede ayudar más en agilizar el desarrollo con CakePHP es hacer un uso intensivo del shell Bake.

A poco que conozcas Cake habrás oído hablar de esta utilidad capaz de generar modelos, controladores y vistas, prefabricados, proporcionándote así una buena base para trabajar, tanto que a veces no necesitas mucho más para disponer de un "backend" suficiente.

Sin embargo, es posible que no te guste de todo el código generado, o que no se adapte completamente a lo que necesitas o a los requisitos del proyecto. ¿No sería estupendo poder personalizar ese código?

En este post voy a intentar explicar las bases de cómo crear plantillas para las vistas generadas con Bake.

Las plantillas

Es una buena idea echar un vistazo a las que vienen de serie, que se encuentran en la carpeta cake/console/libs/templates/views. Parece un galimatías, ¿verdad? Bueno, no es tan complicado una vez que comprendes como va.

Para comenzar a modificar estas plantillas lo mejor es que las copies a la carpeta de la aplicación. Las debes poner en app/vendors/shells/templates/views.

Hay cuatro plantillas:

form.ctp: para generar las vistas add.ctp, admin_add.ctp, edit.ctp y admin_edit.ctp.
home.ctp: para generar la vista home.ctp
index.ctp: para generar las vistas index.ctp y admin_index.ctp
view.ctp: para generar las vistas view.ctp y admin_view.ctp

¿Cómo se escriben estas plantillas?

El HTML se escribe directamente, como si fuese una vista normal.

Puedes usar código CakePHP para generar partes de la vista. Dispones de varias variables que hacen referencia al modelo y los datos que el controlador pasará a la vista. Se asumen varias convenciones que intentaré esplicar:

$action: contiene la acción que será llamada (ejemplo "add"). Puedes chequearla para incluir o no elementos según sea una acción de crear o editar el registro.
$fields: contiene un array con los campos del modelo.
$associations: un array que contiene las asociaciones del modelo, con las cuatro claves (hasAndBelongsToMany, belongsTo, hasMany y hasOne).
$singularHumanName y $plurarHumanName: el nombre del modelo en formato legible por humanos.
$modelClass: la clase del modelo
$displayField: el campo que esté definido como tal en el modelo, o el que Cake toma por defecto (normalmente title).
$pluralVar: el nombre del modelo en plural en formato de variable. Se asume que una acción index pasas los resultados del find o del paginate a la vista en una variable con el nombre del modelo en plural. Por ejemplo, si el modelo es Post, la variable en la vista sería $posts.
$schema: nada menos que el schema o definición de la tabla en la base de datos.

El código PHP que quieras que tenga la vista se tiene que escribir indirectamente a través de PHP, habitualmente usando algo así como:

<div id="view_title">
<h1><?php echo "<?php __('Admin $pluralHumanName'); ?>" ?></h1>
<p><?php echo "<?php __('Manage $pluralHumanName records'); ?>" ?></p>
</div>
Fíjate que los prefijos $ de las variables "reales" deben ser escapados con \ para evitar un error de variable inexistente en la generación. Aparte de eso, puedes usar las variables de la plantilla a tu gusto.

En fin, quizá sea mejor que veas algunos ejemplos. En particular, las vistas edit e index, que son las que yo he modificado.


¿Personalizar modelos y controladores?

No he entrado en este tema, pero básicamente consiste en modificar las tasks que los generan, trabajo que parece más tedioso que difícil. Echa un vistazo en cake/console/libs/tasks/. Puedes copiar las tasks en app/vendors/shells/tasks para modificarlas.

El código es bastante claro, por lo que no parece difícil adaptarlo a tus preferencias concretas.

martes, 18 de agosto de 2009

Volver a la página correcta (y a la ordenación, también)

Este es un tema recurrente. La situación típica es la siguiente:

Nos encontramos en la típica vista index que muestra un listado paginado de registros (posts, usuarios, lo que sea). Supongamos que estamos en la página 17 de 25, con los registros ordenados por algún criterio y que pulsamos el enlace para ver o editar alguno.

Al volver de la edición, nos llevamos el gran palo: otra vez en la primera página y ordenados por id.

¿Cómo hacemos para volver a la misma página y a ser posible a las mismas condiciones de ordenación?

Hay varias técnicas, la última que estoy probando la encontré en este enlace. La explico:

Para obtener la URL a la que queremos redirigir recurriremos al método referer() del controlador, que nos devuelve la URL de la que venimos. Este método nos conserva los parámetros del URL relacionados con la paginación.

Si estábamos en /app/posts/index/page:17 y consultamos referer() en admin_edit(), por ejemplo, nos devolverá ese valor.

Sin embargo, recuerda que las acciones como admin_edit son llamadas dos veces consecutivas. La primera es explícita, cuando hacemos clic en el enlace que nos lleva a ella. En esa situación, referer() nos devolverá la URL de la acción de procedencia. Por tanto, esa es la URL que deberíamos usar para redirigir después de guardar los cambios.

Pero la segunda vez es implícita, cuando pulsamos el botón de envío del formulario, volvemos a admin_edit, con lo cual referer() tendrá la propia URL de la acción. Es en esta pasada cuando se realiza habitualmente la redirección a la acción de index.

Dicho de otro modo: la primera vez que llegamos a la acción editora tenemos que obtener referer() y guardarlo de algún modo para recuperarlo en la segunda pasada.

Una forma es pasar el valor de referer() a la vista en la primera pasada.

$this->set('returnTo', $this->referer());


Así podremos recuperarlo para varios usos:

* Para crear enlaces de cancelación o regreso.

link(__('Return to Admin Index', true), $returnTo);?>

* Para añadir un campo oculto al formulario en el que anotar ese valor y poder retomarlo en la segunda pasada.

input('App.returnTo', array('type' => 'hidden', 'value' => $returnTo)); ?>


Esta última opción, nos permite recuperar la URL correcta, con todos los parámetros de paginación que tuviese, y redirigir la acción correctamente. El valor estará en el array $this->data, bajo las claves con las que hayas puesto el nombre del campo en el formulario.

if (isset($this->data['App']['returnTo'])) {
$this->redirect($this->data['App']['returnTo']);
}

Otro caso

Otra situación es la que ocurre en acciones que redirigen al index sin mostrar ninguna vista, como podría ser un enlace para borrar registros.

En ese caso, podemos redirigir al referer().

$this->redirect($this->referer()

Siempre puede ser conveniente tener un plan B, es decir, en caso de que referer() nos devuelva un valor no válido, tratar de redirigir a la acción básica, aunque perdamos la información de paginado.

Múltiples botones submit en un formulario

Necesitaba conseguir lo que dice el título, o sea, disponer de varios botones de submit en un formulario, de modo que, además del envío normal del formulario, pudiese detectar qué botón concreto se ha pulsado para realizar ciertas operaciones en función del mismo.

Lo que no sabía era cómo hacerlo "a la Cake".

Bueno, pues es sencillo:

1. En el formulario, añade un control $form->submit con opciones para indicarle un nombre y un valor que luego podrás capturar en el controller. Por ejemplo:
$form->submit(__('Reset Filter', true), array('value' => true, 'name' => 'resetFilter'))
Ahora, para comprobar si el botón ha sido pulsado, no tienes más que mirar en la propiedad params del controlador, de esta manera:
$this->params['form']['resetFilter']
Que contendrá el valor del botón que hayas pulsado. Es decir, puedes tener varios botones con el mismo "name" y distinto valor, o bien simplemente botones con distintos "name" y chequear si dentro de $this->params['form'] existen las claves correspondientes.

domingo, 16 de agosto de 2009

Delegar acciones en AppController

Al trabajar en mi actual proyecto me estaba molestando la cantidad de código básicamente idéntico que se produce.

Como he comentado de pasada en una entrada anterior, estoy trabajando desde un enfoque modular basado en plugins para poder incorporar funcionalidades de forma sencilla a las aplicaciones que tendré que desarrollar durante el próximo año.

Ocurre que hay plugins que básicamente aportan un modelo y un controlador con las acciones básicas de administración y que, aparte de usar un modelo diferente y algún otro detalle menor, son calcados unos a otros.

Por otra parte, y por alguna razón, no consigo que Bake me genere el código de los controladores en los plugins. Los tests de los controladores, además, son copias unos de otros en los que lo único que tengo que hacer es sustituir el nombre del Modelo y poco más.

El caso es que me sonaba haber leído algo al respecto y rebuscando en Google encontré un post de AD7six sobre la personalización de plantillas para Bake que también comenta el mismo tema.

En pocas palabras, se trata de escribir en AppController las acciones administrativas de una manera genérica, aprovechando algunas propiedades del propio AppController. Gracias a la herencia, CakePHP lanzará las acciones adecuadas.

A partir del artículo añadí algunos pequeños retoques, como varios callbacks para permitirme personalizar un poco el comportamiento de cada controlador, de modo que no necesite reescribir el método entero cuando tengo que hacer algunos ajustes menores (por ejemplo, cambiar un campo del array de datos del formulario de forma incondicional y así).

El resultado (todavía en progreso) es que ahora no tengo que escribir código para tener un controlador básico: algo parecido al scaffolding pero que sí puedo utilizar en producción. Lo puedes ver pinchando en el enlace siguiente, como digo, es un trabajo en desarrollo y aún faltan muchas cosas (por ejemplo, controlar que no se pueda llamar a la url /app/admin_add y cosas por el estilo).

app_controller.php

Actualización de Validation Cheatsheet

Pulsando en el título de la entrada... He añadido la regla isEmpty

viernes, 14 de agosto de 2009

8 preguntas para diseñar una interfaz web

Pensando en cómo estructurar las vistas administrativas del proyecto acabé redactando una especie de "checklist" con la que me he guiado para determinar qué elementos debería tener cada una de ellas. No tengo mucho tiempo estos días para hacer un bonito PDF Pincha en el enlace para obtener un PDF con la checklist, pero pensé que escribiéndolas aquí bastaría para tenerlas a mano y si además alguien pudiese hacer sugerencias sobre ellas pues mejor.

La idea de partida era imaginar qué preguntas se haría un usuario al llegar a una de las vistas y si en ella habría elementos capaces de responder a las 8 preguntas. Se supone que si la interfaz responde clara y rápidamente, es una buena interfaz.

Las preguntas son:

1. ¿Puedo volver a donde estaba sin tocar nada?

2. ¿Dónde estoy ahora?

3. ¿Qué se supone que puedo/debo hacer aquí?

4. ¿Cómo lo puedo hacer?

5. ¿Dónde se lleva a cabo la acción?

6. ¿Puedo arrepentirme y salir sin estropear nada?

7. ¿Cómo ejecuto/confirmo la acción?

8. ¿Puedo volver al punto de partida o moverme a otro lugar?

martes, 11 de agosto de 2009

Usando Media View para descargar archivos

Estoy trabajando en un plugin para gestionar archivos subidos a una aplicación web y entre otras cosas he aprendido a utilizar las Media Views para facilitar las descargas de esos mismos archivos, estén almacenados donde estén.

La documentación base que he utilizado es la página del manual y un post del siempre recomendable blog sobre CakePHP de Teknoid. A partir de aquí, y tras unas pocas pruebas parece que he conseguido dominar este tipo de vistas.

Qué es una Media View

Se trata de un tipo de vista especial para enviar archivos binarios al usuario. Se utiliza en lugar de las vistas por defecto y la verdad es que se encarga de casi todo ella solita.

Hay dos ventajas principales en usar MediaView en lugar de enlaces directos al archivo:

  • Podemos enviar archivos que no estén bajo el webroot, evitando así que los usuarios puedan descargarlos reconstruyendo la URL, algo relativamente fácil conociendo cómo funciona CakePHP
  • Al realizar la descarga a través de una acción de un controlador, ésta puede tener el acceso controlado a través de Auth o el método que tengamos, con lo cual, incluso con la URL sólo los usuarios autorizados podrían descargar el archivo.
Empleando Media View

Para poder empezar a utilizarlas debemos especificarlo en la acción apropiada del controlador, con la siguiente línea:

$this->view = 'Media';

A continuación hay que pasarle varios parámetros para identificar el archivo, una forma sencilla sería así:

$params = array(
'id' => 'nombre_del_archivo.ext',
'name' => 'Nombre Humano del Archivo',
'download' => true,
'extension' => 'ext',
'mimeType' => array('ext' => 'mimeType/subtype'),
'path' => 'path/a/la/carpeta/del/archivo/',
);
$this->set($params);

Trataré de explicarme:

Empiezo por el final. A la vista habría que pasarle los parámetros id, name, etc. Pasando un array con las claves necesarias hacemos lo mismo. A mí al principio me extrañó esta manera de hacerlo (lo típico es pasar $this->set('params', $params);) pero al ver el código de media.php, me quedó claro que CakePHP desempaqueta el array. En consecuencia, puedes ponerle al array el nombre que quieras, no hace falta que sea $params mientras que las claves sean correctas.

Ahora voy por los parámetros o claves del array.

id: Es el nombre del archivo que vamos a enviar, tal cual está en el sistema de archivos.

name: Es el nombre con el que queremos que el usuario guarde el archivo en su disco duro, esto nos permite ponerle un nombre humanizado si preferimos.

extension: Se explica solo, es la extensión del archivo

download: si está en true, fuerza la descarga el archivo

mimeType: esta parte tiene truco. A mí me costó un poquito cogerle el punto. La clase MediaView "reconoce" una buena cantidad de tipos mime que podrían tener los archivos, pero no todos. El problema es que no se descargan si no proporcionamos el mimeType correcto. Afortunadamente, en esta clave podemos declarar uno o más mimeType aceptables para cada archivo concreto. La forma de declararlos es poner la extensión como clave y como valor el tipo mime. En mi caso, al subir el archivo tomo nota del mimeType y no tengo más que recuperarlo en la acción de descarga y pasarlo en esta clave. De este modo me ha permitido descargar diversos tipos de archivos que MediaView no admitía (como los .rar).

path: es la ruta al archivo relativa a la carpeta de la aplicación (concretamente utiliza el valor de la constante APP), por lo cual, podemos descargar archivos que no estén bajo el webroot, sino en un nivel superior y fuera de él. Esto es ideal si no queremos que los usuarios puedan descargar archivos "imaginándose" la URL. Debe llevar el separador de directorios al final. Recuerda construir los paths usando la constante DS si necesitas que la aplicación sea portable.

size: se puede especificar la clave size para indicar el tamaño de archivo, pero yo no he conseguido que los archivos se descargasen. No sé dónde esta el error.

Y ya está.

jueves, 16 de julio de 2009

Usando plugins para modularizar aplicaciones

Después de una temporada de alejamiento "forzoso" del desarrollo con CakePHP he decidido afrontar un par de nuevos proyectos utilizando nuevos enfoques en mi metodología de trabajo.

El primero de ellos es la modularización del código en base a plugins. El otro es la introducción del unit-testing y del desarrollo dirigido por tests.

Hoy voy a escribir sobre los plugins, por qué me he decidido a replantear mis aplicaciones con ellos y qué detalles he descubierto que hay que tener en cuenta para hacerlos funcionar bien.

Modularizar con plugins

Una queja habitual de la estructura de CakePHP es que no se pueden organizar los modelos, controladores y vistas de forma significativa, pues todos los modelos tienen que ir juntos en su carpeta, al igual que todos los controladores, etc.

Los plugins nos permiten abordar ese problema ya que con ellos agrupamos los modelos, controladores y vistas (y sus extensiones) que guarden alguna relación. Cada plugin es como una mini-aplicación CakePHP y en su interior reproduce su estructura general.

Los plugins pueden ser bastante autocontenidos, de modo que es sencillo reutilizarlos en nuevos proyectos. Supongamos, por ejemplo, que hemos creado un sistema de gestión de usuarios y grupos con el que nos encontramos muy a gusto. Si lo tenemos como plugin, tan sólo debemos copiar la carpeta que lo contiene en la nueva aplicación para disponer de toda la funcionalidad.

En cambio, si no está en forma de plugin, es más fácil perder la pista de algún archivo a la hora de copiarlo, en especial si se manejan varios modelos, etc.

Mi objetivo al usar plugins

Pues mi objetivo es muy sencillo. Se trata de tener "piezas" de código relativamente independientes que puedan servir para montar una diversidad de aplicaciones. De este modo dispondré de una especie de "superframework" con módulos reutilizables. Cada aplicación o proyecto tendría partes propias, y partes comunes. De este modo, al avanzar en uno, estoy contribuyendo a otros. Y, por otro lado, me puedo concentrar en el núcleo de una aplicación específica.

Y en caso de trabajar en equipo, es fácil distribuir el trabajo.

Por otra parte, los plugins son también una buena manera de crear bibliotecas de behaviors, components o helpers, que puedan reutilizarse en varios proyectos. Lo bueno, es que puedes agruparlos por algún criterio útil. Por, ejemplo, una biblioteca de helpers relacionados con la interfaz de usuario. O un módulo con un behavior para añadir comentarios a cualquier otro objeto, que incluye la gestión de los mismos.

Creando un plugin

Lo mejor es utilizar cake bake para crear la estructura básica del plugin.

cake bake plugin nombre_del_plugin

Lo más importante a tener en cuenta es que los modelos y controladores dentro de un plugin no deberían descender de AppModel o de AppController, sino indirectamente, a través de AppPluginModel o AppPluginController. Cake Bake se ocupa de eso por nosotros.

También podemos generar modelos y controladores básicos mediante cake bake

cake bake plugin NombreDelPlugin model NombreDelModelo

Accediendo a las acciones de un plugin

Las URL que nos llevan a un plugin son las típicas de cake prefijadas con el nombre del plugin:

/plugin/controller/action/param

Como, por ejemplo:

/content/posts/edit/first_post
/admin/tickets/tickets/edit/123

Como puedes ver, también es posible usar el admin routing con plugins.

Por otra parte, es perfectamente posible llamar a las acciones de los plugins con RequestAction.

Creando URL

En el caso de que crees las URL para link o redirect con el formato array, debes incluir la clave 'plugin' con valor true.

array('admin' => true, 'plugin' => true, 'action' => 'index')

Asociaciones con modelos dentro de un plugin

Cuando necesitamos asociar un modelo con otro que está en un plugin, tenemos que indicárselo. Puede ser en la forma simple:

var $hasMany = array('Plugin.Modelo');

O si tienes que especificar parámetros de la asociación, incluyendo la clave

'className' = 'Plugin.Modelo',

Recuerda que ésta, y las demás indicaciones, se aplican incluso cuando los modelos asociados están en el mismo plugin.

Usando behaviors, components o helper de un plugin

Decláralos como siempre, añadiendo antes el nombre del plugin, como en este ejemplo:

var $actsAs = array('Comments.Commentable);

Usando elementos de un plugin

Entre los parámetros que pasamos al elemento, hay que indicar la clave 'plugin' y el nombre del plugin.

element('ejemplo', array('plugin' => 'el_plugin')); ?>

Importando modelos o controladores

Hay que indicar el nombre del plugin, para que importe el objeto correcto.

App::import('Model', 'Plugin.Model');
App::import('Controller', 'Plugin.Controller');

Testing con plugins

Algunas cosas que he observado que es necesario tener en cuenta son:

Los controladores que usamos para testar deben incluir el parámetro plugin (gracias a Mark Story)

class TestPostsController extends PostsController {
var $plugin = 'Content';
}

Y hay que tener en cuenta el apartado anterior para importar los objetos que estén en plugins.

La declaración de las fixtures, también lo tiene en cuenta:

var $fixtures = array('plugin.content.post);

El test suite de CakePHP trata cada plugin aisladamente, lo que resulta muy cómodo y casi te evita tener que hacer grupos de tests por funcionalidad u otro criterio.

Shells

Puedes empaquetar shells en plugins. Crea la ruta vendors/shell dentro de la carpeta del plugin que se trate y pon ahí los shells que desees. La utilidad de línea de comandos cake sabrá acceder a ellos.

Por supuesto, hay más información en el Cookbook.

viernes, 27 de marzo de 2009

Autoajuste desarrollo-producción

Si haces clic en el título de la entrada podrás visitar el artículo de Joel Moss sobre cómo seleccionar automáticamente la conexión de base de datos según estés en el servidor de producción o en el de desarrollo.

En mi caso he realizado una pequeña modificación respecto al original, como se puede apreciar en mi database.php. En principio, usar env() es más seguro porque CakePHP utiliza varios recursos si no puede obtener directamente la variable HTTP_HOST.


class DATABASE_CONFIG {

var $default = array(
'driver' => 'mysql',
'connect' => 'mysql_connect',
'host' => 'localhost',
'port' => '8889',
'login' => 'xxxxxxxx',
'password' => 'xxxxxxxx',
'database' => 'intranet',
'encoding' => 'utf8',
'prefix' => ''
);

var $production = array(
'driver' => 'mysql',
'connect' => 'mysql_connect',
'host' => '172.16.0.150',
'login' => 'xxxxxxxx',
'password' => 'xxxxxxxx',
'database' => 'intranet',
'encoding' => 'utf8',
'prefix' => ''
);

var $test = array(
'driver' => 'mysql',
'connect' => 'mysql_connect',
'host' => 'localhost',
'port' => '8889',
'login' => 'xxxxxxxx',
'password' => 'xxxxxxxx',
'database' => 'intranet_test',
'encoding' => 'utf8',
'prefix' => ''
);

/**
* http://bakery.cakephp.org/articles/view/easy-peasy-database-config
*
* Joel Moss' technique to auto-config app to work under production and development
* a bit modified by myself
*
*/

function __construct() {
$server = env('HTTP_HOST');
if($server != 'localhost:8888') {
$this->default = $this->production;
}
}

function DATABASE_CONFIG() {
$this->__construct();
}

}
?>

Por otra parte, mis dos máquinas de desarrollo (la del trabajo y la de casa) están configuradas con MAMP y con los mismos parámetros, por lo que me resulta más fácil controlar si estoy o no en local. De todos modos, el sistema es fácilmente generalizable a cualquier planteamiento. Sólo necesitas definir las distintas configuraciones de base de datos y establecer el criterio que escoja una u otra según el servidor.

El otro punto a controlar es el nivel de Debug, que también he automatizado mediante la misma técnica. He añadido esto en el core.php.


$server = env('HTTP_HOST');
$debug = ife($server != 'localhost:8888', 0, 2);
Configure::write('debug', $debug);

domingo, 25 de enero de 2009

URL mejoradas con rutas personalizadas

La idea es la siguiente. A veces la estructura /controller/action/arguments de algunas URL de Cake son poco significativas para los usuarios. Es un detalle que a veces me resulta un poco molesto y en particular para un proyecto que tengo entre manos. Por ejemplo. Supongamos que tengo un CMS y quiero mostrar los comentarios a un post. Una posible URL sería:

/posts/show_comments/slug_del_post

Una URL mucho mejor sería

/posts/slug_del_post/show_comments

Esto se puede conseguir definiendo una ruta de la siguiente manera:

Router::connect('/posts/:slug/:action/*', array('controller' => 'posts'), array('pass' => array('slug')));

Ahora, la segunda URL nos dispara la acción show_comments del Posts controller y como extra, le pasa el argumento slug, de modo que la puedes definir así:

function show_comments ($slug) {...}

Todo esto sacado del manual