Modelo de información
Introducción
Yupp Framework implementa el patrón de diseño Model-View-Controller (MVC). El corazón de cualquier aplicación Yupp es el Model, que es la capa donde se implementa la información persistente de la aplicación.El modelo de información de una aplicación se implementa mediante clases, con sus atributos, y relaciones entre estas clases, siguiendo el paradigma de Programación Orientada a Objetos. Yupp Framework soporta todo tipo de relaciones entre clases, ya sean relaciones de herencia, o asociaciones. A su vez, las asociaciones pueden ser de cualquier cardinalidad y dirección.
En esta sección se comentan estos y otros aspectos, y se incluyen múltiples ejemplos en código PHP.
Definición de clases del modelo
A continuación se muestra la definición de una clase del modelo de información de una aplicación:
<?php
class Usuario extends PersistentObject {
function __construct($args = array (), $isSimpleInstance = false)
{
$this->setWithTable("usuarios");
$this->addAttribute("nombre", Datatypes :: TEXT);
$this->addAttribute("email", Datatypes :: TEXT);
$this->addAttribute("clave", Datatypes :: TEXT);
$this->addAttribute("edad", Datatypes :: INT_NUMBER);
$this->addAttribute("fechaNacimiento", Datatypes :: DATE);
$this->addConstraints("nombre", array (
Constraint :: minLength(1),
Constraint :: maxLength(30),
Constraint :: blank(false)
));
$this->addConstraints("clave", array (
Constraint :: minLength(5)
));
$this->addConstraints("edad", array (
Constraint :: between(10, 100)
));
$this->addConstraints("email", array (
Constraint :: email(),
));
parent :: __construct($args, $isSimpleInstance);
}
// Métodos estáticos omitidos
// ...
}
?>
Este es un ejemplo reducido de la decaración de una clase del modelo de información. Para ver ejemplos
completos puede descargar alguna de las aplicaciones de prueba
y ver ejemplos completos.
Explicación del código
Todas las clases del modelo heredan de PersistentObject, que es la clase que implementa toda la lógica necesaria para persistencia del modelo de información mediante ORM. La implementación de ORM de Yupp se llama YORM.Mediante el método setWithTable($tableName), se le indica al YORM que las instancias de esta clase se almacenarán en la tabla $tableName de una base de datos relacional. Si no se llama a setWithTable(), por defecto el nombre de la tabla será igual al nombre de la clase en minúsculas.
Mediante el método addAttribute($attrName, $datatype), se le indica al YORM que la clase tiene un atributo llamado $attrName que tiene tipo $datatype. Esto sirve para definir las columnas, con su respectivo tipo, de la tabla donde se guardan las instancias de la clase.
Mediante el método addConstraint($attrName, $constraintList), se le indica al YORM que para el atributo $attrName, se deben cumplir todas las restricciones que aparezcan en la lista de restricciones $constraintList. Estas restricciones son verificadas antes de guardar las instancias de la clase en la base de datos. Si en la verificación, alguna restricción es violada por el valor de un atributo, se genera un mensaje de error, y la instancia no se almacena en la base de datos.
Para los nombres de los atributos se debe tener especial cuidado en no usar nombres
que sean palabras reservadas en el DBMS seleccionado, por ejemplo el atributo "end" será
fuente de problemas en PosgreSQL pero no en MySQL. Aquí se encuentran las referencias a palabras
reservadas de los DBMS soportados por Yupp Framework:
Generación de tablas
Yupp framework genera la tabla donde se guadarán las instancias de la clase Usuario. El SQL que genera YORM para generar la tabla es el siguiente:
CREATE TABLE usuarios (
id INT(11) DEFAULT 1 PRIMARY KEY,
nombre VARCHAR(30) NULL,
email TEXT NULL,
clave TEXT NULL,
edad INT(11) NULL,
fechaNacimiento DATE NULL,
class TEXT NOT NULL,
deleted BOOL NOT NULL
);
Explicación del código SQL
Las columnas id, class y deleted no aparecen definidas como atributos en la clase Usuario, sin embargo se generan como columnas en la tabla. Esto se debe a que son atributos inyectados por la clase PersistentObject para simplificarle el trabajo al YORM.Para la columna "nombre", vemos que se generó una restricción del VARCHAR a 30 caracteres, esto se debe a que el atributo nombre de la clase Usuario tiene una restricción maxLength(30). Por lo tanto, las restricciones sobre atributos de una clase pueden afectar la forma en la que se generan las tablas.
El atributo id es el identificador de la instancia en la base de datos. El atributo class indica la clase de la instancia, en este caso class tendrá el valor "Usuario". Y por último el atributo deleted es utilizado para realizar borrados lógicos.
Relaciones entre clases
YORM soporta la definición de relaciones de distintas cardinalidades entre clases del modelo de datos. La cardinalidad indica cuantas instancias de la clase relacionada se pueden tener asociadas.Las tres posibilidades de cardinalidad de las relaciones son: 1-1, 1-N y N-N. Y las relaciones pueden ser unidireccionales o bidireccionales, en la siguiente imagen se muestran todas las combinaciones que soporta Yupp PHP Framework para las relaciones entre clases:
Yupp PHP Framework soporta salvar un modelo en cascada, esto quiere decir que si se salva
una sola instancia de una clase, se salvan también las instancias de las clases asociadas.
Para poder hacer esto, Yupp necesita saber cual es la clase "fuerte" en la relación.
Yupp posee una forma de declarar explícitamente cual es la parte fuerte de la relación,
esto es a través del atributo "belongsTo" en las clases del modelo, que indica que la clase
pertenece a otra mediante una asociación, y esta última es la parte fuerte de esa
asociación. No siempre es necesario declarar ese atributo, en los casos que no se declara
Yupp Framework sigue ciertas reglas para determinar la parte fuerte de la relació, aquí
se listan todas las reglas:
- Si la relacion es A (1) => (1) B, entonces B belongsTo A.
- Si la relacion es A (1) <=> (1) B, se necesita belongsTo para saber cual es el lado fuerte.
- Si la relacion es A (1) => (*) B, entonces B belongsTo A.
- Si la relacion es A (1) <=> (*), B entonces B belongsTo A.
- Si la relacion es A (*) => (*) B, entonces B belongsTo A.
- Si la relacion es A (*) <=> (*) B, se necesita belongsTo para saber cual es el lado fuerte.
Definiendo relaciones "hasOne"
El siguiente ejemplo muestra una clase que modela las entradas en un blog. Cada entrada es creada por un usuario.
class Entrada extends PersistentObject
{
function __construct($args = array (), $isSimpleInstance = false)
{
$this->setWithTable("entradas");
$this->addAttribute("texto", Datatypes :: TEXT);
$this->addAttribute("fecha", Datatypes :: DATETIME);
$this->addHasOne("usuario", "Usuario"); // Usuario que creó la entrada
$this->addConstraints("texto", array (
Constraint :: minLength(10),
Constraint :: maxLength(1000),
Constraint :: blank(false)
));
parent :: __construct($args, $isSimpleInstance);
}
// Métodos estáticos omitidos
// ...
}
Explicación del código
En general la definición de la clase es análoga a la vista previamente para Usuario, la única diferencia es que en la clase Entrada se invoca al método addHasOne($attrName, $class), que sirve para definir relaciones x => 1, donde x puede ser 1 o N. En este caso es una relación unidireccional N => 1, porque cada instancia de Entrada tiene una instancia de Usuario, pero una misma instancias de Usuario podría ser referenciada desde más de una instancia de EntradaUn ejemplo de como se utiliza el modelo es el siguiente:
// Crea una instancia de Usuario y establece valores de sus atributos
$usuario = new Usuario( array(
"nombre" => "Pablo",
"email" => "a@b.com",
"clave" => "qwerty",
"edad" => 29,
"fechaNacimiento" => "1981-10-24"
));
// Guarda la instancia de Usuario en la base de datos
// Si falla alguna restricción, save() retorna false
// getErrors() indica qué errores de validación ocurrieron
if (!$usuario->save())
{
print_r( $usuario->getErrors() );
}
// Crea una instancia de Entrada
$entrada = new Entrada( array(
"texto"=>"un texto largo sobre ...",
"fecha"=>"2011-02-26 13:43:38"
));
// Asocia el usuario a la entrada
$entrada->setUser( $usuario );
// Intenta guardar la instancia de Entrada en la base de datos
// Si no pasan las restricciones, muestra errores
if (!$entrada->save())
{
print_r( $entrada->getErrors() );
}
Definiendo relaciones "hasMany"
Siguiendo con el ejemplo de la aplicación "blog", ahora tenemos una clase EntradaBlog que especializa a Entrada, y que puede tener varias instancias de Comentario asociados:
class EntradaBlog extends Entrada
{
function __construct($args = array (), $isSimpleInstance = false)
{
$this->setWithTable("entradas_blog");
$this->addAttribute("titulo", Datatypes :: TEXT);
$this->addHasMany("comentarios", 'Comentario');
$this->addConstraints("titulo", array (
Constraint :: maxLength(24),
Constraint :: minLength(5)
));
parent :: __construct($args, $isSimpleInstance);
}
// Métodos estáticos omitidos
// ...
}
class Comentario extends Entrada {
function __construct( $args = array(), $isSimpleInstance = false )
{
parent::__construct( $args, $isSimpleInstance );
}
// Métodos estáticos omitidos
// ...
}
Explicación del código
La definición de la clase EntradaBlog es similar a las definiciones previas de Entrada y Usuario, salvo dos diferencias. La primera, hereda de otra clase persistente, no de PersistentObject directamente. La segunda es que utiliza el método addHasMany($attrName, $class) para declarar una relación 1 => N desde la clase EntradaBlog a la clase comentario Comentario.A continuación se muestra un ejemplo donde se utiliza la relación hasMany para asociar instancias de EntradaBlog a instancias de Comentario:
$com1 = new Comentario( array(
'texto' => 'Este es un comentario para una entrada del blog'
));
$com2 = new Comentario( array(
'texto' => 'Este es otro comentario para la misma entrada del blog'
));
$ent = new EntradaBlog( array(
'texto' => 'Texto de la entrada del blog',
"fecha"=>"2011-02-26 13:43:38"
));
// Asocia los comentarios a la entrada del blog
$ent->addToComentarios( $com1 );
$ent->addToComentarios( $com2 );
// Intenta salvar la entrada, si falla imprime errores
if (!$ent->save())
{
print_r( $ent->getErrors() );
}
Explicación del código
El código de arriba muestra la creación de dos instancias de Comentario y una de EntradaBlog, luego asocia los comentarios a la entrada e intenta salvar la entrada. Podemos destacar que las instancias de Comentario no se están salvando explícitamente, esto se debe a que se salvan en cascada cuando se intenta salvar la instancia de EntradaBlog.Ejemplos de uso del YORM:
Cargar un usuario
// Pedir el usuario con identificador $id $id = ...; $usuario = Usuario::get( $id ); // Muestra el nomrbe del usuario echo $usuario->getNombre();
Borrado físico de un usuario
// Pedir el usuario con identificador $id $id = ...; $usuario = Usuario::get( $id ); // Borrado físico // Si se invoca con el parámetro true, hace borrado lógico $usuario->delete();
Listar entradas
// Parámetros de paginación, se piden 10 registros empezando de 0.
$params = new ArrayObject( array("offset" => 0, "max" => 10) );
// Carga las entradas de la base de datos
// Se obtiene una lista de instancias de Entrada
$list = Entrada::listAll( $params );
// Muestra los textos de las entradas
foreach ( $list as $entrada )
{
echo $entrada->getTexto() . "< br/>";
}
Verificación de errores en un atributo
// Crea una instancia de Usuario y establece valores de sus atributos
$usuario = new Usuario( array(
"nombre" => "Pablo",
"email" => "a@b.com",
"clave" => "qwerty",
"edad" => 29,
"fechaNacimiento" => "1981-10-24"
));
// Guarda la instancia de Usuario en la base de datos
// Si falla alguna restricción, save() retorna false
if (!$usuario->save())
{
// Hay errores para el campo 'nombre'?
if ($usuario->hasFieldErrors('nombre'))
{
// Muestra solo los errores del cambpo 'nombre'
print_r( $usuario->getFieldErrors('nombre') );
}
}
Quitando objetos de relacion hasMany
Supongamos que tenemos una instancia de EntradaBlog con 2 instancias de Comentario asociadas, y queremos quitar de la relación uno de los comentarios.
// Carga la EntradaBlog de la base de datos
// No carga los comentarios asociados (se hace carga perezosa o "lazy loading")
$id = ...;
$ent = EntradaBlog::get( $id );
// Obtengo la lista de comentarios
// Son cargados desde la base de datos
$comentarios = $ent->getComentarios();
// Si hay más de un comentario, quita el primero
if ( count($comentarios) > 1 )
{
// No elimina al comentario de la base, solo lo desasocia de la entrada
$ent->removeFromComentarios( $comentarios[0] );
}
