Bases de datos y Doctrine

Seamos realistas, una de las tareas más comunes y desafiantes para cualquier aplicación involucra la persistencia y lectura de información hacia y desde una base de datos. Afortunadamente, Symfony viene integrado con Doctrine, una biblioteca, cuyo único objetivo es dotarte de poderosas herramientas para facilitarte eso. En este capítulo, aprenderás la filosofía básica detrás de Doctrine y verás lo fácil que puede ser trabajar con una base de datos.

Nota

Doctrine está totalmente desconectado de Symfony y utilizarlo es opcional. Este capítulo trata acerca del ORM de Doctrine, el cual te permite asociar objetos a una base de datos relacional (tal como MySQL, PostgreSQL o Microsoft SQL). Si prefieres utilizar las consultas de base de datos en bruto, es fácil y se explica en el artículo «Cómo utiliza Doctrine la capa DBAL» del recetario.

También puedes persistir tus datos en MongoDB utilizando la biblioteca ODM de Doctrine. Para más información, lee la documentación en «DoctrineMongoDBBundle».

Un sencillo ejemplo: Un producto

La forma más fácil de entender cómo funciona Doctrine es verlo en acción. En esta sección, configurarás tu base de datos, crearás un objeto Producto, lo persistirás en la base de datos y lo recuperarás de nuevo.

Configurando la base de datos

Antes de comenzar realmente, tendrás que configurar tu información de conexión a la base de datos. Por convención, esta información se suele configurar en el archivo app/config/parameters.yml:

# app/config/parameters.yml
parameters:
    database_driver:    pdo_mysql
    database_host:      localhost
    database_name:      proyecto_de_prueba
    database_user:      nombre_de_usuario
    database_password:  password

# ...

Nota

Definir la configuración a través de parameters.yml sólo es una convención. Los parámetros definidos en este archivo son referidos en el archivo de configuración principal al configurar Doctrine:

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
            driver:   "%database_driver%"
            host:     "%database_host%"
            dbname:   "%database_name%"
            user:     "%database_user%"
            password: "%database_password%"
    
  • XML
    <!-- app/config/config.xml -->
    <doctrine:config>
        <doctrine:dbal
            driver="%database_driver%"
            host="%database_host%"
            dbname="%database_name%"
            user="%database_user%"
            password="%database_password%"
        >
    </doctrine:config>
    
  • PHP
    // app/config/config.php
    $configuration->loadFromExtension('doctrine', array(
        'dbal' => array(
            'driver'   => '%database_driver%',
            'host'     => '%database_host%',
            'dbname'   => '%database_name%',
            'user'     => '%database_user%',
            'password' => '%database_password%',
        ),
    ));
    

Al separar la información de la base de datos en un archivo independiente, puedes mantener fácilmente diferentes versiones del archivo en cada servidor. Además, fácilmente puedes almacenar la configuración de la base de datos (o cualquier otra información sensible) fuera de tu proyecto, posiblemente dentro de tu configuración de Apache, por ejemplo. Para más información, consulta Cómo configurar parámetros externos en el contenedor de servicios.

Ahora que Doctrine conoce tu base de datos, posiblemente tenga que crear la base de datos para ti:

$ php app/console doctrine:database:create

Nota

Si quieres usar SQLite como tu base de datos, debes especificar la ruta a donde se debería almacenar tu archivo de base de datos:

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
            driver: pdo_sqlite
            path: "%kernel.root_dir%/sqlite.db"
            charset: UTF8
    
  • XML
    <!-- app/config/config.xml -->
    <doctrine:config
        driver="pdo_sqlite"
        path="%kernel.root_dir%/sqlite.db"
        charset="UTF-8"
    >
        <!-- ... -->
    </doctrine:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'driver'  => 'pdo_sqlite',
            'path'    => '%kernel.root_dir%/sqlite.db',
            'charset' => 'UTF-8',
        ),
    ));
    

Creando una clase Entidad

Supongamos que estás construyendo una aplicación donde necesitas mostrar tus productos. Sin siquiera pensar en Doctrine o en una base de datos, ya sabes que necesitas un objeto Producto para representar los productos. Crea esta clase en el directorio Entity de tu paquete AcmeStoreBundle:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;

class Product
{
    protected $name;

    protected $price;

    protected $description;
}

La clase —a menudo llamada «entidad», es decir, una clase básica que contiene datos— es simple y ayuda a cumplir con el requisito del negocio de productos que necesita tu aplicación. Sin embargo, esta clase no se puede guardar en una base de datos —es sólo una clase PHP simple.

Truco

Una vez que aprendas los conceptos detrás de Doctrine, puedes dejar que Doctrine cree clases de entidad simples por ti:

$ php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"

Agregando información de asignación

Doctrine te permite trabajar con bases de datos de una manera mucho más interesante que solo recuperar filas de una tabla basada en columnas de un arreglo. En cambio, Doctrine te permite persistir objetos completos a la base de datos y recuperar objetos completos desde la base de datos. Esto funciona asociando una clase PHP a una tabla de la base de datos, y las propiedades de esa clase PHP a las columnas de la tabla:

../_images/doctrine_image_1_es.png

Para que Doctrine sea capaz de hacer esto, sólo hay que crear «metadatos», o la configuración que le dice a Doctrine exactamente cómo debe asociar la clase Producto y sus propiedades a la base de datos. Estos metadatos se pueden especificar en una serie de diferentes formatos, incluyendo YAML, XML o directamente dentro de la clase Producto a través de anotaciones:

  • Annotations
    // src/Acme/StoreBundle/Entity/Product.php
    namespace Acme\StoreBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity
     * @ORM\Table(name="product")
     */
    class Product
    {
        /**
         * @ORM\Id
         * @ORM\Column(type="integer")
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        protected $id;
    
        /**
         * @ORM\Column(type="string", length=100)
         */
        protected $name;
    
        /**
         * @ORM\Column(type="decimal", scale=2)
         */
        protected $price;
    
        /**
         * @ORM\Column(type="text")
         */
        protected $description;
    }
    
  • YAML
    # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
    Acme\StoreBundle\Entity\Product:
        type: entity
        table: product
        id:
            id:
                type: integer
                generator: { strategy: AUTO }
        fields:
            name:
                type: string
                length: 100
            price:
                type: decimal
                scale: 2
            description:
                type: text
    
  • XML
    <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                        http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="Acme\StoreBundle\Entity\Product" table="product">
            <id name="id" type="integer" column="id">
                <generator strategy="AUTO" />
            </id>
            <field name="name" column="name" type="string" length="100" />
            <field name="price" column="price" type="decimal" scale="2" />
            <field name="description" column="description" type="text" />
        </entity>
    </doctrine-mapping>
    

Nota

Un paquete sólo puede aceptar un formato para definir metadatos. Por ejemplo, no es posible mezclar metadatos para la clase Entidad definidos en YAML con definidos en anotaciones PHP.

Truco

El nombre de la tabla es opcional y si la omites, será determinada automáticamente basándose en el nombre de la clase entidad.

Doctrine te permite elegir entre una amplia variedad de diferentes tipos de campo, cada uno con sus propias opciones. Para obtener información sobre los tipos de campo disponibles, consulta la sección Referencia de tipos de campo Doctrine.

Ver también

También puedes consultar la `Documentación de asociación básica`_ de Doctrine para todos los detalles sobre la información de asignación. Si utilizas anotaciones, tendrás que prefijar todas las anotaciones con ORM\ (por ejemplo, ORM\Column(..)), lo cual no se muestra en la documentación de Doctrine. También tendrás que incluir la declaración use Doctrine\ORM\Mapping as ORM; la cual importa el prefijo ORM de las anotaciones.

Prudencia

Ten cuidado de que tu nombre de clase y propiedades no estén asignados a un área protegida por palabras clave de SQL (tal como group o user). Por ejemplo, si el nombre de clase de tu entidad es group, entonces, de manera predeterminada, el nombre de la tabla será group, lo cual provocará un error en algunos motores SQL. Consulta la Documentación de palabras clave reservadas por SQL para que sepas cómo escapar correctamente estos nombres. Alternativamente, si estás en libertad de elegir el esquema de tu base de datos, simplemente asigna un diferente nombre de tabla o columna. Ve las Clases persistentes y la Asignación de propiedades en la documentación de Doctrine.

Nota

Cuando utilizas otra biblioteca o programa (es decir, Doxygen) que utiliza anotaciones, debes colocar la anotación @IgnoreAnnotation en la clase para indicar que se deben ignorar las anotaciones Symfony.

Por ejemplo, para evitar que la anotación @fn lance una excepción, añade lo siguiente:

/**
 * @IgnoreAnnotation("fn")
 */
class Product
// ...

Generando captadores y definidores

A pesar de que Doctrine ahora sabe cómo persistir en la base de datos un objeto Producto, la clase en sí realmente no es útil todavía. Puesto que Producto es sólo una clase PHP regular, es necesario crear métodos captadores y definidores (por ejemplo, getName(), setName()) para poder acceder a sus propiedades (ya que las propiedades son protegidas). Afortunadamente, Doctrine puede hacer esto por ti con la siguiente orden:

$ php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product

Esta orden se asegura de que se generen todos los captadores y definidores para la clase Producto. Esta es una orden segura — la puedes ejecutar una y otra vez: sólo genera captadores y definidores que no existen (es decir, no sustituye métodos existentes).

Prudencia

Ten en cuenta que el generador de entidades de Doctrine produce captadores/definidores sencillos. Debes revisar las entidades generadas y ajustar a tus propias necesidades la lógica de los captadores/definidores.

También puedes generar todas las entidades conocidas (es decir, cualquier clase PHP con información de asignación Doctrine) de un paquete o un espacio de nombres completo:

$ php app/console doctrine:generate:entities AcmeStoreBundle
$ php app/console doctrine:generate:entities Acme

Nota

A Doctrine no le importa si tus propiedades son protegidas o privadas, o si una propiedad tiene o no una función captadora o definidora. Aquí, los captadores y definidores se generan sólo porque los necesitarás para interactuar con tu objeto PHP.

Creando tablas/esquema de la base de datos

Ahora tienes una clase Producto utilizable con información de asignación de modo que Doctrine sabe exactamente cómo persistirla. Por supuesto, en tu base de datos aún no tienes la tabla producto correspondiente. Afortunadamente, Doctrine puede crear automáticamente todas las tablas de la base de datos necesarias para cada entidad conocida en tu aplicación. Para ello, ejecuta:

$ php app/console doctrine:schema:update --force

Truco

En realidad, esta orden es increíblemente poderosa. Esta compara cómo se debe ver tu base de datos (en base a la información de asignación de tus entidades) con la forma en que realmente se ve, y genera las declaraciones SQL necesarias para actualizar la base de datos a su verdadera forma. En otras palabras, si agregas una nueva propiedad asignando metadatos a Producto y ejecutas esta tarea de nuevo, vas a generar la declaración alter table necesaria para añadir la nueva columna a la tabla Producto existente.

Una forma aún mejor para tomar ventaja de esta funcionalidad es a través de las migraciones, las cuales te permiten generar estas instrucciones SQL y almacenarlas en las clases de la migración, mismas que puedes ejecutar sistemáticamente en tu servidor en producción con el fin de seguir la pista y migrar el esquema de la base de datos segura y fiablemente.

Tu base de datos ahora cuenta con una tabla producto completamente operativa, con columnas que coinciden con los metadatos que has especificado.

Persistiendo objetos a la base de datos

Ahora que tienes asignada una entidad Producto y la tabla Producto correspondiente, estás listo para persistir los datos a la base de datos. Desde el interior de un controlador, esto es bastante fácil. Agrega el siguiente método al DefaultController del paquete:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/Acme/StoreBundle/Controller/DefaultController.php

// ...
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

public function createAction()
{
    $product = new Product();
    $product->setName('A Foo Bar');
    $product->setPrice('19.99');
    $product->setDescription('Lorem ipsum dolor');

    $em = $this->getDoctrine()->getManager();
    $em->persist($product);
    $em->flush();

    return new Response('Created product id '.$product->getId());
}

Nota

Si estás siguiendo este ejemplo, tendrás que crear una ruta que apunte a esta acción para verla trabajar.

Veamos detenidamente el ejemplo anterior:

  • líneas 9-12 En esta sección, creas una instancia y trabajas con el objeto $product como con cualquier otro objeto PHP normal.
  • línea 14 Esta línea consigue un objeto gestor de entidades de Doctrine, el cual es responsable de manejar el proceso de persistir y recuperar objetos hacia y desde la base de datos.
  • línea 15 El método persist() dice a Doctrine que «maneje» el objeto $product. Esto en realidad no provoca una consulta que se deba introducir en la base de datos (todavía).
  • línea 16 Cuando se llama al método flush(), Doctrine examina todos los objetos que está gestionando para ver si es necesario persistirlos en la base de datos. En este ejemplo, el objeto $product aún no se ha persistido, por lo tanto el gestor de la entidad ejecuta una consulta INSERT y crea una fila en la tabla producto.

Nota

De hecho, ya que Doctrine es consciente de todas tus entidades gestionadas, cuando se llama al método flush(), calcula el conjunto de cambios y ejecuta la(s) consulta(s) más eficiente(s) posible(s). Por ejemplo, si persistes un total de 100 objetos Producto y, posteriormente llamas a flush(), Doctrine creará una sola declaración preparada y la volverá a utilizar en cada inserción. Este patrón se conoce como Unidad de trabajo, y se usa porque es rápido y eficiente.

Al crear o actualizar objetos, el flujo de trabajo siempre es el mismo. En la siguiente sección, verás cómo Doctrine es lo suficientemente inteligente como para emitir automáticamente una consulta UPDATE si el registro ya existe en la base de datos.

Truco

Doctrine proporciona una biblioteca que te permite cargar en tu proyecto mediante programación los datos de prueba (es decir, «datos accesorios»). Para más información, consulta DoctrineFixturesBundle.

Recuperando objetos desde la base de datos

Recuperar un objeto desde la base de datos es aún más fácil. Por ejemplo, supongamos que has configurado una ruta para mostrar un Producto específico en función del valor de su id:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    // ... haz algo, como pasar el objeto $product a una plantilla
}

Truco

Puedes conseguir el equivalente de este sin escribir ningún código utilizando el método abreviado @ParamConverter. Consulta la documentación del FrameworkExtraBundle para más detalles.

Al consultar un determinado tipo de objeto, siempre utiliza lo que se conoce como «repositorio». Puedes pensar en un repositorio como una clase PHP, cuyo único trabajo consiste en ayudarte a buscar las entidades de una determinada clase. Puedes acceder al objeto repositorio de una clase entidad a través de:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');

Nota

La cadena AcmeStoreBundle:Product es un método abreviado que puedes utilizar en cualquier lugar de Doctrine en lugar del nombre de clase completo de la entidad (es decir, Acme\StoreBundle\Entity\Product). Mientras que tu entidad viva bajo el espacio de nombres Entity de tu paquete, esto debe funcionar.

Una vez que tengas tu repositorio, tienes acceso a todo tipo de útiles métodos:

// consulta por la clave principal (generalmente 'id')
$product = $repository->find($id);

// nombres dinámicos de métodos para buscar un valor basad en columna
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');

// recupera TODOS los productos
$products = $repository->findAll();

// busca un grupo de productos basándose en el valor de una columna arbitraria
$products = $repository->findByPrice(19.99);

Nota

Por supuesto, también puedes realizar consultas complejas, acerca de las cuales aprenderás más en la sección Consultando por objetos.

También puedes tomar ventaja de los útiles métodos findBy y findOneBy para recuperar objetos fácilmente basándote en varias condiciones:

// consulta por un producto que coincide en nombre y precio
$product = $repository->findOneBy(array('name'  => 'foo',
                                        'price' => 19.99));

// consulta para todos los  productos que emparejen el nombre, ordenados por precio
$products = $repository->findBy(
    array('name' => 'foo'),
    array('price' => 'ASC')
);

Truco

Cuando reproduces una página, puedes ver, en la esquina inferior derecha de la barra de herramientas de depuración web, cuántas consultas se realizaron.

../_images/doctrine_web_debug_toolbar_es.png

Si haces clic en el icono, se abrirá el generador de perfiles, mostrando las consultas exactas que se hicieron.

Actualizando un objeto

Una vez que hayas extraído un objeto de Doctrine, actualizarlo es relativamente fácil. Supongamos que tienes una ruta que asigna un identificador de producto a una acción de actualización de un controlador:

public function updateAction($id)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    $product->setName('New product name!');
    $em->flush();

    return $this->redirect($this->generateUrl('homepage'));
}

La actualización de un objeto únicamente consta de tres pasos:

  1. Recuperar el objeto desde Doctrine;
  2. Modificar el objeto;
  3. Invocar a flush() en el gestor de la entidad;

Ten en cuenta que no es necesario llamar a $em->persist($product). Recuerda que este método simplemente dice a Doctrine que procese o «vea» el objeto $product. En este caso, ya que recuperaste el objeto $product desde Doctrine, este ya está gestionado.

Eliminando un objeto

Eliminar un objeto es muy similar, pero requiere una llamada al método remove() del gestor de la entidad:

$em->remove($product);
$em->flush();

Como es de esperar, el método remove() notifica a Doctrine que deseas eliminar la entidad de la base de datos. La consulta DELETE real, sin embargo, no se ejecuta efectivamente hasta que se invoca al método flush().

Consultando por objetos

Ya has visto cómo el objeto repositorio te permite ejecutar consultas básicas sin ningún trabajo:

$repository->find($id);

$repository->findOneByName('Foo');

Por supuesto, Doctrine también te permite escribir consultas más complejas utilizando el lenguaje de consulta Doctrine (DQL por Doctrine Query Language). DQL es similar a SQL, excepto que debes imaginar que estás consultando por uno o más objetos de una clase entidad (por ejemplo, Producto) en lugar de consultar por filas de una tabla (por ejemplo, producto).

Al consultar en Doctrine, tienes dos opciones: escribir consultas Doctrine puras o utilizar el generador de consultas de Doctrine.

Consultando objetos con DQL

Imagina que deseas consultar los productos, pero sólo quieres devolver aquellos que cuestan más de 19.99, ordenados del más barato al más caro. Desde el interior de un controlador, haz lo siguiente:

$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
)->setParameter('price', '19.99');

$products = $query->getResult();

Si te sientes cómodo con SQL, entonces debes sentir a DQL muy natural. La más grande diferencia es que necesitas pensar en términos de «objetos» en lugar de filas de una base de datos. Por esta razón, seleccionas from AcmeStoreBundle:Product y luego lo apodas p.

El método getResult() devuelve un arreglo de resultados. Si estás preguntando por un solo objeto, en su lugar puedes utilizar el método getSingleResult():

$product = $query->getSingleResult();

Prudencia

El método getSingleResult() lanza una excepción Doctrine\ORM\NoResultException si no se devuelven resultados y una Doctrine\ORM\NonUniqueResultException si se devuelve más de un resultado. Si utilizas este método, posiblemente tengas que envolverlo en un bloque try-catch y asegurarte de que sólo devuelve un resultado (si estás consultando sobre algo que sea viable podrías regresar más de un resultado):

$query = $em->createQuery('SELECT ...')
    ->setMaxResults(1);

try {
    $product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
    $product = null;
}
// ...

La sintaxis DQL es increíblemente poderosa, permitiéndote unir entidades fácilmente (el tema de las relaciones se describe más adelante), agrupación, etc. Para más información, consulta la documentación oficial de Doctrine Query Language.

Usando el generador de consultas de Doctrine

En lugar de escribir las consultas directamente, también puedes usar el QueryBuilder de Doctrine para hacer el mismo trabajo con una agradable interfaz orientada a objetos. Si usas un IDE, también puedes tomar ventaja del autocompletado a medida que escribes los nombres de método. Desde el interior de un controlador:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');

$query = $repository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();

$products = $query->getResult();

El objeto QueryBuilder contiene todos los métodos necesarios para construir tu consulta. Al invocar al método getQuery(), el generador de consultas devuelve un objeto Query normal, el cual es el mismo objeto que construiste directamente en la sección anterior.

Para más información sobre el generador de consultas de Doctrine, consulta la documentación del Generador de consultas de Doctrine.

Repositorio de clases personalizado

En las secciones anteriores, comenzamos a construir y utilizar consultas más complejas desde el interior de un controlador. Con el fin de aislar, probar y volver a usar estas consultas, es buena idea crear una clase repositorio personalizada para tu entidad y agregar métodos con tu lógica de consulta allí.

Para ello, agrega el nombre de la clase del repositorio a la definición de asignación.

  • Annotations
    // src/Acme/StoreBundle/Entity/Product.php
    namespace Acme\StoreBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity(repositoryClass="Acme\StoreBundle\Entity\ProductRepository")
     */
    class Product
    {
        //...
    }
    
  • YAML
    # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
    Acme\StoreBundle\Entity\Product:
        type: entity
        repositoryClass: Acme\StoreBundle\Entity\ProductRepository
        # ...
    
  • XML
    <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
    
    <!-- ... -->
    <doctrine-mapping>
    
        <entity name="Acme\StoreBundle\Entity\Product"
                repository-class="Acme\StoreBundle\Entity\ProductRepository">
                <!-- ... -->
        </entity>
    </doctrine-mapping>
    

Doctrine puede generar la clase repositorio por ti ejecutando la misma orden usada anteriormente para generar los métodos captadores y definidores omitidos:

$ php app/console doctrine:generate:entities Acme

A continuación, agrega un nuevo método — findAllOrderedByName() — a la clase repositorio recién generada. Este método debe consultar todas las entidades Producto, ordenadas alfabéticamente.

// src/Acme/StoreBundle/Entity/ProductRepository.php
namespace Acme\StoreBundle\Entity;

use Doctrine\ORM\EntityRepository;

class ProductRepository extends EntityRepository
{
    public function findAllOrderedByName()
    {
        return $this->getEntityManager()
            ->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC')
            ->getResult();
    }
}

Truco

Puedes acceder al gestor de la entidad a través de $this->getEntityManager() desde el interior del repositorio.

Puedes utilizar este nuevo método al igual que los métodos de búsqueda predefinidos del repositorio:

$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AcmeStoreBundle:Product')
               ->findAllOrderedByName();

Nota

Al utilizar una clase repositorio personalizada, todavía tienes acceso a los métodos de búsqueda predeterminados como find() y findAll().

Entidad relaciones/asociaciones

Supón que los productos en tu aplicación pertenecen exactamente a una «categoría». En este caso, necesitarás un objeto Categoría y una manera de relacionar un objeto Producto a un objeto Categoría. Empieza por crear la entidad Categoría. Ya sabes que tarde o temprano tendrás que persistir la clase a través de Doctrine, puedes dejar que Doctrine cree la clase por ti.

$ php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string(255)"

Esta tarea genera la entidad Categoría para ti, con un campo id, un campo name y las funciones captadoras y definidoras asociadas.

Relación con la asignación de metadatos

Para relacionar las entidades Categoría y Producto, empieza por crear una propiedad productos en la clase Categoría:

  • Annotations
    // src/Acme/StoreBundle/Entity/Category.php
    
    // ...
    use Doctrine\Common\Collections\ArrayCollection;
    
    class Category
    {
        // ...
    
        /**
         * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
         */
        protected $products;
    
        public function __construct()
        {
            $this->products = new ArrayCollection();
        }
    }
    
  • YAML
    # src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml
    Acme\StoreBundle\Entity\Category:
        type: entity
        # ...
        oneToMany:
            products:
                targetEntity: Product
                mappedBy: category
        # no olvides iniciar la colección en el método __construct() de la entidad
    
  • XML
    <!-- src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.xml -->
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                        http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="Acme\StoreBundle\Entity\Category">
            <!-- ... -->
            <one-to-many field="products"
                target-entity="product"
                mapped-by="category"
            />
    
            <!-- no olvides iniciar la colección en el método __construct() de la entidad -->
        </entity>
    </doctrine-mapping>
    

En primer lugar, ya que un objeto Categoría debe relacionar muchos objetos Producto, agregamos una propiedad Productos para contener esos objetos Producto. Una vez más, esto no se hace porque lo necesite Doctrine, sino porque tiene sentido en la aplicación para que cada Categoría mantenga una gran variedad de objetos Producto.

Nota

El código del método __construct() es importante porque Doctrine requiere que la propiedad $products sea un objeto ArrayCollection. Este objeto se ve y actúa casi exactamente como un arreglo, pero tiene cierta flexibilidad. Si esto te hace sentir incómodo, no te preocupes. Sólo imagina que es un arreglo y estarás bien.

Truco

El valor de targetEntity en el decorador utilizado anteriormente puede hacer referencia a cualquier entidad con un espacio de nombres válido, no sólo a las entidades definidas en la misma clase. Para relacionarlo con una entidad definida en una clase o paquete diferente, escribe un espacio de nombres completo como la targetEntity.

Después, ya que cada clase Producto se puede relacionar exactamente a un objeto Categoría, podrías desear agregar una propiedad $category a la clase Producto:

  • Annotations
    // src/Acme/StoreBundle/Entity/Product.php
    
    // ...
    class Product
    {
        // ...
    
        /**
         * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
         * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
         */
        protected $category;
    }
    
  • YAML
    # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
    Acme\StoreBundle\Entity\Product:
        type: entity
        # ...
        manyToOne:
            category:
                targetEntity: Category
                inversedBy: products
                joinColumn:
                    name: category_id
                    referencedColumnName: id
    
  • XML
    <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                        http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="Acme\StoreBundle\Entity\Product">
            <!-- ... -->
            <many-to-one field="category"
                target-entity="products"
                join-column="category"
            >
                <join-column
                    name="category_id"
                    referenced-column-name="id"
                />
            </many-to-one>
        </entity>
    </doctrine-mapping>
    

Por último, ahora que agregaste una nueva propiedad a ambas clases Categoría y Producto, le dices a Doctrine que genere por ti los métodos captadores y definidores omitidos:

$ php app/console doctrine:generate:entities Acme

No hagas caso de los metadatos de Doctrine por un momento. Ahora tienes dos clases —Categoría y Producto— con una relación natural de uno a muchos. La clase Categoría tiene un arreglo de objetos Producto y el objeto producto puede contener un objeto Categoría. En otras palabras, construiste tus clases de una manera que tiene sentido para tus necesidades. El hecho de que los datos se tienen que persistir en una base de datos, siempre es secundario.

Ahora, veamos los metadatos sobre la propiedad $category en la clase Producto. Esta información le dice a Doctrine que la clase está relacionada con Categoría y que debe guardar el id del registro de la categoría en un campo category_id que vive en la tabla producto. En otras palabras, el objeto Categoría relacionado se almacenará en la propiedad $category, pero tras bambalinas, Doctrine deberá persistir esta relación almacenando el valor del id de la categoría en una columna category_id de la tabla producto.

../_images/doctrine_image_2_es.png

Los metadatos sobre la propiedad $products del objeto Categoría son menos importantes, y simplemente dicen a Doctrine que vea la propiedad Product.category para resolver cómo se asigna la relación.

Antes de continuar, asegúrate de decirle a Doctrine que agregue la nueva tabla categoría, la columna product.category_id y la nueva clave externa:

$ php app/console doctrine:schema:update --force

Nota

Esta tarea sólo la deberías utilizar durante el desarrollo. Para un más robusto método de actualización sistemática de tu base de datos en producción, lee sobre las Migraciones de Doctrine.

Guardando entidades relacionadas

Ahora, ¡puedes ver este nuevo código en acción! Imagina que estás dentro de un controlador:

// ...

use Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function createProductAction()
    {
        $category = new Category();
        $category->setName('Main Products');

        $product = new Product();
        $product->setName('Foo');
        $product->setPrice(19.99);
        // relaciona este producto a la categoría
        $product->setCategory($category);

        $em = $this->getDoctrine()->getManager();
        $em->persist($category);
        $em->persist($product);
        $em->flush();

        return new Response(
            'Created product id: '.$product->getId().' and category id: '.$category->getId()
        );
    }
}

Ahora, se agrega una sola fila a las tablas categoría y producto. La columna product.category_id para el nuevo producto se ajusta a algún id de la nueva categoría. Doctrine gestiona la persistencia de esta relación para ti.

Recuperando objetos relacionados

Cuando necesites recuperar objetos asociados, tu flujo de trabajo se ve justo como lo hacías antes. En primer lugar, buscas un objeto $product y luego accedes a su Categoría asociada:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);

    $categoryName = $product->getCategory()->getName();

    // ...
}

En este ejemplo, primero consultas por un objeto Producto basándote en el id del producto. Este emite una consulta solo para los datos del producto e hidrata al objeto $product con esos datos. Más tarde, cuando llames a $product->getCategory()->getName(), Doctrine silenciosamente hace una segunda consulta para encontrar la Categoría que está relacionada con este Producto. Entonces, prepara el objeto $category y te lo devuelve.

../_images/doctrine_image_3_es.png

Lo importante es el hecho de que tienes fácil acceso a la categoría relacionada con el producto, pero, los datos de la categoría realmente no se recuperan hasta que pides la categoría (es decir, trata de «cargarlos de manera diferida»).

También puedes consultar en la dirección contraria:

public function showProductAction($id)
{
    $category = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Category')
        ->find($id);

    $products = $category->getProducts();

    // ...
}

En este caso, ocurre lo mismo: primero consultas por un único objeto Categoría, y luego Doctrine hace una segunda consulta para recuperar los objetos Producto relacionados, pero sólo una vez/si le preguntas por ellos (es decir, cuando invoques a ->getProducts()). La variable $products es un arreglo de todos los objetos Producto relacionados con el objeto Categoría propuesto a través de sus valores category_id.

Uniendo registros relacionados

En los ejemplos anteriores, se realizaron dos consultas —una para el objeto original (por ejemplo, una Categoría)— y otra para el/los objetos relacionados (por ejemplo, los objetos Producto).

Truco

Recuerda que puedes ver todas las consultas realizadas durante una petición a través de la barra de herramientas de depuración web.

Por supuesto, si sabes por adelantado que necesitas tener acceso a los objetos, puedes evitar la segunda consulta emitiendo una unión en la consulta original. Agrega el siguiente método a la clase ProductRepository:

// src/Acme/StoreBundle/Entity/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
    $query = $this->getEntityManager()
        ->createQuery('
            SELECT p, c FROM AcmeStoreBundle:Product p
            JOIN p.category c
            WHERE p.id = :id'
        )->setParameter('id', $id);

    try {
        return $query->getSingleResult();
    } catch (\Doctrine\ORM\NoResultException $e) {
        return null;
    }
}

Ahora, puedes utilizar este método en el controlador para consultar un objeto Producto y su correspondiente Categoría con una sola consulta:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->findOneByIdJoinedToCategory($id);

    $category = $product->getCategory();

    // ...
}

Más información sobre asociaciones

Esta sección ha sido una introducción a un tipo común de relación entre entidades, la relación uno a muchos. Para obtener detalles más avanzados y ejemplos de cómo utilizar otros tipos de relaciones (por ejemplo, uno a uno, muchos a muchos), consulta la sección Asignando asociaciones en la documentación de Doctrine.

Nota

Si estás utilizando anotaciones, tendrás que prefijar todas las anotaciones con ORM\ (por ejemplo, ORM\OneToMany), lo cual no se refleja en la documentación de Doctrine. También tendrás que incluir la declaración use Doctrine\ORM\Mapping as ORM; la cual importa el prefijo ORM de las anotaciones.

Configurando

Doctrine es altamente configurable, aunque probablemente nunca tendrás que preocuparte de la mayor parte de sus opciones. Para más información sobre la configuración de Doctrine, consulta la sección Doctrine del Manual de referencia.

Ciclo de vida de las retrollamadas

A veces, es necesario realizar una acción justo antes o después de insertar, actualizar o eliminar una entidad. Este tipo de acciones se conoce como «ciclo de vida» de las retrollamadas, ya que son métodos retrollamados que necesitas ejecutar durante las diferentes etapas del ciclo de vida de una entidad (por ejemplo, cuando la entidad es insertada, actualizada, eliminada, etc.)

Si estás utilizando anotaciones para los metadatos, empieza por permitir el ciclo de vida de las retrollamadas. Esto no es necesario si estás usando YAML o XML para tu asignación:

/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Product
{
    // ...
}

Ahora, puedes decir a Doctrine que ejecute un método en cualquiera de los eventos del ciclo de vida disponibles. Por ejemplo, supongamos que deseas establecer una columna de fecha created a la fecha actual, sólo cuando se persiste por primera vez la entidad (es decir, se inserta):

  • Annotations
    /**
     * @ORM\PrePersist
     */
    public function setCreatedValue()
    {
        $this->created = new \DateTime();
    }
    
  • YAML
    # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
    Acme\StoreBundle\Entity\Product:
        type: entity
        # ...
        lifecycleCallbacks:
            prePersist: [setCreatedValue]
    
  • XML
    <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
    
    <!-- ... -->
    <doctrine-mapping>
    
        <entity name="Acme\StoreBundle\Entity\Product">
                <!-- ... -->
                <lifecycle-callbacks>
                    <lifecycle-callback type="prePersist" method="setCreatedValue" />
                </lifecycle-callbacks>
        </entity>
    </doctrine-mapping>
    

Nota

En el ejemplo anterior se supone que has creado y asignado una propiedad created (no mostrada aquí).

Ahora, justo antes de persistir la primer entidad, Doctrine automáticamente llamará a este método y establecerá el campo created a la fecha actual.

Esto se puede repetir en cualquiera de los otros eventos del ciclo de vida, los cuales incluyen a:

  • preRemove
  • postRemove
  • prePersist
  • postPersist
  • preUpdate
  • postUpdate
  • postLoad
  • loadClassMetadata

Para más información sobre qué significan estos eventos y el ciclo de vida de las retrollamadas en general, consulta la sección Ciclo de vida de los eventos en la documentación de Doctrine.

Extensiones Doctrine: Timestampable, Sluggable, etc.

Doctrine es bastante flexible, y dispone de una serie de extensiones de terceros que te permiten realizar fácilmente tareas repetitivas y comunes en tus entidades. Estas incluyen cosas tales como Sluggable, Timestampable, Loggable, Translatable y Tree.

Para más información sobre cómo encontrar y utilizar estas extensiones, ve el artículo sobre el uso de extensiones comunes de Doctrine.

Referencia de tipos de campo Doctrine

Doctrine dispone de una gran cantidad de tipos de campo. Cada uno de estos asigna un tipo de dato PHP a un tipo de columna específica en cualquier base de datos que estés utilizando. Los siguientes tipos son compatibles con Doctrine:

  • Cadenas
    • string (usado para cadenas cortas)
    • text (usado para cadenas grandes)
  • Números
    • integer
    • smallint
    • bigint
    • decimal
    • float
  • Fechas y horas (usa un objeto DateTime para estos campos en PHP)
    • date
    • time
    • datetime
  • Otros tipos
    • boolean
    • object (serializado y almacenado en un campo CLOB)
    • array (serializado y almacenado en un campo CLOB)

Para más información, consulta la sección Asignando tipos en la documentación de Doctrine.

Opciones de campo

Cada campo puede tener un conjunto de opciones aplicables. Las opciones disponibles incluyen type (el predeterminado es string), name, length, unique y nullable. Aquí tienes algunos ejemplos:

  • Annotations
    /**
     * Un campo cadena con una longitud de 255 caracteres que no puede ser nulo
     * (reflejando los valores predefinidos para 'type', 'length'
     * y opciones *nullable*)
     *
     * @ORM\Column()
     */
    protected $name;
    
    /**
     * Un campo cadena de 150 caracteres de longitud que se persiste a una columna «email_address» y tiene un índice único.
     *
     * @ORM\Column(name="email_address", unique=true, length=150)
     */
    protected $email;
    
  • YAML
    fields:
        # Un campo cadena de longitud 255 que no puede ser null
        # (reflejando los valores predefinidos para las opciones 'length' y 'nullable')
        # el atributo type es necesario en las definiciones yaml
        name:
            type: string
    
        # Un campo cadena de longitud 150 que persiste a una columna 'email_address'
        # y tiene un índice único.
        email:
            type: string
            column: email_address
            length: 150
            unique: true
    
  • XML
    # Un campo de tipo «string» de longitud 255 que no puede ser nulo
        # (reflejando los valores predefinidos para las opciones «length» y «nullable»)
        # el atributo «type» es necesario en las definiciones yaml
        name:
    <field name="name" type="string" />
    <field name="email"
        type="string"
        column="email_address"
        length="150"
        unique="true"
    />
    

Nota

Hay algunas opciones más que no figuran en esta lista. Para más detalles, consulta la sección Asignando propiedades de la documentación de Doctrine.

Ordenes de consola

La integración del ORM de Doctrine2 ofrece varias ordenes de consola bajo el espacio de nombres doctrine. Para ver la lista de ordenes puedes ejecutar la consola sin ningún tipo de argumento:

$ php app/console

Mostrará una lista con las ordenes disponibles, muchas de las cuales comienzan con el prefijo doctrine:. Puedes encontrar más información sobre cualquiera de estas ordenes (o cualquier orden de Symfony) ejecutando la orden help. Por ejemplo, para obtener detalles acerca de la tarea doctrine:database:create, ejecuta:

$ php app/console help doctrine:database:create

Algunas tareas notables o interesantes son:

  • doctrine:ensure-production-settings — comprueba si el entorno actual está configurado de manera eficiente para producción. Esta siempre se debe ejecutar en el entorno prod:

    $ php app/console doctrine:ensure-production-settings --env=prod
    
  • doctrine:mapping:import — permite a Doctrine llevar a cabo una introspección a una base de datos existente y crear información de asignación. Para más información, consulta Cómo generar entidades desde una base de datos existente.

  • doctrine:mapping:info — te dice todas las entidades de las que Doctrine es consciente y si hay algún error básico con la asignación.

  • doctrine:query:dql y doctrine:query:sql — te permiten ejecutar consultas DQL o SQL directamente desde la línea de ordenes.

Nota

Para poder cargar accesorios a tu base de datos, en su lugar, necesitas tener instalado el paquete DoctrineFixturesBundle. Para aprender cómo hacerlo, lee el artículo «DoctrineFixturesBundle» en la documentación.

Truco

Esta página muestra cómo trabajar con Doctrine dentro de un controlador. Posiblemente también quieras trabajar con Doctrine en algún otro lugar en tu aplicación. El método getDoctrine() del controlador regresa el servicio doctrine, puedes trabajar con este de la misma manera que en cualquier otro lugar inyectándolo en tus propios servicios. Consulta Contenedor de servicios para más sobre la creación de tus propios servicios.

Resumen

Con Doctrine, puedes centrarte en tus objetos y la forma en que son útiles en tu aplicación y luego preocuparte por su persistencia en la base de datos. Esto se debe a que Doctrine te permite utilizar cualquier objeto PHP para almacenar los datos y se basa en la información de asignación de metadatos para asignar los datos de un objeto a una tabla particular de la base de datos.

Y aunque Doctrine gira en torno a un concepto simple, es increíblemente poderoso, permitiéndote crear consultas complejas y suscribirte a los eventos que te permiten realizar diferentes acciones conforme los objetos recorren su ciclo de vida en la persistencia.

Para más información acerca de Doctrine, ve la sección Doctrine del Recetario, que incluye los siguientes artículos:

Bifúrcame en GitHub