Symfony 2.4, el libro oficial

8.1. Un ejemplo sencillo

La forma más fácil de entender cómo funciona Doctrine consiste en verlo en acción. En esta sección se configura el acceso a la base de datos, se crea un objeto llamado Product, se persiste su información en la base de datos y se obtiene de nuevo mediante una consulta.

Si quieres seguir este capítulo mientras programas los ejemplos, tienes que crear primero un bundle de prueba con el siguiente comando de Symfony:

$ php app/console generate:bundle --namespace=Acme/StoreBundle

8.1.1. Configurando la base de datos

Antes de comenzar, es necesario configurar la información para acceder a la base de datos. Por convención, esta información se configura en el archivo app/config/parameters.yml:

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

# ...

Nota Definir la configuración en el archivo parameters.yml sólo es una convención. De hecho, estos parámetros realmente se utilizan en el archivo de configuración principal de la aplicación:

# app/config/config.yml
doctrine:
    dbal:
        driver:   "%database_driver%"
        host:     "%database_host%"
        dbname:   "%database_name%"
        user:     "%database_user%"
        password: "%database_password%"
<!-- 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>
// 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 definir 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, así puedes almacenar fácilmente la configuración de la base de datos (o cualquier otra información sensible) fuera de tu proyecto.

Ahora que Doctrine ya conoce tu base de datos, puedes utilizar el siguiente comando para crearla:

$ php app/console doctrine:database:create

8.1.2. Utilizando SQLite

Si utilizas SQLite como base de datos, configura la ruta del archivo donde se guarda la información de la base de datos:

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

8.1.3. Creando una clase Entity

Imagina que estás desarrollando una aplicación en la que vas a mostrar productos. Olvidándote de Doctrine y de las bases de datos, seguramente estás pensando en utilizar un objeto Product para representar a los productos. Crea esta clase dentro del directorio Entity del bundle AcmeStoreBundle:

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

class Product
{
    protected $name;

    protected $price;

    protected $description;
}

Esta clase normalmente se llama "entidad" o "entity", lo que significa que es una clase muy sencilla que sólo se utiliza para almacenar datos. Aunque se trata de una clase muy básica, cumple su objetivo de representar a los productos de tu aplicación. No obstante, esta clase no se puede guardar en una base de datos — es sólo una clase PHP simple.

Truco Una vez aprendidos los conceptos fundamentales de Doctrine, podrás generar las clases de tipo entidad más fácilmente con el siguiente comando. Una vez ejecutado, Doctrine te hará varias preguntas para generar la entidad de forma interactiva:

$ php app/console doctrine:generate:entity

8.1.4. Añadiendo la información de mapeo

Trabajar con Doctrine es mucho más interesante que hacerlo directamente con la base de datos. En vez de trabajar con filas y tablas, Doctrine te permite guardar y obtener objetos enteros a partir de la información de la base de datos. El truco para que esto funcione consiste en mapear una clase PHP a una tabla de la base de datos y después, mapear las propiedades de la clase PHP a las columnas de esa tabla:

Mapeando un objeto PHP a una tabla

Figura 8.1 Mapeando un objeto PHP a una tabla

Doctrine simplifica al máximo este proceso, de manera que sólo tienes que añadir algunos metadatos a la clase PHP para configurar cómo se mapean la clase Product y sus propiedades. Estos metadatos se pueden configurar en archivos YAML, XML o directamente mediante anotaciones en la propia clase PHP:

// 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;
}
# 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
<!-- 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 En un mismo bundle no puedes mezclar diferentes formas de definir los metadatos. Si utilizas por ejemplo YAML en una entidad, no puedes utilizar anotaciones PHP en otras entidades.

Truco El nombre de la tabla es opcional y si lo omites, se genera automáticamente en función del nombre de la clase PHP.

Doctrine incluye soporte para una gran variedad de tipos de campo, cada uno con sus propias opciones, tal y como se explicará más adelante en este mismo capítulo.

También puedes consultar la documentación oficial de Doctrine sobre el mapeo. Ten en cuenta que en la documentación de Doctrine no se explica que si utilizas anotaciones, tienes que prefijarlas todas con la cadena ORM\ (por ejemplo, ORM\Column(...)). Igualmente, no te olvides de añadir la declaración use Doctrine\ORM\Mapping as ORM; al principio de tus clases para importar el prefijo ORM\.

Advertencia Ten cuidado de que el nombre de tu clase o el de tus propiedades no coincida con alguna de las palabras clave de SQL (como por ejemplo group o user). Si tu clase se llama Group y no indicas un nombre de tabla, se utilizará automáticamente el valor group, lo que provocará un error en algunos tipos de bases de datos.

Consulta la sección Quoting reserved words de la documentación de Doctrine para conocer la lista completa de palabras reservadas.

Si lo prefieres, también puedes solucionar este problema modificando el nombre generado para la tabla o para las columnas. Consulta las secciones persistent classes y property mapping de la documentación de Doctrine.

Nota Si en tu aplicación utilizas alguna otra librería o aplicación con anotaciones (por ejemplo Doxygen), añade la anotación @IgnoreAnnotation en la clase para indicar qué anotaciones de Symfony se deben ignorar.

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

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

8.1.5. Generando getters y setters

Doctrine ya sabe cómo persistir los objetos de tipo Product en la base de datos, pero esa clase no es muy útil por el momento. Como Product es una clase PHP normal y corriente, es necesario crear métodos getters y setters (getName(), setName(), etc.) para poder acceder a sus propiedades (porque son de tipo protected). Como esto es bastante habitual, existe un comando para que Doctrine añada estos métodos automáticamente:

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

Este comando asegura que la clase Product contiene todos los getters y setters necesarios. Puedes ejecutar de forma segura este comando una y otra vez, ya que los getters y setters sólo se generan si no existen (no se borran los métodos existentes).

Advertencia Los getters y setters que genera Doctrine son muy básicos, por lo que tendrás que ajustar su código a las necesidades de tu aplicación.

También puedes generar a la vez todas las entidades de un bundle (es decir, cualquier clase PHP con información de mapeo de Doctrine) o de un espacio de nombres:

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

Nota A Doctrine no le importa si tus propiedades son protected o private, o si una propiedad tiene o no un getter o setter definido. Pero estos métodos sí que son necesarios para que puedas interactuar con el objeto PHP que almacena la información.

8.1.6. Creando las tablas de la base de datos (el esquema)

Aunque tienes una clase Product utilizable con información de mapeo para que Doctrine sepa persistirla, todavía no tienes su correspondiente tabla product en la base de datos. Afortunadamente, Doctrine puede crear automáticamente todas las tablas necesarias en la base de datos (una para cada entidad conocida de tu aplicación). Para ello, ejecuta el siguiente comando:

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

Truco En realidad, este comando es muy poderoso. Internamente compara la estructura que debería tener tu base de datos (según la información de mapeo de tus entidades) con la estructura que realmente tiene y genera las sentencias SQL necesarias para actualizar la estructura de la base de datos.

En otras palabras, si añades una nueva propiedad a la clase Product y ejecutas este comando otra vez, se genera una sentencia de tipo ALTER TABLE para añadir la nueva columna a la tabla product existente.

Una forma aún mejor para aprovechar esta funcionalidad son las migraciones, que permiten generar estas instrucciones SQL y almacenarlas en unas clases PHP especiales que puedes ejecutar en tu servidor de producción para aplicar los cambios en las bases de datos.

Después de ejecutar este comando, la base de datos cuenta ahora con una tabla llamada product completamente funcional, y sus columnas coinciden con los metadatos que has especificado en la clase Product.

8.1.7. Persistiendo objetos en la base de datos

Ahora que tienes mapeada una entidad Product y su tabla product correspondiente, ya puedes persistir la información en la base de datos. De hecho, persistir información dentro de un controlador es bastante sencillo. Añade el siguiente método al controlador DefaultController del bundle:

// 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 capítulo probando los ejemplos de código en tu propio ordenador, no olvides crear una ruta que apunte a esta acción para poder probarla en el navegador.

Truco En este capítulo se utiliza Doctrine directamente a través del método getDoctrine() de los controladores. En realidad, este método es un atajo para obtener el servicio llamado doctrine. Si quieres usar Doctrine en cualquier otra parte que no sea un controlador, puedes inyectar ese servicio.

Veamos detenidamente cómo funciona el ejemplo anterior:

  • Líneas 9-12 En esta sección, creas una instancia y trabajas con el objeto $product como harías con cualquier otro objeto PHP normal.
  • Línea 14 Esta línea obtiene el "entity manager" o gestor de entidades de Doctrine, que se utiliza para persistir y recuperar objetos hacia y desde la base de datos.
  • Línea 15 El método persist() le dice a Doctrine que debe persistir el objeto $product, pero todavía no se genera (y por tanto, tampoco se ejecuta) la sentencia SQL correspondiente.
  • 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 que el gestor de la entidad ejecuta una consulta de tipo INSERT y crea una fila en la tabla product.

Nota Como Doctrine conoce todas tus entidades, cuando se llama al método flush(), calcula todos los cambios que debe hacer y ejecuta las sentencias más eficientes posibles. Si persistes por ejemplo 100 objetos de tipo Product y después llamas al método flush(), Doctrine prepara una sola sentencia SQL y la reutiliza para cada inserción. Esta forma de trabajar se corresponde con el patrón de diseño llamado Unit of Work y se utiliza porque es muy 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 detectar si la consulta debe ser de tipo UPDATE en vez de INSERT si el registro ya existe en la base de datos.

Truco Doctrine dispone de una librería para cargar de forma automática datos de prueba como los que utilizan los tests (estos datos se suelen llamar fixtures o datos de prueba). Esta librería está disponible en Symfony2 a través del bundle DoctrineFixturesBundle.

8.1.8. Buscando objetos en la base de datos

Buscar información de la base de datos y recuperar en forma de objeto es todavía más fácil. Imagina que has configurado una ruta de la aplicación para mostrar la información de un producto a partir del valor de su id. El código del controlador correspondiente podría ser el siguiente:

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

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

    // ... (pasar el objeto $product a una plantilla)
}

Truco Si utilizas un concepto avanzado llamado @ParamConverter puedes conseguir el mismo resultado que el código anterior sin tener que escribir una sola línea de código. Consulta la documentación del bundle FrameworkExtraBundle para obtener más información.

Al realizar una consulta por un determinado objeto, Doctrine siempre utiliza lo que se conoce como "repositorio". Estos repositorios son como clases PHP cuyo trabajo consiste en ayudarte a buscar las entidades de una determinada clase. Puedes acceder al repositorio de la entidad de una clase mediante el código:

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

Nota La cadena AcmeStoreBundle:Product es un atajo que puedes utilizar en cualquier lugar de Doctrine en vez del nombre completo de la clase de la entidad (en este caso, Acme\StoreBundle\Entity\Product). Este atajo funciona siempre que tu entidad se encuentre bajo el espacio de nombres Entity de algún bundle.

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

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

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

// obtiene todos los productos
$products = $repository->findAll();

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

Nota Por supuesto, también puedes realizar consultas más complejas, tal y como se explicará más adelante.

También puedes utilizar los métodos findBy y findOneBy para obtener objetos en función de varias condiciones:

// busca un producto con ese nombre y ese precio
$product = $repository->findOneBy(array(
    'name'  => 'foo', 'price' => 19.99
));

// obtiene todos los productos con un nombre determinado
// y ordena los resultados por precio
$product = $repository->findBy(
    array('name'  => 'foo'),
    array('price' => 'ASC')
);

Truco En la esquina inferior derecha de la barra de depuración web, puedes ver cuántas consultas a la base de datos fueron necesarias para renderizar esa página.

Información de Doctrine en la barra de depuración web

Figura 8.2 Información de Doctrine en la barra de depuración web

Si pinchas sobre el icono, se abrirá el profiler de Symfony y se mostrarán las consultas exactas que se hicieron.

8.1.9. Actualizando un objeto

Una vez que hayas obtenido un objeto de Doctrine, actualizarlo es relativamente fácil. Supongamos que la aplicación dispone de una ruta que actualiza la información del producto cuyo id se indica:

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'));
}

Actualizar un objeto requiere de tres pasos:

  1. Obtener el objeto utilizando Doctrine.
  2. Modificar el objeto.
  3. Invocar al método flush() del entity manager.

Observa que no hace falta llamar al método $em->persist($product). Este método sive para avisar a Doctrine de que vas a manipular un determinado objeto. En este caso, como el objeto $product lo has obtenido mediante una consulta a Doctrine, este ya sabe que debe estar atento a los posibles cambios del objeto.

8.1.10.  Eliminando un objeto

Eliminar objetos es un proceso similar, pero requiere invocar el método remove() del entity manager:

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

Como puede que imagines, el método remove() avisa a Doctrine que quieres eliminar esa entidad de la base de datos, pero no la borra realmente. La consulta DELETE correspondiente no se genera ni se ejecuta hasta que no se invoca el método flush().