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>