Desde el punto en que acabamos el último día vamos a implementar la base de datos que vamos a necesitar para gestionar nuestras entidades. Como primer desarrollo podemos destacar la necesidad de tener un pequeño blog, que, al acceder a la aplicación informe a nuestros usuarios de los últimos cambios o de las noticias relevantes. De paso aprovecharé para ver la implementación de los usos más cotidianos de acceso a la BD. No esperaba hacer entradas tan largas pero la verdad que a poco que te metes Symfony y Doctrine se llevan tu tiempo, tienen una curva de aprendizaje pronunciada aunque bastante fácil de seguir, espero que no se os haga muy denso…

Qué es Doctrine? ORM?

Doctrine son un conjunto de librerías que nos va a simplificar todas las operaciones que realicemos con la base de datos, además de permitirnos abstraernos de la tecnología del servidor. Doctrine puede conectarse contra bases de datos como MySQL, PostgreSQL o Microsoft SQL, también tiene un conector para trabajar con MongoDB. Esta capa nos va a permitir trabajar con un estándar de definición de datos, tablas y relaciones mediante clases PHP. Posteriormente, el propio Doctrine se encargará de replicar en la Base de datos destino, de esta manera no necesitaremos conocer las rutinas de creación y mantenimiento en estos sistemas…

Es un ORM, es decir, un Object Relational Mapper, con esto entendemos que es capaz de mapear una base de datos relacional mediante objetos (Clases). Con las clases de Doctrine dispondremos de nuestras entidades (tablas) de datos como objetos PHP, que soportaran herencia y polimorfismo. Las operaciones que realicemos con estas clases no dependerán de sentencias escritas en SQL, si no que cada clase tendrá mapeados diferentes métodos y propiedades que nos permitirán realizar las mismas operaciones de manera independiente al lenguaje SQL.

Configurando la base de datos.

En el archivo /app/config/parameters.yml tenemos configurado el acceso a la base de datos, los datos se han seteado al inicializar el proyecto con Composer en la primera parte del tutorial. Así que vamos a crear la base de datos para comenzar a definir las entidades de nuestro backend. Accedemos a la consola de MySQL o en su defecto con cualquier admin de MySQL nos conectamos al servidor, creamos la base de datos y le damos permisos al usuario.

1
2
3
create database symfony;

grant all on symfony.* to 'symfony'@'localhost' identified by 'pass54mfon4';

Ahora hay que cambiar esos valores en el archivo parameters.yml.

1
2
3
4
5
6
7
8
9
10
11
12
# app/config/parameters.yml
parameters:
database_host: 127.0.0.1
database_port: null
database_name: symfony
database_user: symfony
database_password: pass54mfon4
mailer_transport: smtp
mailer_host: 127.0.0.1
mailer_user: null
mailer_password: null
secret: ThisTokenIsNotSoSecretChangeIt

En la documentacion oficial proponen generar la base de datos con el propio Symfony mediante el comando

1
php bin/console doctrine:database:create

En ese caso necesitaríamos tener derechos de administrador para poder crear la base de datos, por ejemplo con el usuario root, con o sin password, aunque yo prefiero algo un poco más seguro. En la documentación de Symfony recomiendan usar el juego de caracteres utf8mb4_unicode_ci por defecto, cambiarlo en el my.cnf o directamente expresarle a doctrine nuestra preferencia en el archivo de configuración. La sección doctrine ya ha de existir al igual que la sub-sección dbal, sería cambiar en esta el charset y añadir las default_table_options.

1
2
3
4
5
6
7
# app/config/config.yml
doctrine:
dbal:
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci

Clase de entidad

Como ya sabemos las tablas de las bases de datos de denominan entidades y a nivel definición utilizamos ese concepto en los diagramas de relación. De manera que la clase encargada de mantener la información de la tabla de base de datos en Symfony se denomina entidad. En nuestro caso, el blog se va a corresponder con un apartado de notificaciones del backend, es decir, un cauce para que nuestra herramienta pueda comunicar a los usuarios las mejoras, cambios, o eventos del sistema a los que tengan que estar atentos. Vamos a crear una entidad para almacenar estos mensajes. Dentro del directorio src/AppBundle crearemos una nueva carpeta llamada Entity y dentro de esta crearemos el archivo PHP que dará vida a nuestra primera clase-entidad “Blog”. De momento esta entidad no la crearemos en base de datos, aun necesitamos completar la información de sus propiedades para que sea correctamente representada en SQL.

1
2
3
4
5
6
7
8
9
10
11
12
// src/AppBundle/Entity/Blog.php
namespace AppBundle\Entity;

class Blog
{
private $id;
private $title;
private $content;
private $created;
private $modified;
private $deleted;
}

Mapeo de la información

Doctrine es un ORM complejo que va más allá de recuperar datos de la base de datos en arrays asociativos sobre los que iterar. Doctrine nos permitirá obtener objetos de estos datos y nos permitirá hacer persistir estos objetos en la base de datos. Para esto debemos relacionar las tablas con los objetos que las representan y mapear las columnas de dichas tablas con las propiedades de sus clases correspondientes. Esta información de mapeo se puede proveer de múltiples maneras, a través de archivos yaml, xml o directamente en la propia clase, al igual que las rutas en los controladores, con anotaciones docBlock.

El nombre de la entidad en la base de datos se sacará de su definición, pero en caso de no existir, Symfony tratará de suponerlo utilizando el nombre de la clase.

Los métodos de mapeo no se pueden mezclar para cada bundle, debería ser realizado de una única manera, o bien yaml, o docBlock…

Crearemos la entidad Blog para poder hacer tests, así como la entidad Category, descrita un poco más abajo, dentro de la carpeta src/AppBundle/Entity. Hay que incluir el use del namespace del mapping de Symfony (use Doctrine\ORM\Mapping as ORM;) para que al utilizar la clase no genere problemas.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// src/AppBundle/Entity/Blog.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(
* name="blog",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="slug_idx", columns={"slug"})
* },
* indexes={
* @ORM\Index(name="category_idx", columns={"category_id"})
* })
*/
class Blog
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(type="integer", name="category_id")
*/
private $category;

/**
* @ORM\Column(type="string", length=255)
*/
private $title;

/**
* @ORM\Column(type="string", length=150)
*/
private $slug;

/**
* @ORM\Column(type="text")
*/
private $content;

/** @ORM\Column(type="datetime") */
private $created;

/** @ORM\Column(type="datetime") */
private $modified;

/** @ORM\Column(type="datetime", nullable=true) */
private $deleted = null;
}

Entidad Category:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/AppBundle/Entity/Category.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="category")
*/
class Category
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(type="string", length=255)
*/
private $name;

/** @ORM\Column(type="datetime") */
private $created;

/** @ORM\Column(type="datetime") */
private $modified;

/** @ORM\Column(type="datetime", nullable=true) */
private $deleted = null;
}

Observando estos ejemplos podemos interpretar un poco como funciona el sistema de anotaciones, en primer lugar informamos al sistema que estamos generando una clase que da soporte a una entidad de la base de datos. Esto se hace con la anotación encima del nombre de la clase, @ORM\Entity, a continuación definimos el nombre de la tabla con la que se corresponde mediante la anotación @ORM\Table(name="blog"), esta última no es obligatoria, en caso de no existir, se utilizaría el nombre de la clase para suponer el de la tabla en Base de Datos.

Para relacionar cada propiedad con su columna correspondiente en la tabla se utiliza la anotación @ORM\Column. Entre paréntesis podemos declarar más parámetros relativos al dato, como la longitud o si acepta valores null, siempre con el mismo formato, nombre=valor y separados por comas.

  • type
    Define el tipo de campo, una cadena de texto (string), un entero (integer), un texto largo (text)… si no se define, el campo se tratará como un string. Aquí tenemos una referencia de los types soportados por Symfony y el tipo de dato SQL equivalente.
  • name
    El nombre de la columna de la tabla en la base de datos, generalmente es el mismo que el nombre de la propiedad, si queremos diferenciarlos utilizaremos este atributo.
  • length
    La longitud máxima del valor en la base de datos. Limita la longitud total del tipo de dato definido, como podría pasar en MySQL, el varchar es de 255 caracteres pero nosotros lo podemos limitar a una longitud menor si así lo deseamos.
  • nullable
    Indica que el campo puede contener un valor null.

Si a una propiedad correspondiente a un campo le asignamos un valor, este será utilizado como el valor por defecto del campo.

Contamos con más anotaciones, como por ejemplo, para identificar la clave primaria @ORM\Id. En conjunción se puede indicar que la columna se autoincrementa respecto a la anterior en cada inserción, cada motor de base de datos lo define a su manera, mediante el ORM lo abstraemos y definimos con @ORM\GeneratedValue(strategy="AUTO"). Todo descrito en más profundidad en el documento enlazado antes.

También debemos prestar atención a los atributos en forma de array uniqueConstraints e indexes a traves de los cuales definiremos los índices y claves únicas de la tabla.

Solo queda indicar que si vamos a utilizar como nombre de una tabla, o de un campo, una palabra reservada, por ejemplo order, es mejor en la definición de la columna indicar el parámetro name con el valor entre acentos (ticks).

1
2
3
4
/**
* @ORM\Column(name="`order`, "type="number")
*/
private $order;

Completado de la clase

Una vez tenemos el mapeado de la entidad en una clase de PHP, le podemos pedir a doctrine que nos genere los getters y setters de cada propiedad privada que que hemos mapeado a nuestra entidad, siempre respetando lo que ya tenemos. Muchos IDE’s ya implementan esta funcionalidad, por ejemplo en netbeans, si pulsamos alt+insert nos mostrará un popup donde podremos seleccionar las propiedades sobre las que queremos generar estos getters y setters. En ambos casos se va a respetar el trabajo previo que tengamos ya hecho en estos métodos. Debajo el comando shell/cmd para la generación, siempre ejecutado desde la carpeta del proyecto.

1
2
3
php bin/console doctrine:generate:entities AppBundle/Entity/Blog

php bin/console doctrine:generate:entities AppBundle/Entity/Category

Gestión de la base de datos

Ahora que ya tenemos el modelo mapeado y con los suficientes métodos para ser usado podemos, mediante el comando symfony, hacer que los datos persistan en nuestro motor SQL. Desde el shell/cmd introducimos…

1
php bin/console doctrine:schema:update --force

Este comando es muy versátil, analiza las estructuras encontradas en la base de datos contra el mapeado que nosotros tengamos definido en la clase de la entidad correspondiente, en caso de encontrar diferencias se encargará de hacer los updates necesarios para sincronizarlas, y, en caso de no existir las creará con sus índices primarios, claves foráneas, etc…

Utilizando las entidades.

Con las clases de entidades ya completas y una vez creadas las tablas en la base de datos ya podemos empezar a utilizar los objetos dentro de nuestro proyecto para hacer persistir los datos que gestiona.

Dentro de nuestro Blog controller podemos instanciar la clase de entidad, con esta a través de la herencia y de la generación de getters y setters nos podremos realizar todas las operaciones pertinentes. Tomando la base ya hecha del blog vamos a desarrollar un poco más el controller, más tarde aprovecharemos estas Action para poblarlas con las acciones del CRUD que vayamos programando para el backend.

Creación de una entidad.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

use AppBundle\Entity\Blog;


class BlogController extends Controller
{
/**
* @Route("/blog/create", name="blog_create")
*/
public function createAction()
{
$blog = new Blog();
$blog->setCategory(1);
$blog->setTitle('My first blog post');
$blog->setSlug('my-first-blog-post');
$blog->setContent('lorem ipsum...');
$blog->setCreated(date_create(date('Y-m-d H:i:s')));
$blog->setModified(date_create(date('Y-m-d H:i:s')));

$em = $this->getDoctrine()->getManager();

// Indicamos que queremos guardar este registro.
$em->persist($blog);

// Ejecuta las querys de las operaciones indicadas.
$em->flush();

return new Response('Saved new $blog entry with id '.$blog->getId());
}

// Previous Code.

/**
* @Route("/blog/{page}", name="blog_list", requirements={"page": "\d+"})
*/
public function listAction($page=1)
{
return new Response(
'<html><body>Showing page number: '.$page.'</body></html>'
);
}

/**
* @Route("/blog/{title}", name="blog_read", requirements={"title": "\S+"})
*/
public function readAction($title)
{
return new Response(
'<html><body>Showing post:'.$title.'</body></html>'
);
}
}

Nada más acceder a la ruta http://localhost:8000/blog/create se ejecutará la acción que creará un registro en la base de datos, cuando veamos el mensaje de éxito podemos acceder a nuestro gestor BD para comprobar que realmente se guardado el registro. Vamos a desglosar un poco la operación.

  • Instanciamos la entidad Blog.
  • A través de los setter (setXxxxx) damos valores a las propiedades que se mapean con los campos de la tabla.
  • Obtenemos la instancia del Gestor de entidades de Doctrine, que será el encargado de hacer persistir los datos del objeto en la Base de datos.
  • Invocamos la función persist que se encargará de la operación de escritura, hay que tener en cuenta que Doctrine funciona en un ámbito tipo transaccional, de manera que tras realizar las operaciones pertinentes debemos indicarle que estas se fijan permanentemente en la BD, mediante el comando flush, que en este contexto funcionará como un commit en una transacción. En caso de haber algún problema con la operación se levantara una excepción de tipo Doctrine\ORM\ORMException.
  • Flush es realmente el método encargado de ejecutar las querys necesarias, Doctrine es consciente de las entidades que gestiona y a través de esta función las querys serán lanzadas en orden y en caso de ser posible aplicará estrategias de optimización para poder ejecutar todo en las menores operaciones posibles. Si por ejemplo realizamos 100 operaciones persist, flush intentará preparar una única query para realizar las 100 inserciones en BBDD.

Acceso a datos de entidades

Tras una operación de creación podemos codificar también una operación de lectura de los registros creados, al controlador le podremos añadir el siguiente método.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Route("/read/{slug}", name="blog_read", requirements={"slug": "\S+"})
*/
public function readAction($slug)
{
$blog = $this->getDoctrine()
->getRepository('AppBundle:Blog')
->findOneBySlug($slug);

if (!$blog) {
throw $this->createNotFoundException(
'No blog entry found for '.$slug
);
} else {
return new Response('<h1>'.$blog->getTitle().'</h1><br>'.$blog->getContent());
}
}

Podremos ver el contenido de la noticia accediendo a http://localhost:8000/read/my-first-blog-post. Al realizar querys sobre una determinada entidad podemos utilizar el repositorio, el repositorio es un objeto especializado en obtener datos de la base de datos para una determinada clase. De manera que al invocar

1
2
$repository = $this->getDoctrine()
->getRepository('AppBundle:Blog');

Doctrine se encargará de instanciar y devolvernos un objeto repositorio basado en la clase-entidad Blog. La notación AppBundle:Blog en realidad es un shorcut para evitar usar todo el namespace de la clase entidad en la que se va a basar. Es equivalente a AppBundle\Entity\Blog. Este shortcut funcionará siempre que nuestra entidad este alojada en el directorio estándar para las entidades, AppBundle/Entity. A la función de la creación del repositorio se le encadena la llamada al método findOneBySlug() encargado de obtener el registro Blog correspondiente al slug pasado como parámetro y devolverlo encapsulado en su propio objeto de entidad Blog.

En el repositorio tenemos a nuestra disposición distintos cauces para poder obtener registros de nuestra entidad.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$repository = $this->getDoctrine()->getRepository('AppBundle:Blog');

// Obtener el registro de una única noticia por la clave primaria (generalmente ID)
$blog = $repository->find($blogId);

// Nombres dinámicos de métodos para encontrar un único registro
// basándonos en el valor de una columna de la BD
$blog = $repository->findOneById($blogId);
$blog = $repository->findOneByTitle('My first blog post');

// Todos los métodos anteriores devuelven un objeto
echo $blog->getContent();

// Nombres dinámicos de métodos para encontrar un conjunto de registros
// basándonos en el valor de una columna de la BD
$blog = $repository->findByCategory(1);

// Encuentra *todas* las entradas de blog
$blog = $repository->findAll();

// Devolverán una colección de resultados
echo $blog[0]->getContent();

Para crear búsquedas un poco más complejas podremos utilizar los métodos findBy y findOneBy, que son métodos donde podremos utilizar más filtros para refinar las búsquedas.

1
2
3
4
5
6
7
8
9
10
11
12
$repository = $this->getDoctrine()->getRepository('AppBundle:Blog');

// Query para encontrar una sola entrada de blog con ese título y categoría
$blog = $repository->findOneBy(
array('title' => 'My first blog post', 'category' => 1)
);

// query para obtener múltiples registros en atención a la categoría y ordenados por fecha.
$blog = $repository->findBy(
array('category' => 1),
array('created' => 'DESC')
);

Modificación de registros

Supongamos que tenemos una ruta que mapea un id de noticia a una acción update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @Route("/blog/update/{blogId}", name="blog_update", requirements={"blogId": "\d+"})
*/
public function updateAction($blogId)
{
$em = $this->getDoctrine()->getManager();
$blog = $em->getRepository('AppBundle:Blog')->find($blogId);

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

$blog->setTitle('Updated blog entry!');
$em->flush();

return $this->redirectToRoute('blog_list');
}

http://localhost:8000/blog/update/1. Son tres sencillos pasos, a través del ID obtenemos el objeto de la base de datos, modificamos el atributo que nos interesa y finalmente invocamos la función flush(). En este caso no es necesario utilizar persist(), ya que al obtener el objeto del registro a través del entity manager, el propio flush() busca las modificaciones realizadas en el objeto y lo hace persistir. Persist() hace que el gestor de entidades setee un watch sobre un objeto ya existente y al hacer flush() busca sus cambios para guardarlo, en el caso anterior este watch ya viene implementado.

Borrado de registros

Tras obtener el objeto solo debemos utilizar el método remove y tras esto hacer el flush para lanzar la operación.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @Route("/blog/delete/{blogId}", name="blog_delete", requirements={"blogId": "\d+"})
*/
public function deleteAction($blogId)
{
$em = $this->getDoctrine()->getManager();
$blog = $em->getRepository('AppBundle:Blog')->find($blogId);

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

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

return $this->redirectToRoute('blog_list');
}

http://localhost:8000/blog/delete/1.

Relaciones entre las entidades

A través de las notaciones en comentarios podremos definir relaciones entre entidades, de manera que si las noticias del blog se relacionan con una categoría, podamos, al buscar una entrada del blog también recuperar el objeto de entidad de su categoría asociada. En este caso una categoría puede pertenecer a múltiples noticias y cada noticia solo se puede asociar a una categoría, el tipo de relación dependerá de dónde hagamos la notación, ya que la relación blog - categoría, descrita en la entidad Blog, es many (entradas de blog) to one (Categoría) pero la relación descrita en la entidad categoría (Categoría - blog) sera one (Categoría) to many (entradas de blog). En el segundo caso será necesario a su vez relacionar la propiedad de la relación que corresponde con el objeto Blog a un objeto tipo Collection para que al traer el registro de una categoría, esta pueda traer a su vez una colección con todas las noticias de Blog relacionadas. En caso de trabajar bajo este contexto deberemos tener cuidado con el alcance, ya que al pedir una simple categoría podremos recibir todas las noticias asociadas a esta con su consecuente carga en tiempo y memoria, de manera que en cada caso acotaremos la profundidad de la query según nos interese.

Los mapeos de las relaciones en el objeto se realizan sobre las propiedades que representan al campo índice de la relación.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/AppBundle/Entity/Blog.php

// ...
class Blog
{
// ...

/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="blogs")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
}


// src/AppBundle/Entity/Category.php

// ...
use Doctrine\Common\Collections\ArrayCollection;

class Category
{
// ...

/**
* @ORM\OneToMany(targetEntity="Blog", mappedBy="category")
*/
private $blogs;

public function __construct()
{
$this->blogs = new ArrayCollection();
}
}

En caso de definir la relación lo mejor sera recrear los getter/setter de las clases y hacer un force-update de la base de datos para que quede bien reflejado.

Construyendo querys con Doctrine

Para esta tarea Doctrine implementa un lenguaje denominado DQL que es muy similar al SQL estándar. Podríamos utilizar este lenguaje para construir querys muy complejas pero creo que es mejor no centrarnos en el lenguaje, si no en como construir estas querys a través de los métodos del QueryBuilder. Este objeto mediante nuestra parametrización se encargará de generar las sentencias DQL que precisemos. Si el lenguaje sufre variaciones, los propios objetos que lo construyen serán actualizados y seguirán generando querys válidas, mientras que si lo hacemos a través de consultas DQL escritas a mano tendremos que corregir aquellas que no se adapten a la nueva norma.

1
2
3
4
5
6
7
8
9
10
11
12
13
$repository = $this->getDoctrine()
->getRepository('AppBundle:Blog');

// createQueryBuilder() automatically selects FROM AppBundle:Blog
// and aliases it to "p"
$query = $repository->createQueryBuilder('p')
->where('p.id > :id')
->orderBy('p.created', 'ASC')
->getQuery();
$query->setParameter('id', 1);
$blogs = $query->getResult();
// to get just one result:
// $blog = $query->setMaxResults(1)->getOneOrNullResult();

FIN

Con esto he pasado un poco por encima de todo aquello que creo tiene algo de relevancia a la hora de trabajar con el ORM Doctrine de Symfony, toda la información se puede ampliar mediante la documentación oficial de Symfony. En la próxima sesión tratare de definir un poco más la estructura de datos que vamos a utilizar para el backend y hacer una pequeña presentación de los Bundles.