Introducción a Doctrine ORM

Explicación y funcionamiento del mapa de objetos relacional Doctrine ORM y sus principales características

Un ORM (Object Relational Mapping) es una técnica de programación que convierte datos entre sistemas de tipos de un lenguaje de programación orientado a objetos y una base de datos relacional. El resultado es una base de datos orientada a objetos que puede usarse desde el lenguaje de programación utilizado: las clases son tablas de la base de datos y los objetos son registros. Así resulta más fácil crear y manipular tablas y datos.

Doctrine Project es un conjunto de librerías PHP entre las que se encuentra suObject-Relational Mapper ORM. Doctrine 1 comenzó en 2006, pero hasta finales de 2010 no se lanzó Doctrine 2, una versión muy mejorada respecto a la anterior. Las entidades en Doctrine 2 son objetos PHP que contienen variables (propiedades) que se guardan y devuelven a una base de datos a través del Entity Manager de Doctrine, una implementación del patrón de mapeador de datos. Su principal característica es la poca configuración que hace falta para empezar un proyecto.

El lector de anotaciones

Doctrine 2 introdujo las anotaciones para especificar la forma en que una clase PHP debía ser mapeada a una base de datos relacional (anteriormente sólo podían utilizarse archivos de configuración en Yaml, XML o PHP).

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class Post
{
    /**
    * @ORM\Column(type="string")
    */
    private $title;
}

Las anotaciones no están escritas en PHP, se analizan mediante un lector de anotaciones desde una aplicación PHP, de esa forma afecta al funcionamiento de una aplicación. Son fáciles de memorizar y escribir, ni siquiera es necesario saber PHP. Es por ello que se considera un DSL (Domain-specific language) o lenguaje específico del dominio ya que permite convertir conceptos complejos en instancias sencillas.

Las anotaciones se forman dentro de doc blocks, que son comentarios que empiezan con /. Las anotaciones regulares se utilizan como documentación del código al que precede** (clases, propiedades, funciones...).

/**
 * @author Diego Lazaro
 */
class PostController
{
/**
 * @param int $id
 * @return array
 */
protected function editAction($id)
{...
}

El lector de anotaciones de Doctrine se salta las anotaciones regulares (que se encuentran en una lista). Cuando la anotación no se encuentra en esa lista, Doctrine la interpreta como una clase. Por ejemplo la anotación @Route:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

    /**
     * @Route("/hello/{nombre}", name="_demo_hello")
     */
    public function helloAction($nombre)
    {
        return array('nombre' => $nombre);
    }

Route es un nombre de clase que viene de la importación de Sensio\Bundle\FrameworkExtraBundle\Configuration\Route en un proyecto Symfony. El lector de anotaciones intentará crear una instancia de la clase y llenarla con los datos entre paréntesis ("/hello/{nombre}", name="_demo_hello"). El objeto que resulta se emplea por el routing loader de Symfony para añadir una ruta al RouteCollection.

Ejemplo de una entidad Producto

Las entidades suelen crearse por cada tabla de la base de datos. En este ejemplo se muestra como sería una tabla sencilla para productos:

/**
 * @Entity @Table(name="productos")
 **/
class Producto
{
    /** @Id @Column(type="integer") @GeneratedValue **/
    protected $id;
    /** @Column(type="string") **/
    protected $nombre;

    public function getId()
    {
        return $this->id;
    }

    public function getNombre()
    {
        return $this->nombre;
    }

    public function setNombre($nombre)
    {
        $this->nombre = $nombre;
    }
}

Los mutadores getters y setters se definen para cada campo salvo para el campo id. Los mutadores permiten a Doctrine hacer llamadas que manipulan las entidades. El campo id no es un campo manipulable ya que es automático.

La anotación @Entity proporciona información acerca de la clase y el nombre de la tabla que representa. @Id con @GeneratedValue indican que es un valor automático de identidad de la base de datos, AUTO INCREMENT en el caso de MySql. @Column señala la existencia de una columna, junto con su argumento type, que indica el tipo de dato: integer, string...

Para que el servicio Entity Manager se entere de que una nueva entidad se debe introducir en la base de datos, se ha de llamar al método persist(). Para que finalmente esta entidad sea creada en la base de datos, se utiliza flush().

$newProductName = $argv[1];

$product = new Product();
$product->setName($newProductName);

$entityManager->persist($product);
$entityManager->flush();

El hecho de que haya dos métodos, persist y flush, permite agregar diferentes artículos por separado en una sola transacción, ejecutada mediante flush().

$productoRepository = $entityManager->getRepository('Producto');
$productos = $productoRepository->findAll();

foreach ($productos as $producto) {
    echo sprintf("-%s\n", $producto->getNombre());
}

El método getRepository() puede crear un objeto finder (llamado repositorio) para cada entidad. Contiene algunos métodos predefinidos como findAll(), pero se puede incluir cualquier método que se quiera para devolver resultados mediante sentencias SQL, DQL...

Para mostrar el nombre de un producto concreto basándose en su id, bastaría con el método find():

$id = $argv[1];
$producto = $entityManager->find('Producto', $id);

if ($producto === null) {
    echo "No se ha encontrado ningún producto.\n";
    exit(1);
}

echo sprintf("-%s\n", $producto->getNombre());

Para modificar un producto hacemos uso del mutador setNombre():

$id = $argv[1];
$nuevoNombre = $argv[2];

$producto = $entityManager->find('Producto', $id);

if ($producto === null) {
    echo "El producto $id no existe.\n";
    exit(1);
}

$producto->setNombre($nuevoNombre);

$entityManager->flush();

El uso de anotaciones es opcional, habiendo un gran debate acerca de si es más conveniente o no. Symfony2 adaptó este sistema para las configuraciones de algunos componentes como Routing o Validator, y es donde se suelen recomendar, además de para definir las entidades.