viernes, 28 de febrero de 2014

Añadir capacidad de log a un objeto

Supongamos que tienes una clase o una biblioteca en un proyecto de CakePHP pero que no desciende de un objeto CakePHP y quieres que sea capaz de escribir en los archivos de log.
Pues basta con esta línea de código:

ClassRegistry::init('Object')->log('el mensaje', 'el archivo de log');

Alternativamente, puedes extender la nueva clase a partir de la clase Object de CakePHP, o de una descendiente de ella.

lunes, 22 de julio de 2013

Dos idiomas en una misma vista

¿Podemos usar dos idiomas en la misma vista con las funciones de internacionalización de CakePHP?

Pues se puede, aunque no es un método del todo limpio.

Se trata de escribir la variable de sesión 'Config.language' con el idioma que queramos usar. Por ejemplo:

$_SESSION['Config']['language'] = 'spa';

En principio, se podría recuperar el que esté actualmente en uso, leyendo la variable de sesión, y guardarlo temporalmente para restaurarlo al terminar de usar el otro.

jueves, 6 de junio de 2013

Excepciones (actualizado)

Artículos para empezar con el tema de las Excepciones:

Buenas prácticas con Excepciones en PHP 5. Texto de Ralph Schlinder sobre el uso de las Excepciones anidadas y el de las Excepciones de la SPL para lograr la mayor expresividad y control.

Usando excepciones para simplificar la lógica de los controladores Texto de Mark Story sobre el uso de las excepciones en los Modelos y cómo puede ayudar a escribir controladores más simples.

Throwing exceptions Un artículo de J. Gauffin sobre cuando un método debería lanzar excepciones y cuándo devolver null o false. Pista: si esperas un resultado y no se produce, lanza una excepción (por ejemplo, Model->read()), Por el contrario, si un resultado posible es que no haya resultados, devuelve false o null (por ejemplo, Model->find()).


miércoles, 27 de febrero de 2013

Algunas notas para tener en cuenta en Test Unitarios

Evitar errores definiendo Fixtures con algunos tipos de campos

Por alguna razón que ignoro, al definir los campos (var $fields) en fixtures con los tipos text, date o datetime (sospecho que también binary) es mejor no usar el array completo para definir todos los parámetros, sino únicamente poner el tipo de campo. De otro modo, CakePHP se empeña en crear una definición de tabla SQL que no es válida y el test falla. Al dejar que lo haga de manera automática va perfecto.

Por ejemplo:

var $fields = array(
    'content' => 'text',
    'created' => 'date'
);

Tests y localización

Añade la instrucción

Configure::write('Config.language', 'eng');

al principio del test para evitar que CakePHP localice las cadenas de texto con __() y similares. De este modo, si un resultado del test depende de una cadena localizada no la traducirá (asumiendo que uses eng como idioma por defecto, claro).


sábado, 28 de abril de 2012

Crear un RSS con CakePHP 1.3

Hace tiempo escribí un tutorial sobre cómo generar feeds RSS en CakePHP pero ya está muy desactualizado. Así que ahí va otro un poco más moderno, que vale para la versión 1.3. Tampoco es que esté muy actualizado, ya que aún no me he planteado migrar a CakePHP 2, pero bueno, espero que  pueda ser útil.

Además, como bonus, intentaré explicar cómo voy resolviendo el tema de que el feed sea validado en el feed validator del W3C, cosa que tiene su dificultad ya que la especificación RSS es un tanto laxa en algunos aspectos.


Paso 1: preparar la aplicación Cake para responder a las peticiones de feeds

Básicamente hay que decirle a CakePHP que queremos parsear urls 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 /views/models/rss. De este modo, puedes tener una misma acción que responde de manera diferente a una petición html o a una petición de rss.

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

Otra ventaja es que CakePHP seleccionará automáticamente un layout específico para rss, que podemos sobreescribir en /views/layouts/rss/default.ctp, o crear otros alternativos con otros nombres. En ese último caso hay que recordar poner en la acción del controlador un paso $this->layout = 'nombre del layout alternativo'; para que la aplicación sepa cuál queremos usar.

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 ('RSS');

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('channelData', 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 $channelData 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. Sin embargo, puede que en la vista tengamos que hacer algunos ajustes a esta información antes de enviarla al Layout.

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 los datos

Los feeds rss tienen una estructura relativamente sencilla, puedes consultar la especificación para tener todos los detalles. Cada uno lleva la información de un canal (que puede ser un blog), el cual puede contener varios ítems (cada una de las entradas del blog). Cada uno de estos elementos puede contener otros, como descripción, autor, link y algunos más. Según el caso puede que tengas la información del canal en un modelo, y la de los ítems en un modelo relacionado.
Canal

El canal debería proporcionar como mínimo los siguientes campos:
  • title: El título o nombre del canal
  • description: Un texto breve que describa o resuma el contenido del canal
  • link: El enlace a la página principal del canal
Aparte de eso, hay un par de campos que son recomendables:

docs: Un enlace a la especificación de RSS (Yo uso http://blogs.law.harvard.edu/tech/rss)
atom:link: Este elemento está recomendado para mejorar la interoperabilidad del feed con las aplicaciones lectoras.

$channel['atom:link'] = array(
     'attrib' => array( 
             'href' => Router::url(null, true), 
             'rel' => 'self', 
             'type' => 'application/rss+xml' 
));

Este elemento contiene varios atributos y, como puedes ver, se indican en forma de array asociativo. Básicamente href es un enlace al propio feed, que podemos obtener fácilmente usando la clase Router, y pasándolo el parámetro nulo para que utilice la url que genera el propio feed.

En mi caso particular, he añadido estos campos en el layout /views/layouts//rss/default.ctp ya que se generan igual para cualquier feed de mi aplicación y, de este modo, me puedo olvidar de ellos en cada vista concreta.

Otra cuestión que hay que controlar es que ni title ni description deberían contener HTML. Respecto a title, suele ser normal tener un campo de título de texto puro. Pero en description puede ocurrir que los usuarios añadan algo de HTML si el sistema lo permite, como es mi caso. Lo mejor es utilizar la función de php strip_tags, que realiza el trabajo sin más problemas:

$channel['description'] = strip_tags($channel['description']);

Esta transformación también la hago en la vista (no en el controlador) porque se refiere al modo en que se va a mostrar la información en el documento final.

Items

Los items se han de pasar a la vista en forma de array de arras a partir de datos del modelo que corresponda. El proceso  de los items en la vista se realiza mediante un bucle foreach, y la tarea que hay que hacer con cada ítem es generar correctamente los elementos hijos necesarios:
  • title
  • description
  • link
  • guid
  • pubDate
  • author
  • enclosure
RSSHelper incluye un método que nos devuelve un elemento ítem correctamente formado si le pasamos un array asociativo con estas claves.

Vamos a ver ahora qué tipo de contenido tiene cada clave y como habría que prepararlo.

Title es una cadena de texto puro, así que no debería tener más complicaciones. Usa strip_tags si es necesario.
Description sería un resumen del contenido. Sin embargo, puedes querer incluir el contenido completo del artículo en cuestión, incluyendo el HTML. El RSSHelper te ayudará a codificar correctamente el HTML, pero tendrás que asegurarte de que algunas etiquetas no son incluidas, como iframe, object o script. La solución es pasar este campo por Sanitize::stripTags

$item['description'] = Sanitize::stripTags($post['Post']['content'], 'iframe', 'object', 'param', 'script');

link: es un enlace al artículo. Puedes usar una URL en forma de array de CakePHP, ya que el RSSHelper lo reconoce y lo procesa, y te beneficias del Reverse Routing si tienes rutas personalizadas.
guid: se trataría de un identificador global único para el artículo, el cual podría ser perfectamente su url. Pero para hacerlo bien debes pasar un formato particular:

$item['guid']  = array( 
    'url' => array(
             'controller' => 'posts', 
             'action' => 'view', 
             $post['Post']['slug'] ), 
    'isPermaLink' => 'true'
));

El atributo isPermalink indica que el guid se corresponde con el enlace permanente del recurso.
pubDate: se trata de la fecha de publicación del ítem.

Author se refiere al autor o autora del artículo. El dato principal es su email, de modo que el formato recomendado sería:

    correo@example.com (Autor del Item)

Sin embargo, no consigo que valide si pongo más de un autor.

Enclosure es un elemento opcional para indicar un contenido multimedia asociado. Tiene tres atributos, por lo que se pasa en forma de array con las claves:
  • url: que nos indicaria la URL del recurso multimedia
  • length: su tamaño
  • type: el mime type del recurso

Paso 4: 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
En la vista basicamente lo que vamos a hacer son dos cosas: revisar la información de Channel, y pasarla al Layout, y procesar la información de cada registro para generar el Item correspondiente.

Para el layout, he copiado en APP/views/layouts/rss/default.ctp el layout que viene por defecto con el core de CakePHP, a fin de poder hacer algunas modificaciones. Lo detalle en el Paso 5.

En este caso, la información de Channel la ventilamos rápidamente asegurándonos de que description no contiene HTML. Luego pasamos la variable al layour, pero con el nombre $channelData, que es el que espera.

$channel['description'] = strip_tags($channel['description']);
$this->set('channelData', $channel);


Lo más normal es que hayamos pasado la información sobre los ítems en un array típico de resultados de un find. Tenemos entonces que recorrer ese array, registro a registro, y construir un array asociativo con los datos del ítem, por ejemplo:

foreach ($posts as $post) {
    $item['title'] = $post['Post']['title'];
    $item['description'] = Sanitize::stripTags($item['Post']['content'], 'iframe', 'object', 'param', 'script');

    // Otros campos
    echo $this->Rss->item(array(), $item).chr(10);
}



En algunos casos, los campos del registro pueden pasarse de forma directa. En otros casos, como description, necesitamos hacer algunos ajustes. En nuestro ejemplo, eliminamos algunos elementos HTML del contenido que no validan.


La vista resultante contiene los items del RSS listos para insertar en el layout.

Paso 5. Layout para RSS

A continuación, el código del layout que tengo en APP/views/layouts/rss/default.ctp

Rss->header(); ?>
"http://www.w3.org/2005/Atom");
}
if (!isset($channelData)) {
$channelData = array();
}
if (!isset($channelData['title'])) {
$channelData['title'] = $title_for_layout;
}
$channelData['atom:link'] = array('attrib' => array(
'href' => Router::url(null, true),
'rel' => 'self',
'type' => 'application/rss+xml'
));
$channelData['docs'] = 'http://blogs.law.harvard.edu/tech/rss';

$channel = $this->Rss->channel(array(), $channelData, $content_for_layout);
echo $this->Rss->document($documentData,$channel);
?> 


Lo más interesante de este código es que aprovecho esta instancia para incluir algunos parámetros que mejorarán las posibilidades de validación del feed.

Paso 6: 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.

lunes, 23 de abril de 2012

Construyendo un permalink con el Router

Se supone que un Permalink es una URL que apunta a un recurso y que no va a cambiar con el tiempo, o dicho de otro modo, es la URL que nos lleva siempre a cierto recurso dentro de una web. Debe ser una URL absoluta.

En CakPHP la URL habría que construirla obteniendo la URL básica del sitio, añadiendo el controlador, la acción y los parámetros necesarios para identificar ese recurso.

En el supuesto de tener un modelo Post, con un campo "slug" (un slug actúa como un id significativo para un post, generado a partir de su título, lo que hace url del tipo /posts/view/el_libro_de_petete), puedes hacerlo así (este código está en la vista view del post):

Router::url(array ($post['Post']['slug']), true);

Es decir, al router nos basta pasarle dos parámetros: un array con datos sobre la URL y un flag para indicarle si queremos una URL absoluta o relativa.

La gracia del router es que es capaz de averiguar qué controlador y acción tiene que utilizar para completar la URL. En el ejemplo, el código está en la vista para la acción View del controlador Posts. Pero si estoy generando el Router en otra parte, podría pasarle

viernes, 13 de abril de 2012

ModSecurity y Ajax Upload

Al cambiar de hosting un proyecto me encontré con un problema pues dejó de funcionar mi módulo de subida de archivos.
Con la ayuda de los logs del servidor y del soporte técnico de Dinahosting localizamos que el problema tenía que ver con ModSecurity y, después de un buen rato de investigación, llegué a la conclusión de que la cusa tenía que estar en el código javascript, que en mi caso es el Valums Ajax File Uploader
La solcuión final es realmente curiosa. Hay que evitar que la petición Ajax especifique la cabecera Content-Type.

martes, 13 de marzo de 2012

Crear Models y Fixtures para Tests

Una de mis frustraciones con CakePHP es que la documentación para crear Test Unitarios no cubre bien todas las posibilidades. Encontrar algunas soluciones concretas requiere bastante investigación en el manual, API, código del core y en blogs dispersos.

Uno de estos puntos oscuros (para mí) es el Test de Behaviors. Los Behaviors, como sabrás, son extensiones a los Models. Normalmente para crear los tests necesitarás un modelo al que asociarlos y el problema es que, por lo general, este modelo debería ser independiente y específicamente creado para la ocasión, o lo bastante genérico como para no introducir factores indeseados en el proceso de test.

¿Cómo crear esos modelos? Pues esto es lo que voy a intentar contar en este artículo. Esto lo he aprendido estudiando el código de tests de Cake y, aunque no tengo claro que sea todo lo completo que desearía, al menos he conseguido que vaya funcionando para mis propósitos.

Models

Para empezar, los Model para Tests extienden la clase CakeTestModel (que a su vez desciende de Model y se ajusta para utilizar la configuración de base de datos de test por defecto),
Pero nuestros Models no deberían acceder a una base de datos "física", a fin de no introducir elementos de distorsión de los resultados del test.

Esto se consigue definiendo el esquema o estructura de datos en el propio modelo y especificando que no se use una tabla. Aquí tienes un ejemplo:

class Basic extends CakeTestModel {
    var $useTable = false;
    var $name = 'Basic';
    var $_schema = array(
        'id'=> array('type' => 'string', 'null' => '', 'default' => '1', 'length' => '36', 'key'=>'primary'),
        'title'=> array('type' => 'string', 'null' => '', 'default' => '', 'length' => '255'),
        'key'=> array('type' => 'string', 'null' => '1', 'default' => '', 'length' => '255'),
    );
}


El lenguaje de esquema de CakePHP es bastante sencillo y te permite definir los tipos de campos que necesites. En este caso, necesitaba dos campos de texto, aparte del campo id.

Pero, ¿dónde defino este modelo?

Pues se me ocurren dos ubicaciones. Si no piensas reutilizarlo, podrías definirlo en el mismo archivo que el test.

Pero si crees que puedes utilizarlo en tests de otros elementos de tu aplicación (otros Behaviors, Controllers, etc) es buena idea guardarlo en un archivo separado e incluirlo cuando sea necesario.
En mi caso, he creado un archivo models.php en la carpeta tests de la aplicación. En este archivo colecciono los modelos creados para tests. Para incluirlo, no tengo más que usar este código:

require_once(TESTS . DS . 'models.php');

Y luego crear una instancia del modelo cuando la necesite con los métodos habituales, ya sea mediante new Model o ClassRegistry.


Los datos

En la mayoría de los tests necesitaré datos en los modelos, para lo cual tendré que crear un archivo de fixtures.

En teoría debería bastar con utilizar la propiedad CakeTestFixture->import y especificar que vamos a usar el modelo creado, pero yo no he conseguido hacerlo funcionar así, por lo que he tenido que especificar los campos del modelo en el archivo de fixtures (basta con copiar y pegar el contenido de Model->_schema). Puede que sea debido al hecho de no utilizar una tabla en la base de datos, sin embargo, no deja de ser un engorro. La parte buena es que estos modelos no tienen que cambiar mucho y realmente no da tanto trabajo.

Incluimos también los registros (records) que necesitemos para empezar. Este es un ejemplo de TEST/fixtures/basic_fixture.php


class BasicFixture extends CakeTestFixture {
    var $name = 'Basic';
    var $fields = array(
        'id'=> array('type' => 'string', 'null' => '', 'default' => '1', 'length' => '36', 'key'=>'primary'),
        'title'=> array('type' => 'string', 'null' => '', 'default' => '', 'length' => '255'),
        'key'=> array('type' => 'string', 'null' => '1', 'default' => '', 'length' => '255'),
    );
   var $records = array(
        array(
            'id' => 1,
            'title' => 'Lorem ipsum dolor sit amet',
            'key' => 'lorem_ipsum_dolor_sit_amet',
        ),
    );
}
?>


En el test declaramos que vamos a usar como fixtures app.basic y así estamos listos para trabajar. Aquí tienes un ejemplo de un test que utiliza el modelo Basic.

Conclusiones

Finalmente, tener modelos para usarlos específicamente en tests unitarios es bastante fácil. Son autocontenidos, por lo que no necesitas tener una base de datos y los puedes reutilizar dentro de un mismo proyecto o en otros.

martes, 28 de febrero de 2012

Una celda de tabla que se ajusta al contenido

Estoy diseñando una interfaz de backend en la que necesito que una columna de una tabla contenga varios botones de acciones que se pueden realizar con el registro correspondiente. En cada tabla puede haber distinto número de acciones (que son enlaces) y quiero que la columna ocupe tan sólo el espacio necesario para mostrar todos los botones.


Pues bien, es bastante sencillo.
Éste es el código CSS para la celda que contiene las acciones (tiene la clase actions). Ten en cuenta que las acciones son simplemente elementos A seguidos.:


.actions {
white-space: nowrap;
width: 1px;
}

La primera propiedad white-space: nowrap; hace que no se salte automáticamente de línea por la separación de los textos cuando no hay anchura suficiente para mostrarlas todas.
La segunda width: 1px; garantiza que el ancho de la celda es menor que cualquiera posible del contenido, pero como la anterior hace que el conjunto de etiquetas A se mantenga en la misma línea la celda se ve forzada a aumentar su tamaño para abarcarlas todas (es algo propio de la propiedad display: table-cell que tienen las TD por defecto).
El resultado es que la columna para las acciones se adapta automágicamente al contenido, sin saltos de línea.

martes, 19 de abril de 2011

Custom Find DRY

Una de las cosas que más me gusta de los custom find en CakePHP es la flexibilidad que pueden proporcionar a partir de una sintaxis única y sencilla.

Recientemente me he dado cuenta de cómo usarlos para evitar duplicaciones innecesarias de código.

Como ya sabrás, un método para custom find tiene esta signatura básica:

function _findCustom($state, $query, $results = array())

Este método se llamaría públicamente así

$model->find('custom' [, $options]);

La llamada $model->find('custom') hace, a grandes rasgos, lo siguiente:

  1. Normaliza las opciones que se han pasado para crear un array, que contiene los datos necesarios para generar la query que se hará a la base de datos.
  2. Llama a _findCustom para incluir las modificaciones que éste método haga en la query.
  3. Convierte la query en SQL y obtiene el resultado de la base de datos
  4. Llama una segunda vez a _findCustom para procesar el array de resultados.

Lo interesante es que nada nos impide llamar desde otros métodos del modelo a _findCustom pasándole el parámetro $state (que puede ser 'before' o 'after', según queremos obtener el resultado de la modificación de $query o de $results, respectivamente.

¿Para qué sirve? Supongamos que tenemos un _findCustom más o menos complejo en el que especificamos varios joins, conditions, etc, para un cierto tipo de búsqueda. Supongamos también que necesitamos hacer un cierto número de variantes de esa búsqueda, cambiando tan sólo algunas condiciones.

Pues bien, en ese caso podríamos llamar internamente desde nuestro segundo método _find a _findCustom con los parámetros extra para que genere el query (o bien, obtener el query y luego modificarlo). Por ejemplo:

function _findVariant($state, $query, $array()) {
    if ($state === 'before') {
         $extraQuery = array('conditions' => array('field' => 'value'));
         $query = Set::merge($query, $extraQuery);
         $query = $this->_findCustom('before', $query);
    }
    return $results;
}