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
?>