Primeras impresiones de CakePHP

Uno de los primeros artículo de este blog hablaba de mis primeras impresiones sobre Ruby on Rails. Ahora voy a hablar sobre otro framework MVC que estuve investigando y utilizando en los últimos días. Se trata de CakePHP, como podrán intuirlo, para PHP.

Llegué a CakePHP porque me gustó mucho la filosofía detrás de Ruby on Rails, pero no tenía tiempo de ponerme a aprender un lenguaje nuevo, como es Ruby. Me pregunté entonces si no existiría algo similar en un lenguaje que conociera, como PHP. Así fue como encontré CakePHP. También encontré otros frameworks MVC para PHP, que no tuve tiempo de investigar, como por ejemplo, el francés symfony.

Creo que no me equivoco si digo que CakePHP es una copia de Ruby on Rails. Las funcionalidades son idénticas, así como el nombre de muchos de los métodos. Pero bueno, por lo menos, podemos programar con PHP al estilo Rails. De hecho, este mismo blog lo desarrollé con CakePHP en tan solo un par de horas.

Sin embargo, CakePHP tiene un problema grave: la falta de buena documentación y ejemplos. Creo que por más bien que funcione, si falta eso, es una gran contra. CakePHP tiene un manual muy básico y, a mi parecer, muy mal organizado. El manual no llega a cubrir con un nivel aceptable todas las características del framework y remite casi todo el tiempo a la documentación de la “API”. Pero la documentación de la API es todavía peor, por no decir casi inservible. Dificil y oscura para navegar, y sin ejemplos que ilustren el uso de los métodos, se hace imposible entender cómo hacer ciertas cosas.

Sin dudas, CakePHP es una buena alternativa a Rails, para programadores de PHP, pero una documentación tan pobre y un manual casi muerto hacen que realmente pueda llegar a ser más fácil aprender un muy buen lenguaje nuevo, como es Ruby y un framework mucho mejor documentado, y con mayor soporte de la comunidad, como esRuby on Rails.

 

Referencia de LLBLGen Pro

Esta es una referencia rápida de LLBLGen Pro a la que podrán acceder para consultar la forma de realizar tareas básicas con este framework.

La documentación la pueden bajar acá. Está disponible en varios formatos.

La clave para empezar a usar la documentación esta rápido es leer el capítulo “Tutorials and Examples -> How do I … ?”

Luego, para ahondar más en conceptos teóricos pueden leer ” Using the generated code”. Cuando habla de los dos modelos de código soportados, SelfServicing y Adapter , el que nos interesa a nosotros es el primero, ya que es el que vamos a usar.

El trabajo con LLBLGen Pro se divide en dos procesos:

  1. Generación de código: LLBLGen Pro es una aplicación de escritorio para generar código. Resumiento, este proceso es así: se crea un proyecto, se le dice con qué base de datos se va a trabajar, el programa lee la estructura de la base de datos y genera un proyecto de tipo “librería de clases” en .NET. Este paso ustedes NO lo van a necesitar hacer ni aprender, ya que la base de datos y el código lo voy a generar yo (obviamente, si les interesa aprenderlo o necesitan modificarlo, no hay problema; yo siempre subo el proyecto de LLBLGen Pro al SVN).
  2. Uso del código generado: Una vez generado el proyecto de acceso a datos y agregado a la Solución de VS en la que van a trabajar, ya pueden utilizar el código generado. El código generado contiene varias clases, pero las que más van a usar ustedes con las “EntityClasses” y las “CollectionClasses”.

El uso del código generado es muy sencillo. Les paso algunos ejemplos (también se los adjunto).

NOTA: Para diferenciar mejor cuáles son palabras que vienen del framework de LLBLGen Pro, las que creo yo son en castellano. Por ejemplo, con “PersonaEntity” se pueden dar cuenta que yo creé una tabla llamada Persona y al generar código, me creó la clase con el nombre de la tabla + el sufijo “Entity”.

Guardar un nuevo registro en la base de datos:

PersonaEntity persona = new PersonaEntity();

persona.Nombre = "Marcelo";
persona.Apellido = "Ruiz";

persona.Save();

Traer un registro de la base de datos

int idPersona = 1;
PersonaEntity persona = new PersonaEntity();
persona.FetchUsingPK(idPersona);

Traer un registro de la base de datos (otra forma)

PersonaEntity persona = new PersonaEntity(IdPersona);

// No estoy seguro qué pasa si el IdPersona no existe,
// pero eso verifico que no sea nuevo.
if (!persona.IsNew)
{
    Console.WriteLine("Nombre = "+ persona.Nombre);
}
else
{
    Console.WriteLine("Persona no encontrada");
}

Actualizo un registro

PersonaEntity persona = new PersonaEntity();
if (persona.FetchUsingPK(1))
{
    persona.SegundoNombre = "Hernan";
    persona.Save();
}
else
{
    Console.WriteLine ("Persona no encontrada");
}

Borrar un registro

PersonaEntity persona = new PersonaEntity(IdPersona);
persona.Delete();

Filtros

PersonaCollection personas = new PersonaCollection();

// quiero un filtro equivalente al SQL "WHERE Edad > 18 AND Edad < 40"
IPredicateExpression filter = new PredicateExpression();
filter.Add(PersonaFields.Edad > 18);
filter.AddWithAnd(PersonaFields.Edad < 40);

personas.GetMulti(filter);

// recorro la colección:
foreach (PersonaEntity persona in personas)
{
    Console.WriteLine (persona.Nombre +" "+ persona.Apellido);
    foreach (TelefonoEntity telefono in persona.Telefonos)
    {
        Console.WriteLine(telefono);
    }
}

Filtro y ordenamiento

PersonaCollection personas = new PersonaCollection();

IPredicateExpression filter = new PredicateExpression();
filter.Add (PersonaFields.Edad > 18);
filter.AddWithAnd(PersonaFields.Edad < 40);

ISortExpression sorter = new SortExpression();
sorter.Add(PersonaFields.Name | SortOperator.Ascending);
sorter.Add(PersonaFields.Apellido | SortOperator.Ascending);

personas.GetMulti(filter, 0, sorter);

// recorro la colección:
foreach (PersonaEntity persona in personas)
{
    Console.WriteLine(persona.Nombre +" "+ persona.Apellido);
    foreach (TelefonoEntity telefono in persona.Telefonos )
    {
        Console.WriteLine(telefono);
    }
}

Otro filtro más elaborado

PersonaCollection personas = new PersonaCollection();

// quiero un filtro equivalente al SQL "WHERE ((Edad < 18 OR Edad > 40) AND Nacionalidad = 'Argentino')"
IPredicateExpression filtroCompleto = new PredicateExpression();

IPredicateExpression filtroPorEdades = new PredicateExpression();
filtroPorEdades.Add(PersonaFields.Edad < 18);
filtroPorEdades.AddWithOr(PersonaFields.Edad > 40);

filtroCompleto.Add(filtroPorEdades);
filtroCompleto.AddWithAnd(PersonaFields.Nacionalidad == "Argentino");

personas.GetMulti(filtroCompleto);

// recorro la colección:
foreach (PersonaEntity persona in personas)
{
    Console.WriteLine(persona.Nombre +" "+ persona.Apellido);
    foreach (TelefonoEntity telefono in persona.Telefonos)
    {
        Console.WriteLine(telefono);
    }
}

Guardar datos asociados de un solo paso:

PersonaEntity persona = new PersonaEntity();
persona.Nombre = "Marcelo";
persona.Apellido = "Ruiz";

TelefonoEntity telefono1 = new TelefonoEntity();
telefono1.Numero = "4567-1234";
telefono1.Descripcion = "Casa";

TelefonoEntity telefono2 = new TelefonoEntity();
telefono2.Numero = "4000-7896";
telefono2.Descripcion = "Trabajo";

persona.Telefonos.Add(telefono1);
persona.Telefonos.Add(telefono2);

// Guardo con "true", para indicar que guarde tmb los
// datos relacionados (telefonos).
bool resultado = persona.Save(true);
if (resultado)
  Console.WriteLine("OK");
else
  Console.WriteLine("Error");

Limpiar la cache de CakePHP al actualizar Modelos

Uno de los errores más frecuentes que se suelen ver al trabajar con CakePHP es que, por más que actualicemos nuestra base de datos (por ejemplo, agregando nuevos campos), los modelos no se ven actualizados.

Esto se debe al hecho de que CakePHP cachea los modelos en la carpeta app/tmp/cache/models.

Cuando estamos en modo development, este no es el caso. El problema aparece en producción. En estos casos lo que tenemos que recordar siempre es borrar el contenido de esa carpeta para forzar el refresco de los modelos.

Es importante recordar también que toda la carpeta tmp y sus subcarpetas necesitan permisos de escritura.

Impresiones de Ruby on Rails

En este artículo voy a comentar las primeras impresiones que me dio Ruby on Rails, visto desde la óptica de alguien que viene de programar aplicaciones web en ASP, PHP y ASP.NET.

Ruby on Rails es un framework para el desarrollo de aplicaciones web con basadas en bases de datos. Se trata de un framework “RAD” (Rapid Application Development) que nos permitirá crear aplicaciones web totalmente funcionales en un tiempo mucho menor que utilizando otras tecnologías, por ejemplo, ASP.NET.

Ruby on Rails (RoR) me sorprendió de entrada con características muy ventajosas que me hicieron preguntar por qué en ASP.NET o en PHP tengo que escribir hasta 10 veces más código para lograr lo mismo.

La respuesta no es “porque RoR es mejor”. El tema es que RoR está muy enfocado en una dirección, y es “el desarrollo de aplicaciones web basadas en bases de datos”. RoR también se maneja mucho con ciertas convenciones que de alguna forma nos limitan en esa dirección, pero nos permiten lograr resultados más rápido.

Un ejemplo de estas limitaciones es que RoR espera que cada una de las tablas de nuestra base de datos tenga un campo autonumérico definido como la primary key llamadoid. Si cumplimos con esa convención, RoR promete hacernos las cosas más fácil.

Por supuesto, RoR permite ciertos deslices de esas convenciones, pero pagando el costo de mayor cantidad de código.

Sinceramente no me puse a investigar qué pasa si queremos trabajar con modelos de datos un poco más complejos. Por ejemplo, si queremos modelar herencia en la base de datos, algo que suelo utilizar bastante. Otra cosa que noté es que las tablas de asociación de muchos-a-muchos no se modelan en RoR, es decir, no son representadas como objetos en el modelo; con esto, no sé qué pasaría si yo quiero agregar algún campo extra a estas tablas para calificar una asociación. Sinceramente son cosas que tengo que investigar.

Patrón MVC

Ruby on Rails implementa el patrón MVC. Por alguna razón este patrón está de moda. Aunque, pensandolo bien, es un patrón que funciona MUY bien para aplicaciones web basadas en datos.

No voy a explicar el patrón MVC aquí, solo voy a explicarles cómo lo implementa RoR, ya que me costó un poquito entenderlo.

El Modelo (la M de MVC), en Ruby on Rails contiene clases que representan a las tablas de nuestra base de datos. Una característica destacable es que nosotros creamos estas clases pero no es necesario definir los atributos para acceder a los campos de las tablas que representan. Rails infiere esta información automáticamente a partir del nombre de la clase (para esto sirven las convenciones).

En las clases del Modelo podemos, además, agregar otra lógica propia de cada clase. Por ejemplo, si tenemos una clase Product, en principio, se vería así:

class Product < ActiveRecord::Base

end

Supongamos que queremos agregar código para calcular el precio con impuestos. En ese caso, la clase Product es el lugar indicado para hacerlo.

class Product < ActiveRecord::Base

  def price_plus_taxes
    price * 1.21 
  end

end

También podemos definir métodos estáticos:

class Product < ActiveRecord::Base

  def price_plus_taxes
    price * 1.21 
  end

  def self.find_available_products
    Product.find_by_available(1)
  end

end

Una de las confusiones que tuve fue con el tema de los Controllers y las Views (C y V en MVC). Creí erróneamente que también debe existir un Controller por cada tabla y una carpeta dentro de View por cada tabla. Si bien es bastante común esto no tiene por qué ser así.

Los Controllers son clases que agrupan un conjunto lógico de páginas. Por ejemplo, páginas para listar y visualizar artículos. En ese caso tendríamos un Controller llamadoArticlesController con métodos list (para listar) y show (para visualizar). A su vez, tendríamos una carpeta articles con plantillas que llevarán los mismos nombres que los métodos: list y show.

Sin embargo, la forma en que manejamos las entidades de nuestro modelo en nuestros controllers es totalmente libre. Solo tenemos que tener en cuenta la relación que hay entre Controllers y Views, como mencioné arriba.

Características principales de Ruby on Rails

Hasta aquí hice una breve presentación de RoR y expliqué cómo está implementado el patrón MVC. Voy a hablar ahora de las características que más destaco de MVC, sobre todo porque me encantaría tenerlas en otros lenguajes.

En general, encuentro que las características que más me atraen de Rails son aquellas que facilitan el trabajo del programador. Creo que Rails sigue la misma filosofía de Ruby, de programar para seres humanos y no para máquinas. Las características de Rails están pensadas para el mundo real; se pensó en cuáles son las necesidades cotidianas más comunes de cualquier programador de aplicaciones web y se implementaron soluciones realmente inteligentes.

Mapeo automático

Al crear una clase de tipo Model, Rails automáticamente infiere las propiedades de la misma, mapeandolas a los campos de la tabla de la base de datos. Rails sabe a qué tabla corresponde cada clase del Model gracias a las convenciones de nombres.

Lo mejor de esto es que, al agregar un nuevo campo a una tabla, no hace falta tocar nada en el Model; la nueva propiedad estará disponible automáticamente.

Hasta el momento no se me ocurre si esto es posible de hacer con .NET. Tal vez se pueda utilizando un poco de Reflection, pero lo dudo; y si se puede, debe ser difícil. Claro, una diferencia fundamental entre Ruby y el CLR de .NET es que Ruby tiene un soporte nativo para meta programación y los lenguajes de .NET son compilados, lo que hace que esto sea un poco dificil.

Validación

Otro ejemplo de una solución inteligente. Mientras que en frameworks como ASP.NET o incluso en CakePHP (otro framework MVC, pero para PHP) las validaciones se realizan a nivel de interfaz de usuario, en Rails se definen las reglas de validación en el Modelo.

Validar que el nombre y precio de un producto no estén vacíos, y que el precio sea un número válido, es tan fácil como agregar esta línea al modelo:

class Product < ActiveRecord::Base
  validates_presence_of :title, :price
  validates_numericality_of :price
end

Automáticamente Rails va a impedir que guardemos datos que no cumplan con dichas validaciones, y nos va a mostrar los errores correspondientes.

ASP.NET ofrece los controles de validación. Pero configurarlos y usarlos no es tan simple. Además, debemos implementarlos cada vez que trabajemos con la entidad.

Funciones find

Leer registros de la base de datos para llenar objetos o colecciones es por lejos una de las tareas más comunes. Para facilitarnos esta tarea, Rails tiene muchas variantes de la función find. Por ejemplo, todas las clases del Modelo tienen una función find para traer todos los registros de su tipo. Por ejemplo:

Product->find(:all)

Devolverá un array con todos los objetos Product de la base de datos.

Product->find(:first)

Devolverá el primer objecto Product.

Esta función acepta también más argumentos para definir un poco mejor los resultados que queremos y cómo los queremos ordenados. Por ejemplo:

Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)

Traerá todas las Person que cumplan con la condición del segundo argumento.

Pero donde esto se pone realmente interesante es cuando llamamos a funciones como:

Product.findByPrice(9.99)

Para traer todos los productos cuyo precio sea 9.99.

Wine.findByType('Malbec')

Para traer todos los vinos cuyo tipo sea Malbec.

Todas las funciones findBy<campo> son automáticamente generadas por Rails en tiempo de ejecución, gracias a las características de meta-programación del lenguaje Ruby.

Esto nos puede ahorrar realmente muchísimo trabajo. ¡Piensen en la cantidad de veces que necesitaron recurrir a funciones de este tipo! Si bien en otros lenguajes podemos construir funciones del tipo findBy<campo>, para evitar la repetición del código, de todas formas tenemos que escribirlas al menos una vez. En Rails, ya están todas “escritas”.

Formularios

Otra de las características destacables de Rails es el manejo de formularios asociados a entidades del Modelo. Mediante el método form_for, creamos un formulario HTML que estará asociado a una entidad determinada:

<% form_for :person, @person, :url => { :action => "update" } do |f| %>
First name: <%= f.text_field :first_name %>
Last name : <%= f.text_field :last_name %>
Biography : <%= f.text_area :biography %>
Admin?    : <%= f.check_box :admin %>
<% end %>

En el ejemplo, creamos un formulario para la entidad Person (esto lo definimos mediante el símbolo :person. La data se la pasamos en la variable @person. Y en el tercer argumento definimos qué acción ejecutar (en este caso, update).

Vean que luego utilizamos métodos especiales para crear los campos del formulario. Al utilizar los mismos nombres de los campos de nuestras tablas para los campos del formulario, Rails automáticamente sabrá cómo completarlos y sabrá luego cómo y dónde guardarlos cuando enviemos este formulario.

Sin dudas, esto simplifica muchísimo las cosas. Fíjense cómo controlamos este formulario desde la clase PeopleController:

@person = Person.findById(1);

¡Listo! No hizo falta ni siquera ir campo por campo asignando la propiedad correspondiente… form_for lo hace por nosotros.

Guardar

Lo bueno de guardar en Rails es que, cuando recibimos un form, recibimos también un array params, con todos los valores del formulario enviado. Entonces, con pasar este array params al método update o create, automáticamente Rails sabrá cómo guardar la entidad.

El método flash

El método flash puede parecer algo tonto, pero es muy útil. Su funcionalidad principal es pasar info entre peticiones diferentes. Uno de los principales usos es para mostrar notificaciones o mensajes de error.

Conclusión

Como ven, Ruby on Rails tiene muchísimas cosas interesantes. Cosas que tal vez no vamos a encontrar en otros frameworks y que se las vamos a envidiar.