Continuamos con otra publicación perteneciente a la serie de artículos dedicados a Object-Relational Mapping (ORM). Esta vez nos centraremos en algo más práctico, creando un aplicación de ejemplo usando Doctrine como motor de ORM.

Autor:

Intentando mejorar cada día en este mundo tan cambiante de la programación. Sígueme en Twitter o en GitHub. Actualmente estoy disponible para contratar.
Descarga Demostración

¿Que es Doctrine?

Doctrine es una librería para PHP que nos permite trabajar con un esquema de base de datos como si fuese un conjunto de objetos, y no de tablas y registros. Si no sabes todavía que significa ORM y que ventajas / desventajas tiene, te recomiendo que leas el tutorial anterior: Introducción a Object-Relational Mapping (ORM).

Doctrine está inspirado en Hibernate, que es uno de los ORM más populares y grandes que existen y nos brinda una capa de abstracción de la base de datos muy completa. La característica más importante es que te da la posibilidad de escribir consultas de base de datos en un lenguaje propio llamado Doctrine Query Language (DQL).

Características principales

Doctrine es una librería muy completa y muy configurable, por lo que casi me resulta complicado seleccionar que detalles destacar. Os pongo las características más globales, ya que este tutorial pretende ser una introducción, por lo que dejamos de lado las cosas más “complejas”.

Generación automática del modelo

Cuando se trabaja con ORM, necesitas crear el conjunto de clases que representa el modelo de la aplicación, luego estas clases serán vinculadas al esquema de la base de datos de forma automática con un motor ORM.

Aunque son cosas diferentes, cuando diseñas un modelo relacional y un modelo de clases, suelen ser muy parecidos. Doctrine  se aprovecha de esta similitud y nos permite generar de forma automática el modelo de clases basándose en el modelo relacional de tablas.

Es decir, si tenemos una tabla llamada usuarios, se autogenerará una clase llamada Usuarios cuyas propiedades son las columnas de dicha tabla.

Posibilidad de trabajar con YAML

Como se comenta en el apartado anterior, Doctrine puede generar de forma automática el modelo, pero también deja la posibilidad (como es lógico) que puedas definir tu mismo el mapeo de tablas y sus relaciones.

Esto se puede hacer con código PHP o con YAML, que es un formato de serialización de datos legible por humanos muy usado para este fin. En el ejemplo siguiente se muestra como definir dos tablas relacionadas:

---
User:
  columns:
    username:
      type: string(255)
    password:
      type: string(255)
    contact_id:
      type: integer
  relations:
    Contact:
      class: Contact
      local: contact_id
      foreign: id
      foreignAlias: User
      foreignType: one
      type: one

Contact:
  columns:
    first_name:
      type: string(255)
    last_name:
      type: string(255)
    phone:
      type: string(255)
    email:
      type: string(255)
    address:
      type: string(255)
  relations:
    User:
      class: User
      local: id
      foreign: contact_id
      foreignAlias: Contact
      foreignType: one
      type: one

En el ejemplo, se muestra una relación entre dos tablas a través de los campos User.contact_id y Contact.id.

Doctrine_Record y Doctrine_Table

Practicamente todo nuestro modelo heredará de estas dos clases. Doctrine_Record representa una entidad con sus propiedades (columnas) y nos facilita métodos para insertar, actualizar o eliminar registros entre otros. Por ejemplo:

<php
$user = new User();
$user->name =  “Iván”;
$user->save();

La clase Doctrine_Table representa el esquema de una tabla. A través de esta clase podemos, por ejemplo, obtener información sobre las columnas o buscar registros específicos:

<?php
$userTable =  Doctrine_Core::getTable('Users');
$user = $userTable->find(1);

Buscadores mágicos (Magic finders)

En Doctrine, puedes buscar registros basándote en cualquier campo de una tabla. En el partado anterior podíamos ver que existe el método find() para buscar un determinado id en una tabla…pues bien, se podría hacer findByX() en donde X es el nombre de la columna.

Si existen los campos llamados name y email, podemos hacer findByName() y findByEmail(). Tambien es importante decir que existe el método findAll(), que obtiene todos los registros de la tabla.

Relaciones entre entidades

En Doctrine, una vez que hemos definido nuestro modelo (o se ha creado de forma automática) con las tablas y sus relaciones, resulta fácil acceder y moverse por entidades relacionadas.

Vamos a ver un ejemplo de como acceder a valores que en nuestro sistema relacional se encuentran en tablas separadas. Teniendo en cuenta que tenemos dos tablas, User y Comments, con relación 1-N:

<?php
$commentsTable =  Doctrine_Core::getTable('Comments');
$comments =  $commentsTable->findAll();
foreach($comments as $comment){
	echo  $comment->User->name;
}

A través de $comment->User estamos accediendo a un nuevo objeto del tipo User que se carga de forma dinámica, de esta forma podemos acceder a las propiedades y métodos de dicha clase olvidándonos de tener que lanzar código SQL.

Lenguaje DQL

DQL es un lenguaje creado para ayudar al programador a extraer objetos de la base de datos. Entre las ventajas de usar este lenguaje se encuentran:

  • Está diseñado para extraer objetos, no filas, que es lo que nos interesa.
  • Entiende las relaciones, por lo que no es necesario escribir los joins a mano.
  • Portable con diferentes bases de datos.

Es importante considerar el uso de DQL para obtener la información a cargar en lugar de usar la “forma automática” de Doctrine para mejorar el rendimiento. En el ejemplo anterior, cuando se accede a $comment->User, hemos dicho que se está cargando un nuevo objeto de forma dinámica, pues bien, esto no es óptimo porque realiza consultas SQL de más.

Si tenemos 100 comentarios, ejecutará 100 consultas SQL para cargar la información del objeto User, lo cual es una pérdida de rendimiento enorme. En lugar de eso, es necesario usar DQL para que traiga toda la información de un solo paso:

<?php
$q =  Doctrine_Query::create()
	->select('c.*,u.name')
	->from('Comments c')
	->innerJoin('c.User u');
$comments =  $q->execute();

El código anterior obtendrá la información de los comentarios cruzados con los datos de usuario, por lo que obtendrá toda la información en un solo paso.

El lenguaje DQL es algo tan extenso que necesitaría varios artículos para explicarlo en profundidad, por eso te recomiendo que mires la documentación oficial que está bien explicado y completo :)

Construyendo un ejemplo real

Para darle algo de vida al tutorial, vamos a crear una implementación muy sencilla de Doctrine en la que veremos todo lo comentado hasta ahora. La aplicación consiste en un listado de comentarios escritos por usuarios.

Cada vez que se desee insertar un nuevo comentario, se deberá rellenar un formulario con: nombre, e-mail y texto, siendo los dos últimos obligatorios. Si el usuario no deja su nombre, se mostrará como “Desconocido”.

Paso 1: crear el esquema de base de datos

El ejemplo es tan sencillo que solamente tendrá dos tablas relacionadas entre sí: users y users_comments.

CREATE SCHEMA  ontuts_doctrine CHARSET UTF8 COLLATE utf8_general_ci;
use ontuts_doctrine;
CREATE TABLE users(
	id INT(11) PRIMARY  KEY auto_increment,
	name VARCHAR(30),
	email VARCHAR(60)
)ENGINE=INNODB;
CREATE TABLE  users_comments(
	id INT(11) PRIMARY KEY auto_increment,
	id_user INT(11)  NOT NULL,
	text VARCHAR(255),
CONSTRAINT fk_users_comments_users  FOREIGN KEY (id_user) REFERENCES users(id)
)ENGINE=INNODB;

Paso 2: crear el modelo

Esto, como ya habrás aprendido, se puede hacer de forma manual (con PHP o YAML) y de forma automática. En este caso lo haremos de forma automática para explicar como funciona. Vamos a crear el script que se encargará de generar los ficheros, create_orm_model.php:

<?php
require_once(dirname(__FILE__)  . '/lib/Doctrine.php');
spl_autoload_register(array('Doctrine',  'autoload'));
$conn  =  Doctrine_Manager::connection('mysql://root:password@localhost/ontuts_doctrine',  'doctrine');
Doctrine_Core::generateModelsFromDb('models',  array('doctrine'), array('generateTableClasses' => true));

Nos aseguramos de que existe la carpeta models en la raíz de nuestro proyecto:

mkdir  /path/myproject/models

Ahora llamamos al script, bien vía Apache o vía CLI:

php5  -f create_orm_model.php

Una vez se ha ejecutado con éxito, vamos a la carpeta models y vemos que se han generado varios ficheros:

  • Users.php
  • UsersTable.php
  • UserComments.php
  • UserCommentsTable.php
  • generated/BaseUsers.php
  • generated/BaseUsersComments.php

Si te fijas, se han creado las clases que se extienden de Doctrine_Record y Doctrine_Table que ya hemos comentado antes.

BaseUsers.php y BaseUsersComments.php representan clases base y que no deberías modificar. Cualquier método o propiedad que desees añadir, debes hacerlo sobre Users.php o UsersComments.php.

Paso 3: extender el modelo

Analizando la aplicación llegamos a la conclusión de que la clase Users necesita dos nuevos métodos:

  • getName: que devuelva el nombre del usuario o “Desconocido” si no ha rellenado esa información.
  • addComment: que inserte un nuevo comentario asignado a dicho usuario.

Editamos entonces el fichero Users.php y lo dejamos así:

<?php
class Users extends  BaseUsers
{
	public function addComment($text){
		$comment = new UsersComments();
		$comment->id_user = $this->id;
		$comment->text = $text;
		$comment->save();
	}
	public function  getName(){
		if(empty($this->name) || is_null($this->name)){
			return  "Desconocido";
		}else{
			return $this->name;
		}
	}
}

Tambien necesitamos extender la clase UsersCommentsTable para sobreescribir el método findAll(), de forma que obtenga la información cruzada con la tabla users y así mejorar el rendimiento. Editamos el fichero UsersCommentsTable.php y lo dejamos así:

<?php
class  UsersCommentsTable extends Doctrine_Table
{
	/**
	* Returns an  instance of this class.
	*
	* @return object  UsersCommentsTable
	*/
	public static function getInstance()
	{
		return  Doctrine_Core::getTable('UsersComments');
	}
	public function  findAll($hydrationMode = null){
		$q = Doctrine_Query::create()
			->select('c.*,u.name')
			->from('UsersComments c')
			->innerJoin('c.Users u');
		return $q->execute();
	}
}

Listo, ya tenemos nuestro modelo ampliado :) .

Paso 4: crear la lógica

A continuación creamos el fichero index.php que contendrá la lógica principal de la aplicación. Nos queda algo así: index.php

<?php
//Carga Doctrine
require_once(dirname(__FILE__)  . '/lib/Doctrine.php');
spl_autoload_register(array('Doctrine',  'autoload'));
$conn =  Doctrine_Manager::connection('mysql://root@localhost/ontuts_doctrine',  'doctrine');
$conn->setCharset('utf8');
Doctrine_Core::loadModels('models');

//Variable en la  plantilla html
$tpl = array("comments"=> array(), "error"=>false);

//Comprueba si se ha  enviado el formulario
if(!empty($_POST) &&  isset($_POST['create_comment'])){
	$email = filter_input(INPUT_POST,  "email", FILTER_SANITIZE_STRING);
	$name = filter_input(INPUT_POST,  "name", FILTER_SANITIZE_STRING);
	$text = filter_input(INPUT_POST,  "text", FILTER_SANITIZE_STRING);
	//Comprueba que se hayan rellenado  los campos obligatorios
	if(!empty($email) &&  !is_null($email) &&
	!empty($text) &&  !is_null($text)){
		$userTable = Doctrine_Core::getTable('Users');
		$users =  $userTable->findByEmail($email);
		$user = null;
		//Si el  usuario no existe, lo crea
		if($users->count()==0){
			$user =  new Users();
			$user->name = $name;
			$user->email = $email;
			$user->save();
		}else{
			$user =  $users[0];
		}
		//Inserta el comentario
		$user->addComment($text);
	}else{
		//Si no se se  han rellenado todos los valores obligatorios
		//mostrará un  error
		$tpl['error'] = true;
	}
}
//Carga los comentarios
$commentsTable =  Doctrine_Core::getTable('UsersComments');
$tpl['comments'] =  $commentsTable->findAll();
//Envia la información
require_once('template.phtml');

Paso 5: crear la plantilla

Por último ya solo nos queda dar formato visual al contenido de template.phtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML  1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html  xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta  http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<h2>Añadir  comentario</h2>
<?if($tpl['error']):?><p>Por  favor, rellena las cajas marcadas con asterisco  (*)</p><?endif?>
<form action="index.php"  method="post">
<label>Dirección e-mail  *</label><br/>
<input type="text"  name="email"/><br/>
<label>Nombre</label><br/>
<input  type="text" name="name"/><br/>
<label>Comentario *</label><br/>
<textarea  name="text" cols="80" rows="5"></textarea><br/>
<input  type="submit" name="create_comment" value="Enviar"/>
</form>
<h2>Comentarios de los usuarios</h2>
<ul>
<?foreach($tpl['comments'] as $comment):?>
<li><a  href="mailto:<?=$comment->Users->email?>"><?=$comment->Users->getName()?></a>  - <?=$comment->text?></li>
<?endforeach?>
</ul>
</body>
</html>

Reflexión final

Como has visto, en cuestión de minutos tenemos una mini aplicación en PHP que nos permite añadir y listar registros de una base de datos de una forma muy ordenada y sin tocar ni una letra de código SQL.

No llevaría mucho tiempo añadir un sistema de login de usuarios y que pudiesen editar y borrar los comentarios, gracias a los métodos save() y delete() de la clase Doctrine_Record.

¡Nos vemos en la próxima publicación!

¿Necesitas desarrollar un proyecto web o para móviles? ¡Estamos disponibles!

Visitar Cokidoo

Cokidoo, los creadores de Ontuts, desarrollamos proyectos tecnológicos centrados en redes sociales y aplicaciones web, aplicaciones móviles y consultoría web y bases de datos.

Somos jóvenes, inquietos, versátiles, apasionados por la innovación y enfocados en las nuevas tecnologías. Con Ontuts tratamos de compartir nuestro conocimiento adquirido en los distintos proyectos, ayudando a la comunidad y mostrando nuestra capacidad tecnológica.

Si necesitas un presupuesto sin compromiso, estamos disponibles, no dudes en contactar con nosotros.

Comentarios en esta publicación (7 comentarios)

¿Te ha gustado esta publicación? ¡Puedes compartir tu opinión con todos nosotros! Simplemente pincha aquí mismo.

Hola
muy interesante tu Post!!
me sirve mucho est ateoria y algo de practica pues estoy entrando al mundo de symfony y esto me valdra mucho!

gracias por postear este tutorial

atte marcos velasquez

Alberto

Es para la versión 1 o 2 de Doctrine?

Jorge Leguia

Muy bueno tu post.
Está bien elaborado y es objetivo.

Gracias y Ojalá cuelgues una Aplicación practica igual utilizando Symfony o Zend.

Este ejemplo lo probe con Doctrine 1.2 y me funcionó.
En cuanto al Doctrine 2.0, solo trabaja con PHP 5.3.+ y estos ejemplos no funcionan ya que la estructura de archivos es otra, etc.

Saludos :)

Rafael

Excelente trabajo, estoy empezando con CodeIgniter y este material es muy valioso.

Gracias

Saludos :D

madeley

hola quiero hacer una consulta donde trabajo hay un sistema que se va a eliminar para crear uno nuevo me estan pidiendo que use doctrine no conozco de esa herramienta asi que tengo que estudiar quiero saber si se puede trabajar con doctrine sin utilizar framework como symfony por ejemplo osea doctrine solo y como se trabajaria? con que herramienta puedo codificar que sea amigable y que acepte doctrine por ejemplo algo como eclipse me han hablado de netbean pero no c mucho de el me puedes ayudar

Lucas

Te agradezco por la info, siempre viene muy bien algo de teoría bien explicada. Uso symfony desde hace 6 meses y cada día me convence más.

Gracias!