La instalación
Catalyst evoluciona muy rápidamente y los paquetes que ofrece Debian no están todo lo actualizado que deberían, así que hacemos una instalación manual en el /usr/local/ desde el CPAN.
cpan -i Bundle::Catalyst
La instalación desde CPAN es un poco larga y pesada y en alguna ocasión, se
ha detenido por referencias incumplidas. En cuyo caso, se vuelve a ejecutar el
comando hasta que no dé error. De todas formas, al parecer esto no incluye
todos los paquetes necesarios (y dependerá de como tengamos la instalación de
Perl en nuestra máquina). Así que cada vez que intento ejecutar alguno de los
scripts que menciono si me da error diciendo que no encuentra un paquete lo
instalo
cpan -i [nombre::del::paquete::que::falta]
También vamos a necesitar una base de datos disponible. Para simplificar la tarea usaremos sqlite versión 3
apt-get install sqlite3
El proyecto
El proyecto será la creación de un sitio web online de recetas de cocina comunitario. Queremos que nuestro libro de recetas:
- Muestre una lista de todas las recetas
- Crear nuevas recetas o modificar las anteriores
- Poder asignar a cada receta una categoría (como «postre» o «sopa»)
Añadido respecto al tutorial de RoR, esta es una típica aplicación que se conoce como CRUD (Create, Read, Update, Delete) se mencionó en un comentario de la otra entrada que Maypole (otro framework para Perl) estaba diseñada especialmente para este tipo de tareas. Aun así, vamos a demostrar en este texto que Catalyst no se queda atrás.
Creación del esqueleto base y prueba inicial
Vamos al directorio de nuestra elección y ejecutamos el script inicial
catalyst.pl -short Cookbook
Nos genera un directorio llamado Cookbook con una serie de subdirectorios que son:
- lib
- Donde se encuentra el código de nuestra aplicación web.
- root
- Es el directorio donde poner los recursos estáticos de nuestra web. Tales como iconos u hojas de estilos.
- scripts
- Utilidades que nos facilita la labor de diseñar la aplicación. Tales como un servidor web para probar la aplicación o utilidades para hacer funcionar la web como fastcgi. Han cambiado la nomenclatura respecto al otro tutorial, ahora los scripts se llaman [nombre del proyecto]_loquesea.pl.
- t
- Catalyst está diseñado usando test unitarios. Los ficheros que va generando a nuestra petición incluyen los test correspondientes. En este directorio están disponibles los test a efectuar sobre nuestra aplicación. Obviamente se pueden añadir los nuestros propios cuando queramos.
Catalyst tiene dos sistemas de nombrar sus directorios, el largo y el corto. Al parecer, el asistente que usaremos (Scanffold) genera sus archivos usando la nomenclatura corta, así que para mantener homogeneidad, emplearemos la nomenclatura corta en todos nuestros scripts.
La prueba inicial de que todo funciona es ejecutar el miniservidor:
script/cookbook_server.pl
Nos indica que podemos acceder a nuestra página en el puerto 3000 de nuestra máquina. En mi caso: http://domagxo:3000 (también vale http://localhost:3000). La web que sale es bonita pero aún no se parece mucho a un libro de cocina. ;-)
Creación del control de la aplicación
Los controladores los podemos considerar como los programas (pueden existir más de uno), que leen la url de la página y determinan que se hace con ella. Por ejemplo, en el caso de libertonia, podríamos hablar de un controlador «user» que toma los siguientes parámetros de la url para mostrar los datos de un usuario registrado, o el controlador «story» cuyos siguientes datos son fechas y otras cosas.
Vamos a crear un controlador llamado Cookbook. Después si queremos podemos redireccionar para que siempre se utilice este controlador y nos ahorramos el tener que escribir «cookbook» en todas las urls. Fijaos que no hay inconveniente en que el controlador se llame igual que el proyecto. El fichero principal del proyecto es /lib/Cookbook.pm y los controladores se instalan en /lib/Cookbook/C/. La 'C' es de Controller, esto es así por usar la nomenclatura corta.
script/cookbook_create.pl -short controller Cookbook Scaffold CDBI::Recipes
Analicemos el comando: Le estamos diciendo que nos cree un controlador (con los ficheros necesarios) llamado Cookbook, usando un asistente llamado Scaffold y que el Modelo (donde se almacena la información, la tabla de nuestra base de datos) será CDBI::Recipes.
Entre otras cosas genera: un fichero de test (t/controller_Cookbook.t) y plantillas de ficheros html para añadir, editar, listar y mostrar recetas. (todos terminan en .tt porque scaffold usa el sistema de plantillas Template Toolkit).
Creando la base de datos y su correspondiente modelo
Hemos hablado del nombre del modelo que usaremos «CDBI::Recipes», pero aún no lo hemos creado ni tampoco hemos creado la base de datos relacionada. Como comenté al principio, trabajaremos con la base de datos sqlite3. Partimos de la descripción de la base de datos en sql. Creamos un fichero llamado cookbook.sql con el siguiente contenido:
CREATE TABLE recipes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255),
description VARCHAR(255),
date DATE,
instructions TEXT
);
Y ahora creamos la base de datos:
sqlite3 cookbook.db < cookbook.sql
Nuestra siguiente misión es la de crear el modelo o lo que es lo mismo, como nuestro sistema va a interaccionar con la base de datos. Para ello usamos otra vez un asistente.
script/cookbook_create.pl -short model CDBI CDBI dbi:SQLite:/home/javi/propios/programacion/perl/Catalyst/Cookbook/cookbook.db
(ojo, todo es una sola línea)
Por cada tabla definida en la base de datos, tendremos un «submodelo». Así que nuestra tabla Recipes se convierte en CDBI::Recipes y queda cerrado el cabo que teníamos abierto en la definición del controlador.
Otra vez analizamos la línea: creamos un modelo llamado CDBI usando un asistente llamado CDBI (esta repetición de nombres parece un convenio), y por último el identificador de la base de datos. Aunque esto sería como para otro artículo completo, mencionaré que el interfaz de bases de datos dbi tiene más de cien bases de datos adaptadas. Así que cambiando ese descriptor podemos usar mysql, postgresql, ficheros cvs, oracle... ver este listado de cpan si hay curiosidad de las bases de datos disponibles.
Creación de la Vista
Nuestro asistente scaffold nos ha creado plantillas para la vista. Aunque no hemos creado aun la vista. (la clase que se encarga de tomar los datos y las plantillas para obtener el html final).
script/cookbook_create.pl -short view TT TT
Y ahora las manos en la masa
Primero es conectar al controlador general lib/Cookbook.pm la vista por defecto: Cookbook::View::TT.
Editamos lib/Cookbook.pm y hacemos dos modificaciones:
La subrutina «end» se ejecuta por defecto en todos las peticiones. La página que se envía al navegador es $c->res->output. Así que si esta no está definida ya (porque haya habido algún error y sea un informe de error o porque se haya usado una Vista alternativa, por ejemplo), transferimos la ejecución a nuestro objeto Vista por defecto.
La segunda modificación que necesitamos es darle a las clases Modelo, la capacidad de convertirse de y a html. La clase que generamos CDBI es clase base de cada tabla, las modificaciones efectuadas ahí, tienen efecto en todas sus clases hijas.
Solo hay que añadir la línea que pone «additional_base_classes»
__PACKAGE__->config(
dsn => 'dbi:SQLite:/home/javi/propios/programacion/perl/Catalyst/Cookbook/cookbook.db',
user => '',
password => '',
options => {},
additional_base_classes => [qw/Class::DBI::FromForm Class::DBI::AsForm/]
relationships => 1
);
Para cambiar la base de datos, solo es necesaria cambiar la línea del identificador «dsn» y si fuera necesario indicar usuario y contraseña.
Primera prueba
Ya podemos ejecutar nuestra primera versión del invento. Ya sabéis:
script/cookbook_server.pl
Y tenemos
que ir a http://localhost:3000/cookbook/ para que el controlador que
hemos creado actúe. Bastante feo, pero funciona. Si lo comparamos con
las capturas del tutorial de RoR, veremos dos diferencias
fundamentales:
La primera es que en RoR, la fecha se introduce
usando combobox, y con Catalyst es una entrada de texto. Este problema
no es de Catalyst, es de sqlite que las fechas las considera como
cadenas de texto. Así que al elaborar el formulario, pide una cadena
de texto. Tiene solución, aunque no automática. Para solucionarlo
tendríamos que tocar la plantilla de añadir/editar fichas y sustituir
esa entrada de texto por los componentes adecuados y luego hacer una
conversión de dicho formulario a una cadena para poder introducirla en
la base de datos. Supongo que usando otras bases de datos este
problema no se daría (si alguien lo prueba, que comente sus
experiencias).
La segunda diferencia es el orden en el que se presentan los diversos
campos. Están por orden alfabético en vez de por orden de definición. esto
está ocasionado por el modelo empleado CDBI. Este considera que cada línea de
la base de datos es un hash de Perl (una agrupación tipo diccionario, tu
preguntas por el campo y te devuelve su contenido) y los hash no son
ordenados. Así que, lo único que se puede hacer aquí es tocar las plantillas
de visualización (los ficheros .tt) para que muestren el orden de la forma
deseada. De hecho, en el tutorial de RoR se hace así, es decir se crea una
plantilla para que los datos salgan en el orden que les interesa. Aquí lo
dejaremos como ejercicio. ;-)
Añadiendo las categorías a las recetas
Primero tenemos que crear, la tabla en nuestra base de datos. Por
simplificar el proceso, vamos a recrear la base de datos completa.
Modificamos cookbook.sql para que quede así:
CREATE TABLE recipes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255),
description VARCHAR(255),
date DATE,
instructions TEXT,
categories_id INTEGER,
FOREIGN KEY (categories_id) REFERENCES categories(id)
);
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255)
);
La tabla «recipes» tiene un campo nuevo: categoria_id que apunta a una entrada en la
tabla «categories».
Recreamos la estructura de la base de datos:
script/cookbook_create.pl -short model CDBI CDBI dbi:SQLite:/home/javi/propios/programacion/perl/Catalyst/Cookbook/cookbook.db
.
(de nuevo, esta es tambien solo una línea)
Si Catalyst pretende modificar un fichero que ya ha sido modificado por
nosotros, lo que hace es crear un fichero con el mismo nombre pero añadiendo
.new al final del mismo. Así podemos comparar el nuevo fichero con lo que
teníamos y resolver qué escogemos de cada cosa. En este caso, aunque hayamos
modificado la estructura de la tabla «recipes», la clase que la controla no ha
sido modificada, en cambio, si que hemos creado una nueva clase:
CDBI::Categories para nuestra nueva tabla.
El segundo paso es crear el controlador para poder añadir/editar/modificar
categorías:
script/cookbook_create.pl -short controller Categories Scaffold CDBI::Categories
Al cargarse la base de datos, identifica la relación entre las dos tablas y
se encarga de visualizar el enlace a la otra tabla(*). Aunque en principio usa
la clave principal (un número en nuestro caso) como identificador en la
columna. Así que vamos a indicarle que queremos que use como identificador la
columna «name». Para eso, en el fichero lib/Cookbook/M/CDBI/Categories.pm
incluimos tras «use strict» la siguiente línea:
__PACKAGE__->columns(Stringify => qw/name/);
Y con esto hemos llegado al final del tutorial de RoR y de nuestro tutorial.
Dicho artículo tiene una segunda parte en el que se habla de registro y
sesiones. Eso quedará para otra entrada.
[*] Montando el artículo he descubierto una errata en la versión 0.22 de la
clase Class::DBI::Loader::SQLite (sobra un ^ en el if final de la función
_relationship). Ya se lo he comunicado al autor y supongo que en breve se
publicará una nueva versión del fichero con ese error subsanado. Así que si no
tenéis mucha gana de pelearos usad otra base de datos o esperad a que se
publique la versión corregida. (Sin corregir, las tablas no se
relacionarán).