miércoles, 3 de octubre de 2007

Diversión con el Form Helper

O más bien con el método input, el encargado de crear los campos de formulario de este Helper.

El método input es una especie de Juan-Palomo-yo-me-lo-guiso-yo-me-lo-como, porque basta pasarle como parámetro el nombre del campo para que él mismo indague por su cuenta y genere un código html bastante adecuado.

El comportamiento estándar de FormHelper::input consiste en generar una DIV que incluye LABEL y el correspondiente INPUT, adecuado al tipo de datos de la columna de la base de datos. De este modo genera controles para fechas, para cajas de texto, checkboxes, etc.

El método admite un segundo parámetro en forma de array, para pasar cuantas opciones nos parezcan necesarias para modular su comportamiento y obtener el resultado más adecuado. La forma de pasarlas es muy sencilla, el nombre de la opción y su valor se pasan como pares key => value.

class sirve para añadir un atributo class al tag INPUT, excepto que su valor sea "required", en cuyo caso el atributo se añada a la DIV contenedora, que queda de la forma DIV class="input required". De este modo puedes asignarles reglas CSS para que tengan un aspecto visual diferente. Si pones otro nombre en class, se aplicará sólo a INPUT.

size añade el atributo size a los INPUT de tipo texto. CakePHP los genera con el tamaño máximo asignado según lo hayamos definido en la base de datos, pero size nos permite controlar el tamaño visible. Si queremos definir el tamaño de una caja de texto usaremos los atributos rows y cols.

type, como su nombre indica sirve para definir qué tipo de control queremos: text, checkbox, radio o select.

label nos permite indicar una etiqueta de texto para el campo. CakePHP utilizará el nombre de campo definido en la base de datos, pero nosotros podríamos preferir utilizar otro.

div nos permite dirigir opciones a la div contenedora del campo. En este caso, el valor de div es un array con las opciones aplicables. De este modo, podríamos asignarle una class distinta. Eso sí, se sobreescribe la class por defecto (input), lo que deberás tener en cuenta si te interesa mantenerla. Por ejemplo, haciendo así

...'div' => array ('class' => 'input miclase')...


Si el type es select, puedes pasar unas cuantas opciones al campo:

options es un array con las opciones que vas a ofrecer al usuario, en la forma 'valor' => 'texto visible'. Es decir, puedes indicar el valor que pasas a la aplicación aunque pongas un texto visible diferente más comprensible para el usuario.

selected te permite indicar si quieres presentar una opción como seleccionada. El valor null toma el valor que tenga el modelo, en su caso.

empty es el texto que quieres que aparezca cuando no hay nada seleccionado. El lugar adecuado para poner aquello de "Selecciona una opción, por favor".

<?php
echo $form->input ('actividad',
array(
'type' => 'select',
'options' => array(
'Taller' => 'Taller práctico herramientas TIC creativas',
'Reunión' => 'Reunión/encuentro del Proyecto Grimm'
),
'selected' => null,
'empty' => 'Selecciona una actividad, por favor.'
)
)
?>


Se pueden usar otras opciones, como before y after, que te permiten añadir lo que consideres conveniente antes y después del código del campo (siempre dentro de la DIV).

Y esto es todo, de momento, me quedan algunas cosas más que no he podido mirar en profundidad, pero con esto ya tienes para generar unos buenos formularios.

lunes, 3 de septiembre de 2007

Cake Bake y MAMP

Llevaba un tiempo bastante molesto para mi incapacidad para usar las herramientas de generación de CakePHP, la antigua utilidad Bake (actualmente cake bake). El problema es que siempre me salía con un error de conexión con la base de datos.

Yo utilizo en mi máquina de trabajo el paquete MAMP (Apache-MySQL-PHP para Mac OS X), instalado tal como viene por defecto.

El caso es que he podido entender el problema y solucionarlo.

bake busca comunicarse con la base de datos a través del socket

/var/mysql/mysql.sock

Pero si instalamos MAMP sin tocar su configuración para nada, el socket está en:

/Applications/MAMP/tmp/mysql/mysql.sock

La solución es crear un enlace simbólico:

ln -s /Applications/MAMP/tmp/mysql/mysql.sock /var/mysql/mysql.sock

Pero hay un par de requisitos previos. Primero tenemos que crear un directorio mysql bajo var:

cd /var
sudo mkdir mysql

y luego crear el enlace

sudo ln -s /Applications/MAMP/tmp/mysql/mysql.sock /var/mysql/mysql.sock

Y listo.

Nota final

Una cosilla... uso sudo porque al menos en Mac OS X mi usuario de trabajo no tiene permisos sobre la carpeta var para crear el directorio y el enlace simbólico, sin embargo, éste funciona sin ningún problema.

martes, 28 de agosto de 2007

Páginas estáticas en CakePHP (actualizado)

A raíz de un mensaje en el grupo Google de CakePHP me he puesto a probar el tema de las páginas estáticas.

No es nada difícil, aunque conviene tener en cuenta alguna cosilla para no extrañarse la primera vez.

Las páginas se ponen en la carpeta app/view/pages, con extensión .ctp. Por ejemplo, pagina.ctp

Además, lo que se pone en la página es el contenido (lo que va dentro del body, vamos) ya que la página se carga dentro del layout que toque. Es decir, no tienes que especificar tags body, ni mucho menos cabeceras html en la página, sino que van en el layout.

Para llamar a una página estática es tan simple como:

http://dominio.tld/pages/pagina

He investigado un poco sobre la pregunta de Pablo, y me encontrado una forma de fijar un título para la página estática. Consiste en añadir la siguiente línea


<?php $this->pageTitle = 'Titulo para la página'; ?>


$this se refiere a la View que muestra esta página. No es muy "limpio" debido a que accedemos directamente a una propiedad de View, pero reesulta efectivo.

Capturar una URL (actualizado)

Después de un rato dándole vueltas y haciendo debug de un controller, se me ha ocurrido este código para obtener un array de la página solicitada. Para hacerlo directamente en el controller, debería bastar con eliminar los $this->controller


$redirectAfterLogin = array (
'controller' => $this->controller->params['controller'],
'action' => $this->controller->params['action'],
);
if (isset($this->controller->params['pass'])) {
foreach ($this->controller->params['pass'] as $pass) {
$redirectAfterLogin[] = $pass;
}
}


Esto lo hice para una redirección post-login, esto es, si la página solicitada requiere autentificación hay que anotar qué página es y recuperarla una vez que el usuario ha hecho login correctamente para llevarlo a dónde quería.


Es posible que haya una solución mejor en el propio CakePHP, pero ¿quién sabe dónde?

Claro que la hay

Está en el router:

$redirectAfterLogin = Router::parse ($this->controller->here)


Funciona aún mejor, pues respeta los parámetros con nombre.

lunes, 27 de agosto de 2007

La técnica de los argumentos como array

Una cosa que me llamó la atención al estudiar algunos ejemplos de código en CakePHP es el amplio uso de los arrays asociativos para pasar múltiples argumentos a los métodos, sobre todo conjuntos de ajustes para Behaviors, Helpers o Components.

La técnica básica es muy sencilla, simplemente es definir un array asociativo con las claves que nos interesen y sus valores. Por ejemplo:

$ejemplo = array (
'etiqueta' => 'La que sea',
'repetir' => 5
);


La ventaja es que así resulta bastante fácil mantener la interfaz de un método y pasarle un número indeterminado de parámetros. Sólo tenemos que declarar que pasaremos un array y será problema del método decidir qué hacer con él.

Algo así:

class ejemplo {
function metodo ($argumentos ) {
}
}


Al ser $argumentos un array asociativo, en realidad le podemos pasar cualquier estructura de datos que se nos ocurra.

Valores por defecto

Para resolver el tema de los valores por defecto lo que haremos será definir una variable con una "plantilla" del array y luego mezclarla con el array que pasamos. De este modo, nos aseguramos de que el array final tenga todas las claves necesarias y, a la vez, los valores que hayamos pasado.

Por ejemplo:

$porDefecto = array (
'etiqueta' => '',
'repetir' => 10,
'plantilla' => 'Campo = %campo% ',
);

$array_final = array_merge ($porDefecto, $ejemplo);


Uso

Para utilizar los valores del array podemos optar por dos estrategias:

1. Utilizar el array, como en $array_final['repetir'].
2. Utilizar extract, para convertir las claves del array en variables y poder referirnos a ellas como $repetir y así.

El segundo método parece más legible, pero a veces me resulta más inteligible el primero. Una vez que hemos hecho "extract" de un array nos salen un montón de variables nuevas y puede ser fácil perder la pista de su origen, sin descontar el problema de que pueda haber conflictos de nombres. Pero es una elección personal, claro.

martes, 21 de agosto de 2007

CakePHP-es

Se acaba de poner en marcha hace unos días CakePHP-es , un espacio comunitario sobre CakePHP en español. La iniciativa se ha iniciado con una wiki-traducción del manual actual de CakePHP, lo que va a suponer un alivio para buena parte de los que se están iniciando en este framework.

Esta traducción no es oficial, pero está autorizada por la CakePHP Software Foundation.

Esta web se une al grupo de google ya existente y bastante activo.

Además, se ha puesto en marcha un pastebin, que es una herramienta en línea para publicar y revisar código colectivamente.

O sea, que ya cuentas con unos cuantos recursos en español acerca de CakePHP. ¡A cocinar!

lunes, 6 de agosto de 2007

Curiosas propiedades de los behaviors

Recién vuelto (casi) de las vacaciones, hoy he retomado el proyecto Cake en el que estoy metido. Sigo dado toques a la fase de autentificación para mis aplicaciones web. Concretamente estoy trabajando en un behavior que me permita usar cualquier modelo para autentificar.

Esto es, en lugar de tener un Model Usuario con los métodos necesarios, lo que quiero es trasladar la funcionalidad a un Behavior de modo que el modelo quede "liberado" de la parte de autentificación y que ésta sea reutilizable. La cuestión es que las diferentes aplicaciones que tengo que poner en marcha o reescribir tienen distintos requisitos en lo que respecta al modelo de usuario.

Eso me ha permitido aprender unas cuantas cosas acerca del uso de Behaviors aparte de lo que ya había escrito anteriormente.

Una de las más importantes es la capacidad de añadir métodos al Model como si fueran propios, es decir, los puedes llamar con model->metodo (), aunque los hayas definido en el behavior.

Hay que tener una precaución según desde donde llames al método. Si es desde el propio modelo o behavior no ocurre nada especial. Pero si lo llamas desde un controlador, por ejemplo, CakePHP automágicamente añade una referencia al propio modelo como parámetro.

Por otra parte, la capacidad de integración de los behaviors con los models a mí me sigue pareciendo asombrosa. Así, aunque los behaviors no contemplan el callback beforeValidate, es posible, por ejemplo, invalidar campos de beforeSave, para realizar validaciones personalizadas.

El resultado puede parecer un poquito extraño ya que los formularios se validan entonces en dos etapas (la "normal" y la del "behavior") pero ciertamente funciona como debe. Es decir, la aplicación protesta primero por unos campos y luego por otros. Supongo que se puede mejorar esto, pero aún no he llegado tan lejos.

En cualquier caso, el resultado de este "subproyecto" me ha permitido sacar a un behavior todo el código relacionado con la autentificación, incluyendo el procesado de elementos "extraños", como todos los tejemanejes de contraseñas hasheadas procedentes del formulario pero que no son realmente parte del modelo.

Me explico. Al crear un usuario, por ejemplo, la contraseña es "hasheada" en javascript para que viaje encriptada desde del cliente al servidor. El campo hash contiene esta encriptación, mientras que el campo de la contraseña vuelve vacío. Si no hay javascript la contraseña viene desprotegida en su campo El modelo debe entonces controlar esta situación antes de guardar los datos y asegurarse de que el modelo los contiene tal y como los espera la base de datos. Y eso es justamente lo que hace el behavior en beforeSave.

Claro que el behavior forma parte de un paquete que incluye un helper y un component. La idea final es que este paquete sea una "caja negra" y que el Model no tenga que saber nada sobre ella, ocupándose sólo de sus cosas.

Este es el código que habría que poner en models/helpers/autentificacion.php.


<?php
/**
* autentificacion
*
* Created by Frankie on 2007-07-19.
* Copyright (c) 2007 Macintec. All rights reserved.
* 2007-08-06. Added abstraction to valid() method. The behavior now is model independant
* 2007-08-06. Added beforeSave() method to preprocess incoming data
**/

/**
* This behavioir adds Authentitfication abilities to a model, you can assign fields
* to act as user and password fields...
*
* @package autentificacion
* @author Frankie
**/
class AutentificacionBehavior extends ModelBehavior {
var $default_settings = array (
'user' => 'user',
'password' => 'password'
);
var $settings;
var $model;

function beforeSave (&$model) {
/**
* Si el campo hash contiene valores, significa que esa es la contraseña hasheada
* y por lo tanto, debemos poner ese valor en el campo de contraseña para guardar.
* El Javascript habrá borrado la contraseña en clave, por lo que este campo viene
* vaío.
* Por otro lado, si hash no contiene valores, entonces es que el cliente no tiene
* javascript y la contraseña viene abierta en el campo clave. Por último, en caso
* de que estemos editando el usuario, la clave puede venir vacía. En ese caso,
* se elimina el campo clave para no borrar la clave hash en la base de datos.
* El campo Hash se elimna de todas maneras porque no pertenece al modelo.
* TO DO: Establecer como preferencia que se permitan o no claves en abierto
*
* @author Frankie
*/
if (empty ($model->data[$model->name][$this->settings['password']]) && empty ($model->data[$model->name]['hash']) && empty ($model->id)) {
$model->invalidate ($this->settings['password'], __('Please, you must provide a password', true));
return false;
}

if (!empty ($model->data[$model->name]['hash'])) {
$model->data[$model->name]['clave'] = $model->data[$model->name]['hash'];
} elseif (!empty ($model->data[$model->name]['clave'])) {
$model->data[$model->name]['clave'] = sha1 ($model->data[$model->name]['clave']);
}
if (empty ($model->data[$model->name]['clave']) && !empty ($model->id)) {
unset ($model->data[$model->name]['clave']);
}
unset ($model->data[$model->name]['hash']);
return true;
}

function setup (&$model, $settings) {
// TODO: verificar que los campos pertenecen al modelo ???
$this->settings = am ($this->default_settings, $settings);
$this->model = $model;
}

/**
* Returns user record if the user is a registered user
*
* @return boolean true if user and password exists
* @author Frankie
**/
function valid (&$model, $userData, $salt = false) {
$user = $userData[$model->name][$this->settings['user']];
$password = $userData[$model->name][$this->settings['password']];
if ($salt) {
// User validation with salt
$conditions = "WHERE {$this->settings['user']} = '$user' AND sha1(concat({$this->settings['password']},'$salt')) = '$password'";
} else {
// User validation without salt, insecure login
$conditions = "WHERE {$this->settings['user']} = '$user' AND {$this->settings['password']} = '$password'";
}
if ($userFound = $this->model->find ($conditions)) {
return $userFound;
}
return false;
}


} // END class AutentifiacionBehavior extends ModelBehavior


?>




Y esta es la forma de usarlo en un modelo cualquiera. El modelo tiene los campos id, usuario, clave, email.

Al asociar el behavior indicamos que utilice usuario como campo user y clave como campo password.


<?php
/**
* usuario
*
* Created by Frankie on 2007-07-04.
* Copyright (c) 2007 Macintec. All rights reserved.
**/

/**
* Usuario
*
* @package /Applications/MAMP/htdocs/micake
* @author Fran Iglesias
* @copyright Copyright 2007 Fran Iglesias
* @version Revision: 0.1
* @modifiedby Fran Iglesias
* @lastmodified 2007-07-04
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
**/

class Usuario extends AppModel
{
var $name = "Usuario";
var $validate = array (
'usuario' => array (
'esUnico' => array (
'rule' => 'usuarioEsUnico',
'message' => 'User name exists. Please, choose another.'
),
'usuarioNoVacio' => array (
'rule' => VALID_NOT_EMPTY,
'message' => 'User name empty. Please, give it a name.'
)
),
'email' => array (
'emailValido' => array (
'rule' => VALID_EMAIL,
'message' => 'Please, type a valid email.'
)
)
);

var $actsAs = array ('Autentificacion' => array ('user' => 'usuario', 'password' => 'clave'));

/**
* Verifica que el nombre de usuario aportado no existe en la bd
*
* @return boolean true if unique
* @author Frankie
**/
function usuarioEsUnico ($nombreUsuario) {
$esValido = false;
$condiciones['usuario'] = $nombreUsuario;
if ($this->id) {
$condiciones['id'] = '<> '.$this->id;
}
$esValido = $this->isUnique ($condiciones, false);
return $esValido;
}

}

// END class Usuario
?>

sábado, 14 de julio de 2007

Acceder a valores de los campos de un formulario

Con html::tagValue ($campo) puedes acceder en la vista a los valores de los campos del modelo del que estás construyendo un formulario (son los que el Controller tenga en Controller::data).

O sea, cuando uno hace cosas como ésta:

$this->data = $this->Model->Read ()

puede acceder a esos datos mediante el método tagValue del HtmlHelper.

Muy útil para construir enlaces en la vista, sin tener que crear una variable de vista para guardarlos.

domingo, 8 de julio de 2007

Título de la página desde el controller

Conocía este consejillo de Armando Sosa, para definir el título de la página desde la vista. Pero buscando no sé qué en el API, encontré que si ajustas la variable 'title' en el controller para pasársela a la view, queda fijada como título de la página. O sea, en la acción deseada del controlador ponemos:

$this->set ('title', 'Lo que quieras que sea el título');

Actualización (8-7-07)

La parte "mala" es que $title no aparece como variable de la vista. Sin embargo, puedes usar $this->pageTitle en la vista para acceder a ella.

Diseño de Interfaz: barra de botones (2)

En el post anterior sobre este tema dejaba colgados un par de asuntos. Fundamentalmente, la solución al problema de tener que fijar una altura para el contenedor UL, a fin de que se muestre "envolviendo" los botones.

Por otro lado, tampoco está de más simplificar el ejemplo y reducirlo al mínimo necesario, de modo que los temas estéticos se resuelvan en cada caso particular.

El problema de la altura de un contenedor de elementos flotados

Veamos. Los elementos flotantes (que tienen propiedad float asignada) salen del flujo normal, por lo que se altera visualmente el tamaño o posición de sus elementos contenedores o de los que sigan el flujo normal a partir de ellos. Por esta razón, si en una lista (UL) todos los ítems (LI) son flotantes, la lista no obtiene altura, porque para el motor de dibujado no contiene elementos (que como tienen float, están fuera del flujo y no cuentan a la hora de calcular el tamaño del contenedor).

Un apaño es darle altura explícitamente, pero es problemático si no sabemos exactamente qué altura darle o si el usuario cambia el tamaño de la letra. Así que es una solución menos que óptima.

La solución que he adoptado finalmente ha sido meter todo el conjunto en una DIV, de modo que sea ésta la que se encargue de simular el fondo y de contener visualmente la lista. En principio, va a ocurrir lo mismo, es decir, no va a obtener altura porque el contenido de la lista sigue "flotando". Pero existe una solución sencilla:

Basta con añadir un BR después de la UL al que asignamos clear:both;. De este modo, el BR fuerza a la DIV a "cerrarse" abarcando el contenido aunque esté flotante. El resultado es válido para XHTML, HTML y CSS, o sea, que bien.

El código mostrado a continuación incluye la solución con los estilos simplificados.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Barra de botones para registros</title>
<style type="text/css" media="screen">

/* Se eliminan los asuntos cosméticos y quedan sólo las reglas básicas para lograr la funcionalidad deseada */

div.barra_botones_registro {
display: block;
}

div.barra_botones_registro ul {
margin: 0;
padding: 0;
list-style: none;
}

div.barra_botones_registro ul li {
}

div.barra_botones_registro ul li a {
/* En esta regla se pondrían las características visuales de los botones */
display: block;
border: 1px solid #000;
padding: 2px 4px;
}

div.barra_botones_registro ul li.bloque1 {
float: left;
margin-right: 5px;
margin-left: 0px;
}

div.barra_botones_registro ul li.bloque2 {
float: left;
margin-right: 5px;
margin-left: 0px;
}

div.barra_botones_registro ul li.bloque3 {
float: right;
margin-right: 0;
margin-left: 5px;
}

div.barra_botones_registro ul li.bloque2.primer_boton_bloque2 {
margin-left: 10px;
}


</style>
</head>

<body>
<h1>Barra de botones para registro</h1>
<p>Ejemplo de una barra de botones para un registro. Está pensada para incluir tres grupos de botones, de forma que: </p>
<ul>
<li>Exista una separación lógica y visual</li>
<li>Se puedan prevenir pulsaciones erróneas</li>
</ul>

<div class="barra_botones_registro">
<ul>
<li class="bloque1"><a href="#">Borrar</a></li>
<li class="bloque1"><a href="#">Limpiar formulario</a></li>
<li class="bloque2 primer_boton_bloque2"><a href="#">Lista</a></li>
<li class="bloque2"><a href="http://some-site.com/">Imprimir</a></li>
<li class="bloque3"><a href="http://some-site.com/">Nuevo</a></li>
<li class="bloque3"><a href="http://some-site.com/">Duplicar</a></li>
</ul>
<br style="clear: both;" />
</div>

</body>
</html>

Diseño de interfaz: barra de botones para un registro

Lo mismo resulta que el título no es muy claro, lo que pretendo con esta nota es mostrar una prueba de concepto (qué bien suena eso) sobre cómo podría ser una barra de botones para operaciones con un registro de una tabla de base de datos.

No forma parte, estrictamente, del diseño del formulario, sino que más bien es un añadido en el que damos al usuario los medios para realizar algunas operaciones como volver a la lista de registros, borrar el objeto, crear uno nuevo, etc.

El problema que se trata de resolver es cómo agrupar los botones de una forma lógica, de forma que tengamos algunos de ellos fuera de la ruta de "escaneado visual" para evitar pulsaciones accidentales y que tengamos separados visualmente los bloques de botones.

La idea es que botones para acciones como crear un nuevo registro o editar el que se está viendo aparezcan en la parte inferior derecha, en el lugar donde termina el "escaneado de página", al menos para los lectores occidentales (aunque los principios creo que se pueden aplicar igual en sistemas de escritura con orientación diferente).

Los botones para eliminar registros o similares aparecerían en la esquina inferior izquierda, fuera de la ruta principal, de forma que normalmente el usuario tendría que moverse a propósito para buscarlos y pulsarlos.

En el centro, se pondrían otros botones. Algo más o menos como esta imagen que pego a continuación:


Usando HTML + CSS es posible acercarse bastante a esto. Básicamente se trata de que la barra de botones se asimila a una lista en la que cada ítem es un botón.

Para lograr que los diferentes botones se agrupen, a cada uno se le aplica una regla que lo identifica como propio de un grupo. De este modo, se pueden dar formatos diferentes a los botones.

El último problema sería definir puntos especiales en los puntos de separación de los botones.

Lo que he hecho ha sido empezar con la siguiente lista:


<ul class="barra_botones_registro">
<li class="bloque1">Borrar</li>
<li class="bloque1">Limpiar formulario</li>
<li class="bloque2 primer_boton_bloque2">Lista</li>
<li class="bloque2">Imprimir</li>
<li class="bloque3">Nuevo</li>
<li class="bloque3">Duplicar</li>
</ul>


Si no se especifica ninguna regla de formato, la barra se ve como una lista normal. A todos los ítems se les aplican al menos una regla, que define a qué grupo pertenece. En un caso, se asigna una regla especial para indicar que se trata de un botón que inicia un bloque.

A continuación, iré introduciendo las reglas css.

La lista como barra

Esta regla aplica un estilo a la lista, básicamente para se pueda ver como una barra horizontal que abarca a los demás botones. El punto más "controvertido" es asignar un height. El problema es que con CSS estándar, al estar el contenido "flotado" UL no toma "altura". Una solución sería meter todo en una DIV y que sea ésta la que se encargue de mostrar el "fondo" de la barra, dejando la UL "invisible". Sin embargo, de momento lo voy a dejar así porque me parece que el concepto queda más claro. Aparte de eso, la mayor parte de las reglas son de carácter cosmético.


ul.barra_botones_registro {
display: block;
margin: 0;
padding: 2px 4px;
border: 1px solid black;
background-color: #E1B250;
height: 20px;
}


Pintando botones

El punto más importante aquí es el display: block de los botones. Luego los haremos "flotar" para colocarlos en el sitio adecuado mientras nos dejan el diseño fluido. Lo demás tiene que ver con la cosmética.


ul.barra_botones_registro li {
display: block;
padding: 2px 6px;
border: 2px outset #907130;
font-family: Verdana, "MS Trebuchet", sans-serif;
font-size: 10px;
background-color: #FC6;
}


Colocando los botones

Las siguientes tres reglas definen el comportamiento de los grupos de botones. En el planteamiento que esto haciendo, los dos primeros grupos de botones se colocan hacia la izquierda y el tercero hacia la derecha, lo que sugiere el "float" correspondiente.

En el caso del bloque3, hay que tener en cuenta que el botón que queramos tener más a la derecha ha de ser el primero en la lista.

Las propiedades margin-right y margin-left nos permiten fijar la separación entre los botones. Debería ser igual en todos los bloques (excepto en el primer botón del bloque2, que trataremos en el siguiente punto y que debería separarse más).

De momento, hemos conseguido una separación flexible entre los botones del bloque2 y bloque3.


ul.barra_botones_registro li.bloque1, ul.barra_botones_registro li.bloque2 {
float: left;
margin-right: 5px;
margin-left: 0px;
}

ul.barra_botones_registro li.bloque3 {
float: right;
margin-right: 0;
margin-left: 5px;
}


Separando el segundo bloque

Simplemente tenemos que identificar el primer botón de ese bloque y especificar que debe tener una separación mayor con respecto al bloque anterior.


.primer_boton_bloque2 {
margin-left: 10px;
}


Más allá

Una cosa que podríamos hacer es dar diferente formato visual a los bloques de botones para destacar algunos de ellos, los que tienen la "función primaria" frente a los que tienen funciones secundarias.

Ya que éste es un blog sobre aprendizaje de CakePHP. mencionar que probablemente escriba un helper de forma que sea fácil crear barras de botones que sigan este modelo. Al menos, una vez que consiga resolver el problema del fondo de la barra.

Una nota sobre las reglas

Como puedes ver, mis selectores css describen toda la ruta de la regla en algunos casos. Aparte de mejorar algo la comprensión de la regla (siempre sabes dónde se aplica), es una buena forma de asegurarse de que la regla se aplica sólo donde quieres que se aplique, definiendo su contexto.

miércoles, 4 de julio de 2007

Comprobar que algo es único

Tenía la impresión de haber escrito algo sobre esto, pero parece que no. Así que pego este pequeño método que comprueba si un modelo es único. En este caso, un usuario comprobando si su nombre es único.

La clase Model tiene un método isUnique () al que le podemos pasar varias condiciones, en formato de pares "campo" => "condición" (como los métodos Find) y un segundo parámetro para booleano para responder en cuanto una condición se cumpla. Si le pasas true, isUnique devuelve true con tal de que se cumple alguna de las condiciones. Si le pasas false, tienen que cumplirse todas las condiciones.

Un pequeño falloUna pequeña limitación de esta función es que no está pendiente automáticamente del id del modelo. Esto genera el problema de que cuando editas un modelo, la validación falla porque efectivamente existe un registro igual en la base de datos: justamente el que estás editando. Incluyendo el chequeo del id como condición en caso de que estemos editando un registro, funcionará correctamente. Para saber si un modelo está en "edición" no tenemos más que mirar si hay valor en model::id.

Cuando estamos añadiendo un modelo (todavía no está guardado en la base de datos) el problema no se presenta y valida correctamente.

Pienso que el código se explica bastante bien:

function usuarioEsUnico ($nombreUsuario) {
// Empezamos suponiendo que el nombre no va a validar
$esValido = false;
// La condición básica es que no coincidan los nombres de usuario
$condiciones['usuario'] = $nombreUsuario;
// Si estamos editando (hay valor en model::id, entonces comprobamos que no sea el mismo id
if ($this->id) {
$condiciones['id'] = '<> '.$this->id;
}
// Y ahora preguntamos al modelo si es único
$esValido = $this->isUnique ($condiciones, false);
return $esValido;
}

miércoles, 27 de junio de 2007

Acerca de la autorización (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.


Autorización, permisos, privilegios... como quieras llamarle. Después de aclararme (algo) las ideas con respecto al problema de la autentificación (asegurarse en lo posible de que un usuario es quien dice ser) ahora me toca romperme los cuernos con un sistema de autorización.

CakePHP proporciona algunas herramientas, como el soporte para ACL, pero es una de las partes que al parecer está más inestable en la versión 1.2 y también de las que menos documentación hay. No es que sea especialmente complicado lo de manejar las ACL, pero la implementación concreta igual es un poco abstracta de más por lo que he podido ver echando un vistazo al API.

En algunos de los artículos que he leído al respecto de la autorización he visto ideas que me han llamado la atención. Un sistema de permisos o autorizaciones es algo que tienes que pensar bastante bien porque debería cumplir varios requisitos:
  • Estar lo más aislado posible del código de la aplicación. Esto es, la aplicación no tiene que saber nada sobre el "significado" de un permiso, sino simplemente si en una situación dada un usuario puede o no hacer algo, con uno o varios objetos. Hay que buscar el equilibro entre un modelo que sea abstracto y a la vez tenga significado. Y dado que estamos trabajando con CakePHP un objetivo es que el uso del sistema sea lo menos "intrusivo" posible.
  • Ser fácilmente escalable, o sea, que pueda manejar los permisos con soltura a medida que crece la cantidad de usuarios y objetos, y a la vez, que sea fácil actualizar la estructura de permisos según sea necesario.
  • Granularidad fina: que pueda controlar qué usuario puede hacer qué acción sobre qué objeto.
  • Que funcione. Ya, ya sé. Es de perogrullo, pero un sistema de autorizaciones no sólo tiene que permitir a un usuario hacer lo que se supone que puede hacer, sino también debe prohibirle hacer lo que no puede.
Seguro que se me quedan más cosas, pero éstos requerimientos ya llegan bien para ponerte a pensar.

Enfoques

Existen varios enfoques para diseñar sistemas de autorización. No soy experto en el tema, así que no puedo hablar en mucha profundidad. Hay tres sospechosos habituales:
  • Sistemas "a la Unix", en los que los objetos tienen un propietario que define los permisos básicos (de lectura, de escritura y otros que se puedan definir) para sí mismo, para un grupo y para el resto del mundo.
  • Sistemas basados en ACL (Listas de Control de Acceso). En estas listas, se definen relaciones entre los sujetos, organizados en grupos jerárquicos, y los objetos, de modo que podemos permitir o denegar acceso a un objeto para un grupo atribuyendo en cascada el mismo privilegio a todos los grupos y sujetos que incluye. Y, posteriormente, podríamos añadir reglas para "matizar" esos privilegios en cada caso necesario.
  • Sistemas basados en Roles. Los roles son conjuntos de permisos o privilegios que podemos asociar a los usuarios de forma individual o grupal. Esto es, definimos un rol describiendo todo lo que puede hacer y luego asignamos roles a los usuarios.
Por supuesto, existen sistemas más evolucionados y más o menos complejos. No voy a meterme, necesito desarrollar uno que me permita un control razonable.

Un modelo conceptual

Dándole vueltas he comenzado diseñar un sistema que parece tener buena pinta. Intentaré explicarlo paso a paso. Es bastante dependiente de la forma particular de hacer las cosas en Cake y de momento es sólo teórico, pues no lo he puesto en código aún. Quedan unos cuantos flecos por resolver.

Permisos

Una característica de CakePHP es la estructura de las URL. En una aplicación Cake cualquier cosa que pueda hacer un usuario se puede caracterizar con una URL de la forma /controlador/accion/atributos. Aunque hay casos particulares, nos vale como punto de partida.

Por lo tanto, para definir lo que un usuario puede hacer o no en esa aplicación podríamos empezar estableciendo si puede o no ejecutar la acción indicada por esa URL. Esquemáticamente, podríamos definir permisos en la forma:

Permitir /controlador/accion/
Denegar /controlador/accion/

Esta idea la encontré en este artículo de ThinkingPHP: A lightweight approach to ACL, Y alguna más también.

Por ejemplo, podríamos referirnos a tener acceso a todas las acciones de un controlador concreto mediante un sistema de comodines, o incluso de expresiones regulares:

Permitir /controlador/*/

Podríamos forzar un poco las cosas y ampliar el uso de esta simple estructura con los atributos y referirnos incluso si se puede aplicar la acción a un objeto determinado, pero a la larga eso nos lleva a algunos problemas, así que nos quedaremos ahí de momento.

Roles

Por supuesto, lo anterior no nos vale de mucho si no relacionamos los permisos con alguien que los pueda utilizar o no. Pero en lugar de hacerlo directamente vamos a definir roles, es decir, vamos a agrupar los permisos en perfiles de privilegios.

Los roles pueden tener asociados varios permisos y éstos pueden pertenecer a distintos roles. (o no, este es uno de los puntos que habría que matizar). Pero espero que captes la idea. Es decir, o bien

Roles hasMany Permisos, o bien,
Roles HABTM Permisos

Lo siguiente sería asociar los Roles a los Usuarios que los pueden desempeñar. Un mismo rol se puede asociar a varios usuarios, y un usuario puede tener distintos roles. O sea que:

Usuarios HABTM Roles

De modeo que ahora podemos saber qué permisos tienen un usuario viendo sus roles.

Ámbitos

Pero hay una cosa que no hemos definido. ¿Sobre qué objetos puede actuar un usuario o se puede ejercer un rol?

Una forma simple sería definir el permiso incluyendo en la url el parámetro que define el objeto sobre el que se realiza la acción (normalmente el id). Pero probablemente tropezaremos con un problema al tratar con acciones que implican varios objetos (como un listado de posts de un blog y cosas así). ¿Cómo podemos restringir la búsqueda de objetos para que sólo devuelva los que corresponden al sujeto?

Una posibilidad que se me ocurre es definir ámbitos sobre los que se ejerce un
rol
permiso
. Los ámbitos describen los objetos a los que tendríamos acceso ejerciendo ese rol
permiso
. Se me ocurren los siguientes:
  • Objetos propiedad del usuario
  • Objetos propiedad del grupo o grupos al que pertenezca el usuario
  • Cualquier objeto
  • Objeto especificado por su id
  • Objetos del usuario especificado por su id
  • Objetos del grupo espeificado por su id
(podría haber otros, claro)

Esto implica que llevamos cuenta de la propiedad de los objetos creados. Volveré sobre eso más adelante, de momento nos basta con tener presente que el sistema puede saber qué objetos pertenecen a un usuario.

Definiendo roles

Para definir un rol nos bastaría con darle un nombre, especificar un ámbito y crear permisos asociados con ese rol. El rol de root podría quedar definido así (de manera informal, claro):

rol: root

permisos:
Accion: /*/*/
Permitir: Sí
ambito: Cualquier objeto
rol: root

Un rol de visitante no registrado a un blog podría definirse así para que pueda ver los posts y comentar en ellos:

rol: no registrado

permisos
Accion: /Posts/ver
ambito: objetos del grupo "cualquiera"
Permitir: Sí
rol: no registrado

permisos
Accion /Posts/comentar
ambito: objetos del grupo "cualquiera"
Permitir: Sí
rol: no registrado

Políticas

Un aspecto que nos queda pendiente es saber qué se hace cuando no hay una regla explícita para un permiso. Una forma de resolver esto puede ser estableciendo políticas que regulen la manera en que se asignen permisos por defecto. Por ejemplo, una política podría ser la de denegar cualquier acceso salvo que se indique lo contrario. En cierto modo, una política puede definirse como un permiso por defecto para una determinada acción o para un determinado patrón de acciones.

Propiedad de los objetos

Todo lo anterior nos permitiría saber qué puede hacer un usuario en nuestro sistema, pero no nos dice mucho acerca de sobre qué objetos puede actuar. Los ámbitos nos permiten describir el rango de objetos, pero con referencia a sus propietarios.

Un sistema de propiedad de los objtos es algo tan simple como asociar cada objeto con un usuario o un grupo. Esto se podría hacer en la tabla de cada modelo de objeto "apropiable" o en una tabla aparte en la que se defina el objeto (mediante su modelo y su identificador único) y el sujeto que lo posee (usuario o grupo).

Una de las cosas maravillosas de CakePHP 1.2 es que este proceso de asignar la propiedad de los objetos se podría "automatizar" mediante un behavior "apropiable" para usar en los modelos de objetos susceptibles de tener poseedor. El behavior podría utilizar los Callbacks adecuados para poblar los campos de propiedad con los datos del usuario actual y/o los grupos a los que pertenece.

Determinando los objetos accesibles

Con la información de ámbito obtenida al analizar los
roles
permisos
ejercidos por un usuario debería ser fácil establecer las condiciones que sirvan para filtrar los objetos a los que el usuario tiene acceso. Por ejemplo, el ámbito "Objetos propiedad del usuario" se puede traducir como "propietario_id = usuario.id" (creo que se me entiende).

Estas condiciones se pueden pasar a las acciones para que tenerlas en cuenta en la búsqueda de datos.

Esto nos llevaría a que el proceso de autorización tendría dos fases:
  • Determinar si el usuario puede realizar o no la acción que solicita.
  • En caso positivo, determinar sobre qué objetos puede realizarla, o bien si los objetos sobre los que solicita realizarla pertenecen al ámbito definido en el rol
    permiso
    .
  • En caso de que alguno de los dos puntos anteriores falle, comunicar al usuario que no tiene permisos y redirigirlo a un lugar adecuado.
Problemas del modelo conceptual

Hay algunos temas sin resolver en el modelo o que potencialmente pueden generar problemas.

El más importante tiene que ver con los conflictos de roles, esto es, si un usuario puede tener roles que definen permisos distintos o en distintos ámbitos para la misma acción. En particular si los permisos son contradictorios. Es necesario establecer un sistema que permita definir una precedencia.

Por ejemplo, un rol de usuario normal podría tener permisos para añadir, ver y editar sus propios registros pero no para borrarlos. Esto se podría definir de forma económica:

Permitir /registros/*
Denegar /registros/borrar

Teniendo un sistema de precedencia que determine correctamente que el segundo permiso sobreescribe al primeto. Aunque también podríamos definir el mismo rol, con más reglas:

Permitir /registros/agregar
Permitir /registros/ver
Permitir /registros/editar

Y una política que deniegue cualquier permiso no explícitado.

En fin. Lo dejo por ahora. Todavía tengo que darle un par de vueltas antes de poner esto en código. Es necesario también definir un API para interrogar al sistema, que adoptará la forma de un componente para añadir a los controladores que deban tener supervisión de permisos.

También hemos dicho hace un rato que hará falta un behavior que se encargue de registrar la propiedad de los objetos, así como tal vez algunos plugins que nos permitan manejar el sistema y definir reglas, roles y políticas.

lunes, 25 de junio de 2007

Acerca de la autentificación (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.


Después de varias semanas parece que va siendo hora de empezar a escribir código de verdad y no simples ejercicios y pruebas "a ver cómo va esto del Cake".

Por supuesto, para una aplicación seria tengo que buscar un sistema de autentificación y de gestión de permisos.

CakePHP 1.1 no incorpora ningún método propio de autentificación y sí un sistema de ACL (Access Control Lists) sobre la base de que:
  • No es difícil desarrollarlo usando el framework
  • Cada desarrollador tienen sus necesidades
CakePHP 1.2 tiene un componente Auth con muy buena pinta pero sin documentación. De hecho, todavía no he visto ningún tutorial que te permita entrar un poco en harina. Además, incorpora una nueva implementación de la ACL sobre base de datos que tiene una pinta excelente también, pero también poco documentada.

Aparte, existen unos cuantos sistemas de autentificación que son interesantes o tienen elementos interesantes:
El problema es que ninguno de ellos me acaba de convencer totalmente por una u otra razón.

Por lo pronto, habría que separar el sistema de autentificación del sistema de permisos. O al menos eso es lo que quiero hacer yo, y en alguno de los casos señalados hay cierta interacción entre ambos.

Quizá lo mejor sea coger lo que más me guste de cada uno y hacer algún refrito.

Autentificación

Lidiar con la autentificación supone intentar que:
  • Los usuarios del sistema con credenciales adecuadas puedan "entrar"
  • Evitar que usuarios maliciosos puedan lograr acceso o hacerse con contraseñas legítimas
Lo explica muy bien, Dieter Plaetnick en este artículo y en este otro. Plantea los problemas que hay que afrontar y resolver. Básicamente viene a decir:
  • Almacenar las contraseñas encriptadas para que no se puedan obtener en caso de acceso a la base de datos (cierto, si un atacante accede a la base de datos estás vendido de todos modos, pero Dieter se refiere, por ejemplo a los alojamientos compartidos, donde hay técnicos que podrían curiosear).
  • Yo añadiría que la aplicación no permita ver las contraseñas (ni siquiera los hash) a ningún usuario, ni siquiera administrador ni el mismo propietario. Si la contraseña se pierde, se cambia por otra, no es tan difícil.
  • Enviar las contraseñas encriptadas del navegador al servidor, bien sirviendo por https (http + ssl, de modo que toda la comunicación viaja encriptada) o bien encriptando con Javascript en el navegador.
  • Para dificultarlo más, utilizar una "sal" que convierta cada contraseña en única para la sesión. Esto es, que exista una especie de modificación de la contraseña para una sesión en particular, de modo que incluso en el supuesto caso de que un atacante intercepte la contraseña encriptada no pueda utilizarla en otra sesión. La cosa es generar una cadena (la sal) al iniciar una nueva sesión, incorporarla en el formulario (o en una cookie) del lado el cliente y en la sesión del para que se añada de algún modo a la contraseña (por ejemplo, encriptar la contraseña en el formulario y adjuntarle la "sal" y a lo mejor encriptar de nuevo). En el servidor, hacer algo parecido con la contraseña almacenada y comparar los hash resultantes.
  • Lidiar con los ataques de fuerza bruta ue prueban a gran velocidad miles de contraseñas hasta encontrar alguna válida. Una forma de resolver esto es limitando la cantidad de intentos que se pueden hacer en un tiempo determinado, estableciendo listas "negras" de IP bloqueadas.
  • Lidiar con la SQL injection, o sea, un intento de colar código SQL en los datos del formulario de login con el que se pueden conseguir cosas tan hermosas como login sin contraseña ni nada. Simplemente, un saneamiento de datos al recibir un formulario lo previene.
  • Yo añadiría establecer unas reglas de validación de las contraseñas que obliguen a los usuarios a elegir contraseñas sólidas, como por ejemplo, incluir caracteres en mayúscula, minúscula, números y algún signo de puntuación. Es molesto, pero más molesto es que te birlen las credenciales y hagan guarrindongadas con tu cuenta.
  • Otra historia es prevenir el posible "secuestro de sesión", es decir, que un usuario malicioso nos "sustituya" en medio de una sesión en la que ya estamos autentificados. Se puede hacer algo al respecto si guardamos suficiente información sobre la sesión actual, de modo que podamos detectar cosas como que haya cambiado la IP del cliente en plena sesión y cosas así.
Todo lo anterior es el planteamiento del problema. Luego hay que tener en cuenta todo el proceso de autentificación:
  • Si no hay usuarios conectados, presentar el login (o si el usuario activa un enlace para que le salga el login)
  • Mantener la información del usuario conectado para no pedirle el login cada dos por tres.
  • Si un usuario está conectado, que no le salga el login.
  • Si una acción está reservada a usuarios conectados, actuar en consecuencia. Es decir, que le pida el login si no está conectado ya o que no se lo pida si ya está dentro.
En cuanto a cómo montar esto en CakePHP parecen claras algunas cosas.

Debe haber un componente. Puesto que la acción se lleva a cabo en los controladores, es en éstos donde debemos introducir el código que se encarga de gestionar los usuarios conectados. La mejor forma es un componente.

Por otro lado, habría que definir modelos para usuario e intentos de login, (los grupos, en su momento). El modelo de usuario podría tener una información acerca del último login o del estado de conexión. O igual es mejor tenerlo en una tabla aparte. Algo así:

Users
id
username
password
email
login_desde (para controlar si agota el tiempo de sesión, hay que definir un tiempo de timeout)
remote_ip (para controlar si cambia en algún momento)

Intentos
id
remote_ip (para controlar la IP que está intentando acceder)
status (bloqueada, lista negra..., esto nos permitiría controlar que una determinada IP sea tratada de modo especial)

Tiene que haber, lógicamente controlador y modelos para administrar los usuarios y también alguna forma de revisar los intentos de conexión para poder hacer cosas como desconectar usuarios desde el sistem, o desbloquear/bloquear manualmente direcciones, mostrar información del usuario conectado.

Un helper para generar formularios de login con los scripts de encriptación tampoco vendría mal.

Sobre este sistema, se podría montar un sistema de perfiles que nos permita tener información y preferencias del usuario conectado, y de grupos para asignar permisos en bloque. Eso sí, el tema de permisos lo dejaré para otro rato. Por hoy ya me llega.

Alguna aclaración sobre este sitio

Antes de continuar "pasteleando" y en vista de que se empiezan a recibir visitas (gracias, de verdad), me gustaría dejar claros algunos asuntos.

Este sitio no pretende ser una referencia sobre CakePHP, sino más bien el diario de aprendizaje de un tipo que ha decidido portar sus aplicaciones web a este framework.

"Yo era un desgraciado..."

Después de varios años escribiendo aplicaciones PHP he acabado, como mucha gente, creando mi propio pseudo-framework, con resultados más o menos irregulares. Llegado un punto, el nivel de desestructuración y spaghetti alcanzó cotas poco manejables, lo que aconsejaba (más bien imponía) un cambio.

Decidido a empezar desde cero, comencé a tomar nota de las características y necesidades que tenía que cubrir el nuevo planteamiento. Mi idea era reescribir mi framework y me lo fui tomando con calma. Pero con el tiempo, me encontré con un cuaderno con un montón de notas que dejaban bastante claro por dónde quería tirar.

Llegué a la conclusión de que quería que mi framework se basase en MVC, que tuviese la suficiente inteligencia como para hacer algo parecido a ActiveRecord y scaffolding (aunque entonces no sabia ni que existía eso), que me permitiese más libertad con el HTML, que fuese más modular, más sencillo de escribir, que llevase bien las bases de datos relaciones, etc.

En algún momento, me hablaron de CakePHP y le eché un vistazo, pero aún no estaba yo lo bastante maduro como para caerme del guindo. Sin embargo, tomé nota de algunas de las ideas y las incorporé a mi lista.

Unas semanas después, una nueva conversación sobre el tema me decidió a mirar en serio CakePHP y encontré que todas las características (y más que ni sabia que necesitaba) estaban ya contempladas. Me leí el manual de cabo a rabo un par de veces, brujuleé por la Bakery y hasta eché un vistazo a un par de frameworks más.

Desde hace un mes y medio o así la decisión ya está tomada. Me siento cómodo con lo que CakePHP ofrece y a partir de ahora mis aplicaciones web van a migrar como está mandado.

El blog

El blog viene a cuento de lo anterior. No hay muchos recursos sobre CakePHP en castellano, y menos si has optado como es mi caso por la versión 1.2 (no tiene documentación oficial). Por otro lado, pensé que sería buena idea ir recogiendo aprendizajes, recursos y código en un sitio donde pudiese tenerlo todo a mano.

De ahí la idea del blog. Además, necesitaba la práctica de crearlo por otra razón y me venía bien trastear un sistema de blog más o menos popular (entre otras cosas porque aún es pronto para crear el mío encima de CakePHP...).

Por tanto, lo que se recoge aquí no es precisamente un manual, más bien es un diario de aprendizaje. El código, los ejemplos y las explicaciones pueden ser útiles, aunque si eres experto en Cake puede que sean más interesantes las aportaciones que puedas hacer en forma de comentarios que mis propios posts.

Acerca del código

El código que voy publicando puede no estar del todo completo pero he decido documentarlo lo más posible. Sobre todo por mí mismo, no veas el palo que resulta mirar una función que escribiste hace meses y ahora no saber ni qué parámetros se le pasan.

Suele pasar que cuando pongo un post aquí, se produce una reordenación de los bits en Internet relacionados con el tema y, de repente, aparecen varios artículos excelentes sobre el mismo, enseñando técnicas más simples, elegantes y eficaces, que el día anterior "no estaban" (más bien yo no supe encontrarlas). Por eso, a veces cambio mucho el código, incluso varias veces al día.

En fin, como puedes ver, se trata de un "blog de notas", más que otra cosa.

lunes, 18 de junio de 2007

Manipulacion de arrays

Un artículo en ThinkingPHP sobre cómo se manipulan arrays usando la clase set, que resulta ser una herramienta estupenda para extraer información de arrays complejos (como los resultados de FindAll y así).

La clase Set tiene una variedad de métodos que voy a ponerme a explorar en cuanto pueda. Ya contaré.

domingo, 17 de junio de 2007

Componente para mantener la información de paginado

Después de darle como muchas vueltas se me ha ocurrido escribir un componente para gestionar la información de paginado entre vistas. La idea es la siguiente:

Supongamos que estoy en una típica vista index que muestra una tabla de registros paginada y que me encuentro en la página "p" de la misma.

Entonces voy y pulso un botón o enlace para editar un registro cualquiera de esa página. Al terminar de editar ¿a dónde vuelvo? En principio a la vista de index (si lo he especificado así), pero ¿a qué página?.

Si no especifico la página de origen normalmente volveré a la página 1 y no a la "p" que es en la que estaba, lo cual es un engorro, por muchas razones que te puedes suponer.

El componente paging que he escrito básicamente permite a la acción index "acordarse" de qué página estaba mostrando la última vez.

Instrucciones de uso

  1. Copiar el código al archivo /app/controllers/components/paging.php
  2. En el controller, añadir Paging a la lista de components
  3. En la acción en que queremos usarlo, llamar al método $this->Paging->manage()
  4. Opcionalmente puedes pasarle el nombre del modelo al que debe referirse la información de paginación


Por si no queda claro, la cosa es así


class MiControlador extends AppController {

var $components = array ('Paging');

function index () {
$this->Paging->manage();
}

}


Cómo funciona

Básicamente, el componente comprueba si hay información de paginación almacenada en la sesión y la lee si es así.

Seguidamente, comprueba si hay información en la URL. Esta información tiene prioridad porque Controller::paginate() la utiliza si está disponible para definir los límites de la búsqueda.

Finalmente, el componente combina la información procedente de todas las fuentes para determinar el nuevo contenido de la variable Controller::paginate y también lo guarda en la sesión.

La información se guarda de forma específica para cada controlador.

El código

He puesto el código en CakeBin, pero como no sé si durará, intento copiarlo aquí:

<?php
/**
* Component to manage paging info between reloads
*
* Created by Fran Iglesias on 2007-06-17.
* @version: 0.1
* Copyright (c) 2007 Fran Iglesias. All rights reserved.
**/

/**
* Usage:
*
* 1. Put paging.php into your /app/controllers/components folder
* 2. Add 'Paging' to your controller::components array
* 3. Call $this->Paging->manage () into the action that retrieves data using paginate()
*/

class PagingComponent extends Object {
var $components = array ('Session');
var $controller = false;

function startup (&$controller) {
$this->controller = $controller;
}

/**
* Manage pagination data retrieving and storing into session as needed,
* Paging info passed via URL arguments takes precedence over session data
*
* @params string $forModel Specify a model name to manages its paging data
* @return void
* @author Frankie
**/
function manage ($forModel = null) {
// Determines a model name to work
if (in_array ($forModel, $this->controller->modelNames)) {
$model = $forModel;
} else {
$model = $this->controller->modelNames[0];
}
// Check if there is paging info in the Session and read it
$sessionVarName = $this->controller->name . '_paging_info';
if ($this->Session->check ($sessionVarName)) {
$pagingInfoFromSession = $this->Session->read ($sessionVarName);
} else {
$pagingInfoFromSession = array ();
}
// Check if there is paging info in passedArgs and build a paginate-like record with it
$pagingArgs = $this->_getPagingFromArgs ();
// Set paging info based on paging info from session and form arguments
if (isset ($pagingInfoFromSession[$model])) {
$paging = am($pagingInfoFromSession[$model], $pagingArgs);
} else {
$paging = $pagingArgs;
}
// Pass the new paging info to controller->paginate variable
if (isset ($this->controller->paginate[$model])) {
$this->controller->paginate[$model] = am($this->controller->paginate[$model], $paging);
} else {
$this->controller->paginate[$model] = am ($this->controller->paginate, $paging);
}
// Store new paging info into Session
$this->Session->write ($sessionVarName, $this->controller->paginate);
return true;
}

/**
* Helper to read pagination info from URl if there is any
*
* @return void
* @author Frankie
**/

function _getPagingFromArgs () {
if (!isset ($this->controller->passedArgs)) {
return array ();
}
$paginate = array ();
if (isset ($this->controller->passedArgs['page'])) {
$paginate['page'] = $this->controller->passedArgs['page'];
}
if (isset ($this->controller->passedArgs['limit'])) {
$paginate['limit'] = $this->controller->passedArgs['limit'];
}
if (isset ($this->controller->passedArgs['sort']) && isset ($this->controller->passedArgs['direction'])) {
$paginate['order'][$this->controller->passedArgs['sort']] = $this->controller->passedArgs['direction'];
}
return $paginate;
}
}

?>

viernes, 15 de junio de 2007

Anatomía de una acción

Uno de los conceptos básicos que hay que entender cuando se empieza con CakePHP es el de cómo "funcionan" las acciones en los controladores. Como novato que soy tropiezo con esta piedra varias veces al día, por lo que me siento muy autorizado a exponer algunas ideas sobre el particular.

Cuando pedimos una URL de una aplicación CakePHP, en el clásico formato /controlador/acción, es el controlador el que la recibe, prepara lo necesario y ejecuta la acción.

La fase de preparación consiste en "averiguar" lo que ha pasado ahí fuera, en el mundo de los usuarios, y poner esa información a disposición de la acción de una manera estructurada.

Básicamente, lo que el controlador mira son dos cosas:
  • Los datos que hayan sido recogidos en un formulario y enviados mediante POST (normalmente). Los encontrarás en Controller::data
  • Los argumentos que hayan sido pasados por la URL. Los encontrarás en el array Controllers::passedArgs
Aparte de estos datos básicos, puedes encontrar mucha información importante en Controller::params, o detalles sobre la petición si usas RequestHandler. Pero para lo básico vamos a quedarnos sólo con los argumentos y los datos de formulario.

Datos de formulario

Si el usuario ha rellenado un formulario y apretado el botón Submit, Controller::data será poblado por los datos de Post. Nuestra acción tendrá entonces que detectar esta situación y abrir dos "modos" de trabajo según haya datos para procesar el formulario o no.

Habitualmente, si no hay datos la acción tendrá que mostrar el formulario adecuado.

Lo anterior establece algo así como dos "modos" de la acción: uno de postproceso de formulario y otro de preparación del mismo.

Argumentos pasados

Dentro de cada uno de los modos anteriores, los argumentos pasados por URL nos sirven para modular el comportamiento concreto de la acción. Por ejemplo, en el típico caso de una acción edit, normalmente pasamos un argumento con el id del registro que queremos editar. Por lo tanto, nuestra lógica será:
  • Si tenemos datos del formulario se trata de una actualización y hay que tratar de guardar los datos nuevos en el registro con el id indicado.
  • Si no tenemos datos del formulario, se trata de cargar el registro cuyo id se nos ha pasado y poner un formulario ya cubierto con esos datos para que el usuario los modifique.
CakePHP es lo bastante inteligente como para no obligarnos a leer el array Controller::passedArgs cuando ciertos argumentos sean específicos para la acción. Es el típico caso del id en acciones tipo edit o view.

Lo que hace es mapear estos argumentos de la URL en los argumentos que hayamos definido para nuestra acción. En cierto modo, esto nos define la estructura de la URL. Por ejemplo:

function accion ($id, $title= false)

espera una URL de la forma:

/controlador/accion/12/ejemplo
/controlador/accion/15

El argumento id sería obligatorio y title opcional.

Los argumentos que no sean específicos podemos leerlos en Controller::passedArgs, que es un alias a Controller::params[pass] (y a Controller::passed_Args, por cierto).

Una url con argumentos tiene la forma

/controller/accion/12/pagina:5/ordenar:id/

En este caso 12, seria pasado como argumento a la acción "accion", mientras que encontraríamos pagina y ordenar en el array Controller::passedArgs.

Por supuesto, hay muchas variedades de acciones. No todas ellas tienen que lidiar con formularios, algunas solo van a presentar un contenido estático, etc. Pero básicamente todas siguen un mismo esquema:
  • Compobar si hay información procedente de ciertas fuentes (argumentos, formulario)
  • Si hay información, ver qué tengo que hacer con ella y presentarla al usuario
  • Si no hay información, presentar una vista que la solicite al usuario

Entendiendo las relaciones en CakePHP

Un artículo de Josh Benner muy clarito, aunque en inglés, sobre las asociaciones en CakePHP.

jueves, 14 de junio de 2007

Una nota breve sobre getid3

Pues nada, que estoy recopilando material y referencias para montar mi primera aplicación real sobre CakePHP (o al menos parte de ella).

Quiero hacer un plugin para ofrecer un servicio de podcast. He estado buscando la forma de poder leer información de los archivos mp3, m4a y otros candidatos con PHP y me he encontrado esta clase: getid3.

Lo bueno es que reconoce muchos tipos de archivos, por lo que será capaz sacar la información necesaria para crear el feed del podcast.

Lo malo (o lo bueno según se mire) es que saca decenas de datos, lo que hace un poco complicado sacarle provecho.

Me imagino que trataré de hacer algún tipo de wrapper que simplifique la interacción con la clase y tendré que ver la manera de "acoplarla" en Cake.

Respecto al plugin en sí, a ver si me pongo con ello en los próximos días.

miércoles, 13 de junio de 2007

Buscando página para un registro nuevo

Llevo un rato buscando una estrategia para que al añadir un registro la tabla (paginada) donde se presentan no sólo se actulice, sino que muestre el nuevo registro para proporcionar feedback adecuado al usuario.

Se me ha ocurrido una solución un poco bruta, que por supuesto necesita ser pulida para mantener la consistencia de la aplicación web. Por lo menos funciona.

Simplemente se trata de reordenar la tabla por id descendente justo después de Model::save mediante la especificación de la variable Controller::paginate.

array ('order' => 'modelo.id', 'page' => '1')

Claro que hay que tener muchas cosas a tener en cuenta. Por un lado están otros valores anteriores de la variable. Esto es algo que tal vez se pueda solucionar mediante un array_merge, tal que así:

$this->paginate = array_merge ($this->paginate, array ('order' => 'modelo.id', 'page' => '1'))

En realidad esto no se conserva entre recargas de página, por lo que sería adecuado guardar esta información en la sesión y recuperarla antes de generar la vista..

Ejercicio para el lector

Esto me lleva a pensar que se podría crear un método beforeFilter o beforeRender (no sé exactamente) que se encargase de lidiar con el tema de conservar las páginas actuales de las vistas index, tener en cuenta algunas opciones por defecto para las aplicaciones (como por ejemplo que hubiese una preferencia de usuario o de la aplicación sobre la cantidad de items que se muestran o la ordenación por defecto o lo que sea), y esas cosas. Más o menos tendría que lidiar con:
  • Opciones por defecto "hardcoded"
  • Opciones por defecto de la aplicación, tal vez fijadas en un archivo de configuración u otro almacenamiento.
  • Opciones del perfil de usuario, si procede.
  • Opciones de la sesión de usuario, si procede.
  • Información de página pasada en la sesión, teniendo en cuenta a qué Modelo hace referencia.
Si es lo suficientemente generalizable, se podría hacer un componente.

Más cornás da el Ajax (¿o era el hambre...?) (Muy actualizado)

Llevo todo el día dándome cabezazos contra una tontería que al final no tiene que ver con Ajax (o eso creo).

Resulta que en mi experimento de formulario + tabla, el formulario se empeñaba en mantener lo escrito con una constancia digna de mayor causa. Eso no es malo, lo malo es que se mantenía incluso cuando no lo necesitaba, o sea, cuando los datos habían sido enviados, validados y guardados. Se supone que $('formulario').reset() (una función de prototype) tendría que hacer el trabajo. Pero, ¡que si quieres arroz Catalina!

Así que me he pasado horas intentando entender ¿por qué? sin que haya conseguido una respuesta clarificadora. En algún lugar encontré una referencia a la necesidad de asegurarse de mantener el contenido tecleado en un formulario incluso entre refrescos de páginas. Es de agradecer que Cake? Ajax? (no lo sé todavía?) lo mantengan, pero no había conseguido vaciar los campos del formulario recorriendo a Ajax o Javascript.

[... Mala solución borrada por el autor ...]

¡Borra Controller::data, idiota!

La solución final viene por vía de Cake y gracias a Geoff Ford en el grupo Google de CakePHP, así que ¡gracias Geoff!

La solución, por supuesto, es trivial:

Cuando enviamos un formulario cubierto pulsando el botón Submit correspondiente, CakePHP puebla la variable Controller::data (el $this->data que le pasamos a Model::save) con los datos procedentes de POST. Esto es lo normal.

Pero resulta que yo estoy pidiendo que se vuelva a mostrar el formulario justo en el mismo ciclo que cuando salvamos los datos, por lo tanto, Controller::data sigue llevando los datos de POST y, en consecuencia, cuando se dibuja la vista ahí siguen los datos.

Tan sólo hay que vaciar Controller::data para que no se pase ningún valor al formulario. Tan simple como eso.

Dos cosas más
  1. Controller::data es el mecanismo que usa Cake para que no se pierdan los datos existentes en el formulario si éstos no validan y tiene que redibujar el formulario en el mismo ciclo.
  2. Hay persistencia de los datos en el formulario incluso recargando la página. Eso es muy bueno.
  3. En el planteamiento "típico" en que el formulario de añadir datos se retira de la vista al enviarlo, los datos del Post se pierden una vez utilizados. Al volver a llamar al formulario, este se muestra vacío... porque estamos en otro ciclo.
  4. Ajax no tenía nada que ver en esto.

martes, 12 de junio de 2007

Organizando las vistas para Ajax

Siguiendo con el ejercicio anterior he descubierto unas cuantas cosas más. Por ejemplo, los problemas que supone la validación CakePHP interaccionando con un planteamiento "ajaxiano" de la vista.

La solución que he encontrado es la siguiente:

De lo general a lo particular

Lo primero sería hacer un croquis general de lo que voy a necesitar poner en la vista. En mi caso, por ejemplo, un título, un formulario y una tabla. Hay que tener en cuenta las partes que se van a tener que actualizar solas, en un momento dado, o las que habrá que actualizar globalmente.

Lo mejor parece ser descomponer la vista en partes, valorando si necesitan ser incluidas en una div, y crear elements para generar cada una de ellas. De este modo dispondremos de piezas de construcción para generar las vistas definitivas. Procura ponerles nombres bien descriptivos a los elementos.

Preparando las vistas

Con los elements ya preparados generamos las vistas que nos haga falta, aprovechando el método This::Element ('elemento').

La idea es crear tantas vistas como podamos necesitar. Intentaré explicarlo:

Normalmente la primera petición para cierta página será una petición normal, sin Ajax ni cosas raras. Entonces tendremos que tener una vista que muestre todo lo necesario. En mi ejemplo, una vista que muestre el título, el formulario y la tabla. Esa podría ser index.ctp, si mi acción es index.

Como mi tabla tiene enlaces Ajax para ordenar los resultados y éstos piden que se actualice sólo la div que contiene la tabla, necesitaré una vista que sólo muestre la tabla. La llamaré tabla.ctp.

El formulario envía una petición Ajax al servidor para añadir entradas sin actualizar toda la página. El resultado del formulario debería actualizar la tabla, pero puede ocurrir que tenga que actualizarse también el propio formulario, por ejemplo si hay errores en la validación en CakePHP. En este caso, podría crear una nueva plantilla, aunque veo que index.ctp me sirve para esto sin tener que duplicar el trabajo.

También podría preparar una vista específica para cuando se solicita la acción con una petición no Ajax.

Y luego están las vistas para las demás acciones que programemos, por supuesto. En ellas hay que considerar los mismos aspectos acerca de si es posible que sean solicitadas a través de Ajax y qué contenido deben proporcionar.

En el controlador

Ahora bien, en el controlador tenemos que determinar qué tipo de petición está entrando y qué contenido tenemos que devolver. Tenemos dos armas:

RequestHandler->isAjax() es el método que nos permite saber si la petición es Ajax y hacer cosas diferentes en cada caso.

Controller::Render ('vista') nos permite especificar una vista concreta con la que mostrar el resultado de la acción.

Usándolas en combinación, podemos hacer que nuestra acción sepa qué contenido debe entregar a la petición Ajax (o de cualquier otro tipo) y mediante qué vista.

No te olvides de generar el contenido

Eso sí, hay que tener presente qué contenido es el que vamos a actualizar, porque las cosas pueden ser un poco diferentes que en el uso normal.

Por ejemplo, en este caso de la combinación de formulario de entrada más tabla de datos, he decidido que al añadir una entrada hay que actualizar la vista que contiene tanto el formulario como la tabla. Por lo tanto, la acción add, cuando es llamada mediante Ajax, tiene que lidiar no sólo con tomar los datos y hacer lo que haga falta con ellos, sino también obtener el listado para mostrar como si fuera una acción index y enviárselos a la vista index, en vez de a la vista por defecto (add).

Alternativamente, la versión "no Ajax" de la acción podría usar una vista diferente y necesitar información diferente.

Otros beneficios

Algunas de las ideas expuestas aquí, aparte de ser útiles e incluso necesarias para integrar Ajax, tienen algunos beneficios colaterales.

Al descomponer una vista, o una familia de vistas, en elementos reutilizables es más fácil, por ejemplo tener un único formulario para añadir y editar. Estamos siguiendo eso que llaman la metodología DRY (Don't Repeat Yourself).

Otro beneficio es que si una parte de una vista es relativamente complicada, al empaquetarla en un element podemos mantener controlada esa complejidad.

lunes, 11 de junio de 2007

Ajax... qué lío

Acabo de hacer algo intencionadamente con Ajax. Quiero decir que no es como el PaginatorHelper, que te lo hace todo. O sea: que me he planteado un pequeño ejercicio con Ajax y me ha salido.

Eso sí, con cierto esfuerzo. Voy a intentar poner aqui las ideas que han ido surgiendo, aunque no creo que ponga código, al menos de momento. Entre otras cosas, porque no está conseguido todo lo necesario para considerarlo completo.

El ejercicio

Consistía en combinar un formulario para añadir entradas en un tabla (citas o frases célebres y su autor) con la propia tabla. Al escribir una nueva frase en el formulario y pulsar el botón de enviar tendría que actualizarse la tabla.

La función de las técnicas Ajax aquí es enviar los datos del formulario al servidor, elicitar la acción add correspondiente en el controlador y recoger el nuevo contenido de la tabla

Suena bien. Hacerlo tiene truco.

Lo primero es planificar un poco la página que se va a generar y ver cuántas zonas (div) necesitamos establecer. En principio, aquí debería llegarnos con una para el formulario y otra para la tabla.

Luego habría que tener vistas para generar el contenido de cada una de estas partes, más una vista que las combine todas. Las vistas de partes serían para actualizar cada div cuando toque, la vista combinada sería para usar la primera vez.

Esta es una parte que no tengo del todo clara cómo organizar. El tutorial que he seguido está basado en la versión 1.1 de CakePHP y algunas cosas no son exactamente iguales.

Por una parte, es posible utilizar Controller::render($vista, 'ajax') para explicitar la vista que queremos mostrar usando el modo "Ajax". Combinando eso con una consulta Controller::ResquestHandler->isAjax () para ver si la petición nos la hace Ajax, podemos hacer que un mismo controlador extraiga los datos necesarios y decida si debe devolver la vista "normal" o la actualización "ajax". No sé si me explico.

Ejemplo. La acción index podría tener una view asociada, llamada también index, que genere tanto el formulario como la tabla de datos (¿me sigues?) y que usaría ante una petición normal. Por otro lado, podríamos escribir una view para actualizar sólo la parte de la tabla y que se usaría para atender una petición Ajax.

Nos quedaría algo así:


function index () {
$data = $this->Paginate ();
$this->set ('frases', $data);
if ($this->RequestHandler->isAjax()) {
$this->render ('tabla');
}
}
No sé si se aprecia bien. El último if nos permite indicar una view específica que genera el contenido que queremos devolver.

Recuerda que tenemos que usar el component RequestHandler y llamar a su método startup en beforeFilter para que funcionen bien estas peticiones Ajax (de otro modo, se empeña en devolverlas con layout y esto es un despiporre).

Las acciones implicadas tienen que saber qué actualizan

Esto me hizo rascarme la cabeza un montón hasta que cai de la burra. Resulta que en mi ejercicio todo funcionaba bien hasta que añadía una entrada a la tabla y trataba de ordenar ésta. Bueno, pues se añadían más entradas, y se generaba un error del layout y no salía la tabla.

Bien. Resulta que el boton de enviar el formulario llamaba a la acción add del controlador. Correcto. Pero la acción add, aparte de añadir el nuevo registro, tiene que volver a pedir los datos para mostrar la tabla. Eso por un lado.

(Nota: lo que no he mirado es si sería más adecuado y económico llamar a la acción index...).

Por otro lado, hay que controlar que la acción add dibuje la vista que genera el contenido de la tabla (en el ejemplo de arriba, es tabla.ctp, en lugar de su add.ctp que dibujaría normalmente), al menos cuando es llamada por Ajax. Si no es llamada por Ajax debería dibujar su vista normal, o hacer algo que le explique al usuario qué pasa o qué puede hacer.

Y, por último...

Más cosillas a las que prestar atención

Al generar enlaces o botones que hagan llamadas Ajax hay que asegurarse de que llaman a la acción adecuada del controlador adecuado. Es posible que tengas que especificar explícitamente la acción y no dejar que sea CakePHP el encargado.

Esto es así porque si dibujamos una vista desde una acción que no es la "propia" (como en el apartado anterior que dibujábamos tabla desde index, pero también desde add, Cake interpreta que esos enlaces se dirigen a la acción "generadora", si no hemos indicado otra.

En mi ejemplo, debo especificar en las opciones del paginador de la tabla, que la acción a la que llaman los enlaces Ajax que ordenan la tabla ha de ser siempre index.

Ya puestos, vamos con Ajax

Un artículo recopila enlaces sobre Ajax y CakePHP. Ya contaré cómo me va.

Cache de elementos

Un tutorial de Tane Piper sobre cache de elementos. Hasta yo lo he entendido. Este otro también explica el uso de la clase cache, para cacheado específico que quieras hacer.

(Lo que no acabo de ver que funcione es el cache de vistas del que habla el manual.)

La cosa es sencilla: cuando quieres incluir un elemento y cachearlo para no volver a pedir los datos, añade un array con la clave 'cache' y el tiempo en segundos (o en un formato apto para strtotime).

echo $this->element ('menu', array ('cache' => '1 day'));

Eso sí, para asegurarte de que el elemento muestra la información actualizada, tendrías que borrar esta cache cuando se haga alguna modificación de los datos implicados.

El artículo propone este método en el afterSave del modelo, implicado (aunque también debería ir en el afterDelete, creo:

@unlink (CACHE.'views'.DS.'element__nombre_del_elemento');

El ahorro de peticiones a la base de datos puede ser brutal.

Una de Axaj: haciendo que los Ajax Request no recarguen todo

CakePHP incluye un Helper para Ajax y cosas como el PaginatorHelper hacen uso de Ajax.

Yo no es que tenga mucha idea del asunto, pero al hacer mis primeros experimentos de paginación me encontré con el problema de que en vez de actualizarse los listados, se me actualizaba toda la página en el espacio del listado.

La solución es básicamente decirle al controlador que cuando reciba una petición desde Ajax que genera la vista teniendo eso en cuenta. Después de reinventar la rueda varias veces, resulta que lo único que hay que hacer es añadir esto a los controladores que tengan vistas que usan Ajax:


var $components = array ('RequestHandler');

function beforeFilter () {
$this->RequestHandler->startup ($this);
}


Y, si aún así, necesitas controlar peticiones Ajax en algún punto del controlador o en alguna acción, no tienes más que hacer un simple:


if ($this->RequestHandler->isAjax ()) {
// Es Ajax
} else {
// Es otra cosa
}

Crear un feed RSS

Me llevó todo el domingo intentar desentrañar el pequeño misterio de generar feeds RSS con CakePHP a partir de este artículo de Nate, que lo explica, pero no demasiado.

La gracia es que acabé consiguiéndolo a medias: el feed salía pero no podía controlarlo a mi gusto (¿dónde c... se pone el channel?). Más tarde (a punto de ponerme a dormir) me di cuenta de que había pasado por alto ver cómo funcionaba la plantilla por defecto para rss, la cual tendría todas las respuestas. Efectivamente. Lo malo es que, acto seguido, voy y me encuentro este breve tutorial de Jiri Kupiainen donde explica todo el proceso. :-(

Ocurrió lo de siempre, te pegas contra la documentación, el código, el API... descubres como se hace algo, y una vez que lo tienes aparecen como por encanto artículos en Google que "no estaban ayer". Lo bueno, es que el aprendizaje por la vía dura suele ser bastante eficaz.

En fin, el caso es que basándome en ambas fuentes, he aquí mi propio tutorial para generar feeds rss... ¡a partir de cualquier modelo!

Paso 1: preparar Cake

Básicamente hay que decirle a CakePHP que queremos parsear url con extensiones, de la forma: /controller/action.rss. Esto es así porque la versión 1.2 de CakePHP puede seleccionar al vuelo diferentes vistas en función de la extensión que le pasemos. Por lo tanto, en un paso posterior crearemos una vista específica para el feed y la pondremos en el sitio adecuado para que la use Cake.

En tu /app/config/routes.php tienes que añadir:

Router::parseExtensions('rss');


Esto le indica a Cake que si se encuentra una URL con la extensión 'rss' busque las vistas en la carpeta /vistas/modelos/rss.

Podemos no especificar ninguna extensión, para que parsee cualquiera, o bien dar una lista limitada de ellas 'rss', 'xml'...

Paso 2: cosas para hacer en el Controller

Hay varias, así que vayamos por partes.

Lo primero es habilitar el uso del componente RequestHandler en el controlador. Aparte de para este tema de los feeds es útil siempre que necesites obtener información sobre la petición que está recibiendo el controlador. Lo segundo es indicar que quieres utilizar el RSSHelper, claro.

var $components = array('RequestHandler');
var $helpers = array ('RSSHelper');


Una cosa opcional: El componente HandleRequest te permite comprobar si el usuario está pidiendo un RSS (u otra cosa) y actuar en consecuencia. Por ejemplo, si es para el feed obtener sólo los últimos 15 items que cumplan ciertas condiciones y si no obtener todos. En cualquier caso, usarías:

if ($this->RequestHandler->prefers('rss')) {
// Cosas para hacer si es rss
} else {
// Cosas para hacer si no lo es
}


Lo segundo es enviarle los datos a la vista del feed (que aún no hemos creado, ¡ojo!). Lo puedes hacer así:


Configure::write('debug', '0');
$data = $this->Paginate ();
$this->set('channel', array ('title' => 'Ejemplo',
'link' => 'http://localhost:8888/micake/',
'description' => 'Frases y citas célebres para que las puedas leer'
));
$this->set ('frases', $data);


Lo explico un poco:

La primera línea es para desactivar el Debug (en el caso de que lo tengas distinto de 0, como ocurre en un entorno de pruebas o de desarrollo). Con Debug, Cake añade cosas a la salida generada y los feeds no validarían de ninguna forma aunque estuviesen bien construidos. Opcionalmente puedes poner un if para controlar si es necesario hacerlo o no.

La segunda línea es para recabar los datos de la manera habitual.

La tercera línea ajusta los valores para el channel. El layout por defecto para feeds rss espera que definas una variable $channel como un array con claves que serán los elementos del channel (como title, link, description y los demás). Si no la defines, CakePHP se busca la vida para poner algunos.

La cuarta línea pasa el array de datos que serán los ítems del modelo. En este caso, la variable es $frases, pero se supone que esto ya sabías hacerlo. Aquí no vamos a hacer nada más.

Paso 3: preparando la vista, o cómo pasar tu modelo al feed

La vista tienes que ponerla en /views/nombre_del_modelo_plural/rss/nombre_de_la_action.ctp

Y el contenido tiene que ser más o menos así:


$items = $rss->items($frases, 'convertirRSS');

echo $items;
function convertirRSS($data) {
return array(
'title' => $data['Frasecita']['frasecita'],
'link' => array('action' => 'view', $data['Frasecita']['id']),
'guid' => array('action' => 'view', $data['Frasecita']['id']),
'description' => $data['Frasecita']['frasecita'],
'author' => $data['Frasecita']['autor'],
'pubDate' => $data['Frasecita']['usado']
);
}


La explicación es como sigue:

Usamos el método rssHelper::items () para convertir el array de datos en elementos items del feed. Para convertir los campos del modelo en los items con sus correspondientes subelementos, usamos una función callback que, en este caso, hemos llamado convertirRSS.

La función tiene que tomar un array asociativo simple, cuyas claves son los nombres de los campos del modelo (y cuyos valores serán los de los diferentes registros, pero de eso se encarga el método items).

La función, por otra parte, tiene que devolver un array cuyas claves sean elementos válidos de item. Tarea nuestra es decidir cómo movemos los datos entre el modelo y los elementos del item.

Algunas observaciones interesantes en el ejemplo. Link y Guid piden url y entienden que se las pasemos como arrays, tal como se explica en el artículo sobre el router.

PubDate, admite campos de tiempo y los prepara en el formato adecuado.

Aparte, mirando el API, creo que hay soporte específico para enclosures, o sea, que el camino para feeds de podcasts está abierto. Aún no lo he investigado.

Paso 4: Ya está, ¿cómo suscribirse?

Pues sí, el feed ya está listo para servir. La URL para suscribirse sería:

http://exemple.com/controller/action.rss

Sustituye lo que haga falta. Supongo que se podrían hacer rutas para que ciertas url se sirvan como feeds.

domingo, 10 de junio de 2007

El router de la 1.2 (atualizado): indicar URL como arrays

He estado leyendo un artículo de Andy Dawson sobre el router de CakePHP 1.2.

El router, entre otras cosas, es el que se encarga de gestionar las URL de Cake, permitiéndole intrepretar sus características /controlador/accion/[parametros]. Pero también, desde la versión 1.2, se puede aprovechar para generar esas mismas URL desde el código de manera inteligente, sin tener que pensar dónde estoy exactamente.

Para eso usamos un array, que en su forma completamente desarrollada viene siendo así:

$url = array (
CAKE_ADMIN=>false,
'plugin'=>false,
'controller'=>'Post',
'action'=>'view',
)


Pero que nosotros podríamos usar así:

$url = array {
'action' = 'edit',
12
}

Que sería el equivalente de

$url = '/controller/edit/12';

Y que podríamos emplear en un $html->link (), por ejemplo, o en otros métodos que necesiten una URL.


Ventajas

La principal, que hacemos que sea Cake quien se encargue de formar correctamente las URL.

Por otro lado, el código se hace más transportable y genérico. Ideal para helpers.

En consecuencia, he empezado a migrar mis URL internas al formato array.

Ojo con requestAction

Por lo que veo, requestAction no soporta las url en forma de array. Así no me funcionan a mí los elements.

sábado, 9 de junio de 2007

¿Lo quiere por id o por nombre? (actualizado)

Este pequeño trozo de código permite que una acción pueda localizar un mismo modelo de dos maneras distintas, gracias a los métodos mágicos de búsqueda de Cake (Model::findBy...).
if (is_numeric ($id)) {
// Buscar por ID
$this->MenuBar->id = $id;
$resultado = $this->MenuBar->Find ();
} else {
$resultado = $this->MenuBar->FindByMenuBar ($id);
}
Explaneishon

Capturamos el parámetro id de una URL típica como /controller/action/parametro.
Vemos si es de tipo numérico, en cuyo caso debería ser un ID.
Y si no, será que es el nombre.

Explaneishon 2

Los métodos mágicos Model::findByCampo () nos sirven para hacer búsquedas de modelos por un campo específico (Campo) del modelo. Dispones de un método findBy por cada campo que tenga el modelo.