Nuevo ORM para Vendimia

No he tenido tiempo de escribir sobre mi framework de desarrollo web Vendimia. Y ahora he tenido una idea para mejorar su ORM. Por algo aún está versión alpha Así que let’s rubber ducking!.

El presente

Actualmente Vendimia tiene un ORM similar a Rails: una clase inicialmente vacía que extiende a Vendimia\ActiveRecord\Record, y las relaciones se definen creando variables estáticas $belongs_to, $has_one, y $has_many dentro de la clase modelo. En su primera versión le había creado varios métodos mágicos tipo Django como:

namespace products;

$products = models\product::find_where_name_contains("keyboard");

Lo que genera un SELECT `products_product`.* FROM `products_product` WHERE (`name` LIKE "%keyboard%");, y devuelve un objeto Vendimia\ActiveRecord\RecordSet con cada registro del resultado.

En la segunda iteración del ORM no le implementé dichos métodos mágicos, en parte por flojera, y en parte porque no son buenas prácticas de programación (los IDE no los identifican, pueden causar confusión, etc.). Tengo pensado crear un trait para selectivamente añadir a un modelo los métodos mágicos.

Actualmente no realiza validación de los valores de los campos. Vendimia no conoce los campos de la tabla (a pesar que si los conoce, lee más adelante), ni su tipo. Si añades una propiedad de la clase cuyo campo no existe en la db, será el SQL quien genere una excepción.

A mi no me molesta mucho eso, pues funciona bien. Pero en un momento surgió un proyecto web donde tuve que guardar una lista de idiomas por cada participante. Usualmente solo es un idioma, pero puede haber participantes con dos (o más). Y me pareció demasiado overkill tener una tabla para guardar los idiomas, en especial por que solo usa dos caracteres para cada idioma.

Al final usé un simple campo string, y al obtener un registro de participante, le hago un explode. Y pensé que sería bonito que Vendimia haga eso automágicamente, asi como otros tipos de conversión de datos desde la db (Los campos date a un objeto Vendimia\DateTime, por ejemplo). Para ello necesito que el modelo sepa el tipo de cada campo.

El problema

El problema es que Vendimia tiene una forma de crear y actualizar la estructura de la base de datos, ergo tiene información sobre los campos de un modelo. Una definición de una tabla tiene esta forma (guardada en un fichero apps/ventas/db/venta.php):

namespace ventas\db;

use Vendimia\Database\Tabledef;
use Vendimia\Database\Field;

class venta extends Tabledef
{
var $fecha_creacion = Field::DateTime;

var $tipo = [Field::FixChar, 1,
'index' => true,
];

var $serie = [Field::SmallInt,
'index' => [
'unique' => false,
]
];

var $numero = [Field::Integer,
'index' => [
'unique' => false,
],
];
// ...

Pero esta estructura la he creado completamente aislada del modelo, enfocada únicamente a la base de datos. A parte, los campos son simples constantes, que el motor de la base de dato lo convierte al nombre correspondiente para la base de datos.

El problema #2

Este es un problema pequeño: el concepto de ‘modelo es la base de datos’ en un sistema MVC que hizo popular Django y Rails está mal. Creo que un refactoring del ORM de Vendimia también presenta una oportunidad para colocarlo en otro lado, y las clases dentro de models serían los services y/o los domain objects de un MVC real. A mi parecer, esto sería suficiente separación.

El problema #3

Este problema se llama ‘PHP’. A pesar que sus objetos han mejorado bastante en los últimos años, y son más elaborados que los de Python o Ruby, aun le falta mucha de la flexibilidad de estos dos para implementar cosas interesantes.

Un punto en particular: PHP no tiene setter o getters bonitos como tiene Python (llamados Descriptors), para poder procesar la información que se guarda en cada propiedad a través de un objeto, sin recurrir a métodos mágicos (que es lo que actualmente hago, pero no para procesar, sólo para asignar). Ya hablamos sobre los problemas de los métodos mágicos, y también tengo la intención de liberarme de la mayoría de ellos.

Esto también trae el problema que no puedo definir el objeto que referencia al campo como una variable simple, por que al asignarle un valor después, simplemente se borraría el objeto. Y usar mutators y accessors como getName() no me parecen muy elegantes y/o simples (los dos pilares de Vendimia )

El futuro (con una solución)

Entonces, ahora el ORM de Vendimia estará en su propio namespace, distinto del modelo. El ORM también servirá para obtener los campos para crear la tabla en la base de datos. Usará annotations para definir los parámetros de cada campo. Un prototipo de declaración sería:

namespace products\orm;

use Vendimia\ORM;
use Vendimia\ORM\Record;

class product extends Record
{
/**
* @V:ORM type ORM\Char 8
*/
private $name;

/**
* @V:ORM type ORM\Char 32
* @V:ORM index
*/
private $barcode;

/**
* @V:ORM type ORM\Decimal 8, 2
*/
private $price;

/**
* @V:ORM type ORM\Boolean
* @V:ORM default `True`
*/
private $tax_affected;

/**
* @V:ORM type ORM\Array
* @V:ORM valid_values "PCIE", "USB&lt;2", "FM2"
*/
private $requires;

/**
* @V:ORM type ORM\DateTime
*/
private $create_at;

}

De esta forma, los campos estarán disponibles para autocompletación de las IDEs, para validad los valores que se coloquen desde el código, para formatearlos correctamente cuando vayan o venga desde o hacia la base de datos, y para crear la estructura de la tabla en ella.

El declarar los campos ‘private‘ depende de si al final sigo usando métodos mágicos, o no.

Pros de usar métodos mágicos:

Puedo hacer lazy evaluation, y sólo ejecutar el query cuando se intenta acceder a una propiedad (como funciona hoy). De no usarlos, habría que explícitamente solicitar la obtención de los valores de la base de datos, quizás ejecutando un ->fetch() al final de la cadena de métodos de consulta.

Puedo grabar sólo los campos que han sido modificados. No estoy seguro si eso tiene una ventaja de velocidad, pero en el log de la DB se ve más elegante

Puedo validar en ese instante el valor que se asigna a una variable.

Puedo ejecutar setters personalizados al colocar un valor.

Contras de usar métodos mágicos

Para que funcionen, las propiedades deben ser inaccesibles, por lo que tendría que declararlas como private (como en el ejemplo). Pero si modificas una propiedad en un método dentro de la clase, no se ejecutarán los getters y setters de dicha propiedad.

Por ahora, creo que lo implementaré los getters y setters con métodos mágicos y quizas un método para usarlo dentro de la clase misma.

El tag @V:ORM es el trigger que usará el parser para analizar la annotation y sacar los valores que anteriormente se almacenaban directamente en un array. Será necesario guardar una cache del array generado.

Un problema de esta aproximación es que el elemento ‘type’ debe ser una clase que implemente una interface definida. El problema viene en la resolución del FQCN del mismo, tomando en cuenta el namespace actual, y los aliases definidos con use. El keyword ::class sólo funciona en tiempo de compilación y con nombres de clases (aunque no existan). No existe una función similar para obtener el FQCN de un string en tiempo de ejecución, y no veo una forma de poder implementarlo.

Para solventar ello, se me ocurre buscar una clase dentro de un lugar fijo en Vendimia si el nombre de la clase empieza con ORM\ (ignorando el alias), y usar una clase particular si el nombre empieza con un \.

¿Comentarios? ¿Sugerencias?