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á.