De ahora en más cuando nos refiramos a una tabla dentro de Symfony hablaremos de un entity o entidad ya que este último es la forma en que Symfony ve a las tablas es decir objetos. Recordemos que nuestras entidades se encuentran dentro de nuestro Bundle en de carpeta src\MDW\DemoBundle\Entity\ y tenemos mapeadas las tablas “articles” y “comments”.

El acceso a nuestras entidades se hará por medio de un objeto de Doctrine llamado EntityManager que vamos a decir que sería como el administrador de las entidades y quién sabrá como interactuar con ellas. Para obtener este objeto simplemente, dentro de nuestro action lo invocamos de la siguiente manera:

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

Con esto ya tenemos la referencia a este objeto dentro de una variable “$em”. Ahora bien, para trabajar con los datos necesitaremos de un repositorio. Este repositorio sería el objeto que nos permite solicitar y actualizar datos y para obtenerlo simplemente usamos el nombre lógico de nuestra entidad de esta manera:

$em->getRepository('MDWDemoBundle:Articles');

Obteniendo Datos

Para obtener datos de las tablas tenemos varios métodos realmente mágicos:

  • findAll(): Obtiene todos los registros de la tabla. Retorna un array.
  • find(): Obtiene un registro a partir de la clave primaria de la tabla.
  • findBy(): Obtiene los registros encontrados pudiendo pasar como argumentos los valores que irían dentro del WHERE. Retorna un array.
  • findOneBy(): obtiene un registro pudiendo pasar como argumentos los valores que irían dentro del WHERE.

Veamos unos ejemplos de la utilización de estos métodos:

$em = $this->getDoctrine()->getEntityManager();
//-- Obtenemos todos los artículos de la tabla
$articulos = $em->getRepository('MDWDemoBundle:Articles')->findAll();
//-- Obtenemos el artículo con el id igual a 5
$articulo = $em->getRepository('MDWDemoBundle:Articles')->find(5);
//-- Obtenemos el artículo cuyo slug sea "articulo-1"
$articulos = $em->getRepository('MDWDemoBundle:Articles')->findOneBy(array('slug' => 'articulo-1'));
//-- Obtenemos todos los artículos de autor John Doe que sean de la categoría "Symfony"
$articulos = $em->getRepository('MDWDemoBundle:Articles')->findBy(
	array(
		'author' => 'John Doe',
		'category' => 'Symfony'
	)
);

Caso de ejemplo

Para realizar nuestros ejemplos, crearemos unas páginas para nuestras pruebas. Primeramente como ya vimos en el capítulo 3 tenemos que crear nuestras rutas en el archivo src\MDW\DemoBundle\Resources\config\routing.yml, por lo tanto agreguemos el siguiente código:

articulo_listar:
    pattern:  /articulos/listar
    defaults: { _controller: MDWDemoBundle:Articulos:listar }

articulo_crear:
    pattern:  /articulos/crear
    defaults: { _controller: MDWDemoBundle:Articulos:crear }

articulo_editar:
    pattern:  /articulos/editar/{id}
    defaults: { _controller: MDWDemoBundle:Articulos:editar }

articulo_visualizar:
    pattern:  /articulos/visualizar/{id}
    defaults: { _controller: MDWDemoBundle:Articulos:visualizar }

articulo_borrar:
    pattern:  /articulos/borrar/{id}
    defaults: { _controller: MDWDemoBundle:Articulos:borrar }

Como segundo paso crearemos un nuevo controlador dentro de nuestro Bundle con el nombre ArticulosController.php. El archivo lo crearemos dentro de src\MDW\DemoBundle\Controller y contendrá inicialmente el siguiente código con 5 actions para nuestras pruebas, uno por cada ruta creada anteriormente:

<?php

namespace MDW\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use MDW\DemoBundle\Entity\Articles;

class ArticulosController extends Controller
{
	public function listarAction()
	{

	}

	public function crearAction()
	{

	}

	public function editarAction($id)
	{

	}

	public function borrarAction($id)
	{

	}
}

Ahora crearemos dos plantillas para usarlas como visualización de respuesta de nuestros actions. Los archivos los crearemos en  src\MDW\DemoBundle\Resources\views\Articulos\ con los nombres listar.html.twigarticulo.html.twig.

  • listar.html.twig: Conforme un array de artículos crea una tabla para mostrar datos:
<h1>Listado de Articulos</h1>
<table border="1">
    <tr>
        <th>ID</th>
        <th>Titulo</th>
        <th>Fecha de Creacion</th>
    </tr>
    {% for articulo in articulos %}
    <tr>
        <td>{{ articulo.id }}</td>
        <td>{{ articulo.title }}</td>
        <td>{{ articulo.created | date('d-m-Y') }}</td>
    </tr>
    {% endfor %}
</table>
  • articulo.html.twig: Conforme a un artículo muestra sus datos:
<h1>Articulo con ID {{ articulo.id }}</h1>
<ul>
    <li>Titulo: {{ articulo.title }}</li>
    <li>Fecha de creacion: {{ articulo.created | date('d-m-Y') }}</li>
</ul>

Ahora ya tenemos nuestro código inicial y así que comencemos a trabajar con los datos.

Manipulando datos

1. Inserción de datos
Primeramente trabajaremos en el método crearAction() de nuestro ArticulosController en donde usaremos la entidad Articles para insertar registros. Para esto es bueno notar que en las primeras lineas de nuestro controlador estamos importando el namespace de la entidad con la palabra reservada “use”.

Crearemos un objeto nuevo de la manera tradicional:

$articulo = new Articles();
$articulo->setTitle('Articulo de ejemplo 1');
$articulo->setAuthor('John Doe');
$articulo->setContent('Contenido');
$articulo->setTags('ejemplo');
$articulo->setCreated(new \DateTime());
$articulo->setUpdated(new \DateTime());
$articulo->setSlug('articulo-de-ejemplo-1');
$articulo->setCategory('ejemplo');

Usando los setters insertamos los datos y pongamos atención en que no usamos el setId() ya que le dijimos a nuestra entidad que se encargue de generarlo por medio de la anotación @ORM\GeneratedValue(strategy=”AUTO”). Tambień notemos que para asignar las fechas de creación y modificación cargamos un objeto DateTime nuevo agregándole una barra invertida adelante para especificar que es una clase del CORE de php ya que si no la ponemos va a esperar que importemos un namespace tal cual como lo hicimos con la clase Articles.

Una vez que tenemos nuestro objeto creado por medio del EntityManager le diremos que sea insertado con el siguiente código:

$em = $this->getDoctrine()->getEntityManager();
$em->persist($articulo);
$em->flush();

Con el método persist() le decimos que el objeto pasado por argumento sea guardado para ser insertado y la inserción en sí se realizará cuando ejecutemos el método flush(). Con esta orden Doctrine generará la sentencia INSERT necesaria. Finalmente vamos a invocar a nuestra plantilla a quién le pasaremos el artículo para que muestre sus datos:

return $this->render('MDWDemoBundle:Articulos:articulo.html.twig', array('articulo' => $articulo));

2. Actualizando datos
La actualización de datos es exactamente igual a la inserción con la diferencia que estamos modificando un objeto Articles ya existente, para este ejemplo crearemos el siguiente código en el método editarAction($id):

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

$articulo = $em->getRepository('MDWDemoBundle:Articles')->find($id);

$articulo->setTitle('Articulo de ejemplo 1 - modificado');
$articulo->setUpdated(new \DateTime());

$em->persist($articulo);
$em->flush();

return $this->render('MDWDemoBundle:Articulos:articulo.html.twig', array('articulo' => $articulo));

El método obtiene el id que llega por GET y por medio del EntityManager obtiene el registro y nos devuelve el objeto al cual, usando los setters asignamos los cambios y de la misma forma que la inserción ejecutamos el persist() y el flush(). Esto creará un update en lugar de un insert ya que doctrine puede identificar perfectamente que el artículo ya existe en la base de datos porque ya fue persistido con anterioridad.

3. Mostrando un listado de artículos
Para obtener todos los artículos de una entidad ya vimos el método findAll() por lo que tendremos el siguiente código dentro del método listarAction() para luego enviar el array de Articles a la vista que se encargará de mostrarlos en una tabla.

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

$articulos = $em->getRepository('MDWDemoBundle:Articles')->findAll();

return $this->render('MDWDemoBundle:Articulos:listar.html.twig', array('articulos' => $articulos));

4. Eliminar un artículo
Para eliminar un artículo simplemente lo obtenemos e invocamos el método remove() que marcará el artículo para ser borrado hasta que ejecutemos el método flush(). Este código lo pondremos en el método borrarAction()

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

$articulo = $em->getRepository('MDWDemoBundle:Articles')->find($id);

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

return $this->redirect(
    $this->generateUrl('articulo_listar')
);

Una vez que lo borramos, redirigimos al usuario a la ruta “articulo_listar”.

Formas alternativas de obtener datos

1. Extensión de los métodos findBy() y findOneBy()
Los métodos findBy() y findOneBy() tienen otros métodos similares conocidos como findBy*() y findOneBy*() donde el asterísco representa cualquier propiedad de nuestra entidad:

//-- Obtenemos todos los artículos de la categoría 'Symfony'
$articulos = $em->getRepository('MDWDemoBundle:Articles')->findByCategory('Symfony');
//-- Obtenemos el artículo con el slug 'artículo-1'
$articulo = $em->getRepository('MDWDemoBundle:Articles')->findOneBySlug('articulo-1');

2. Utilizando las claves foráneas
Otra de las formas de obtener datos es por medio de las claves foráneas las cuales fueros configuradas en nuestra entidad por medio de los annotatios @ManyToOne, @OneToMany. Una vez que obtemos por ejemplo un artículo podríamos obtener todos sus comentarios de la siguiente manera.

$em = $this->getDoctrine()->getEntityManager();
$articulo = $em->getRepository('MDWDemoBundle:Articles')->findOneBySlug('articulo-de-ejemplo-1');
$comentarios = $articulo->getComments();

foreach($comentarios as $c)
{
    echo $c->getContent();
}

Al utilizar la clave foránea configurada en nuestra entidad invocando al getter getComments(), doctrine se encargará se generar la sentencia SELECT necesaria para obtener todos los comentarios.

3. Generando DQL
Por si las formas de obtener datos que ya vimos nos quedan cortas, cosa que por lo general es así, Doctrine nos permite trabajar con algo muy parecido al SQL estándar al que estamos acostumbrados a trabajar solo que como estamos trabajando con el ORM se llama DQL es decir Doctrine Query Language.

El DQL es realmente muy parecido al SQL con la diferencia que en lugar de hacer queries contra registros de las tablas, los hacemos sobre objetos de tipo Entity, por ejemplo un select bien sencillo:

SELECT * FROM articles

en DQL sería:

select a from MDWDemoBundle:Articles a

donde la “a” es nada más que un simple alias que podemos llamar como queramos. El cambio principal se nota en que en lugar se hacer referencia a la tabla articles estamos haciendo referencia a la entidad MDWDemoBundle:Articles. Con esta sintaxis estamos dejando que doctrine se encargue de la traducción al SQL necesario para el motor de base de datos utilizado y configurado inicialmente.

También es posible pedir solo algunos campos y no un SELECT * poniendo los nombres de las propiedades del objeto usando el alias:

select a.id, a.title, c.author from MDWDemoBundle:Articles a

Para decirle a Doctrine que ejecute este DQL lo hacemos a través del EntityManager de la siguiente manera:

$em = $this->getDoctrine()->getEntityManager();
$dql = "select a from MDWDemoBundle:Articles a";
$query = $em->createQuery($dql);
$articulos = $query->getResult();

Con el código anterior utilizamos el DQL para generar un objeto de Doctrine llamado “Doctrine_Query” representado por $query y luego a este objeto le pedimos que nos devuelva los resultados invocando al getResult() lo que nos devolverá un array de objetos Articles y para acceder a sus datos simplemente utilizamos los getters del objeto. Por ejemplo si quisieramos recorrer el array de articulos y obtener el id lo haríamos así ya que siguen siendo objetos metidos dentro de un array:

foreach($articulos as $articulo)
{
    $id = $articulo->getId();
    $title = $articulo->getTitle();
}

En caso de necesitar pasar filtros para el WHERE, podemos hacerlo usando el método setParameter() del objeto Doctrine_Query de la siguiente manera:

$em = $this->getDoctrine()->getEntityManager();
$dql = "select a from MDWDemoBundle:Articles a where a.author=:author and a.title like :title";
$query = $em->createQuery($dql);
$query->setParameter('author', 'John Doe');
$query->setParameter('title', '%ejemplo 1%');
$articulos = $query->getResult();

Con la utilización del setParameter() ya no nos preocupamos de poner por ejemplo comillas a los filtros que no son numéricos ya que Doctrine ya sabe de que tipo de dato es cada columna por medio de la definición que hicimos de la entidad.

También tenemos por supuesto soporte para unir entidades por medio de la cláusula JOIN por lo que este SQL estándar lo podríamos convertir a DQL de la siguiente manera:

$em = $this->getDoctrine()->getEntityManager();
$dql = "select a.id, a.title, c.author
        from MDWDemoBundle:Comments c
        join c.article a
        where a.author=:author
        and a.title like :title";
$query = $em->createQuery($dql);
$query->setParameter('author', 'John Doe');
$query->setParameter('title', '%ejemplo 1%');
$articulos = $query->getResult();

Hay una diferencia a la hora de obtener los datos. Ya que estamos obteniendo una mezcla de datos de articulos y comentarios, el método getResult() nos devuelve todo ya directamente en un array como siempre estuvimos acostumbrados a trabajar con PDO por lo tanto la estructura del array devuelto sería la siguiente:

Array
(
    [0] => Array
    (
        [id] => 4
        [title] => Articulo de ejemplo 1
        [author] => Autor 1
    )
    [1] => Array
    (
        [id] => 4
        [title] => Articulo de ejemplo 1
        [author] => Autor 2
    )
)

4. Utilizando el Repositorio
En el capítulo anterior cuando nos ocupamos de crear nuestras entidades Articles y Comments con el generador doctrine:entity:create, se nos hizo una pregunta   sobre si queríamos que el generador nos cree un repositorio vacío para la entidad a crear y hemos dicho que sí. Esto hizo que se cree un segundo archivo a parte de la entidad llamado NombreEntidadRepository.php. Para nuestro ejemplo hemos creado el ArticlesRepository.php y el CommentsRepository.php.

Estos archivos se utilizan para organizar sentencias DQL de una entidad en cuestión. Por ejemplo, en lugar de tener todos los códigos de DQL escritos más arriba esparcidos por nuestros Controladores, podríamos ( y deberíamos para mantener el código más ordenado y mantenible)  tener todos las consultas relacionadas con los articulos dentro de nuestro repositorio ArticlesRepository.php. Esto es muy útil ya que desde el primer capítulo hablamos que Symfony intenta mantener todas las cosas en su lugar y es realmente útil.

En este mismo momento nuestro repositorio de artículos se encuentra de la siguiente manera:

<?php
namespace MDW\DemoBundle\Entity;
use Doctrine\ORM\EntityRepository;
/**
 * ArticlesRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class ArticlesRepository extends EntityRepository
{
}

Dentro de esta clase crearemos métodos que serán cada una de nuestras consultas . Fijémonos que la clase hereda de EntityRepository lo cual ya nos da ciertas ventajas por ejemplo que para obtener el EntityManager simplemente tenemos que invocarlo como $this->getEntityManager(). Así que podríamos tener por ejemplo un método para obtener artículos de un autor con un cierto contenido en el título creando el siguiente método:

public function findArticlesByAuthorAndTitle($author, $title)
{
    $em = $this->getEntityManager();

    $dql = "select a.id, a.title, c.author
        from MDWDemoBundle:Comments c
        join c.article a
        where a.author=:author
        and a.title like :title";

    $query = $em->createQuery($dql);
    $query->setParameter('author', $author);
    $query->setParameter('title', '%' . $title . '%');

    $articulos = $query->getResult();

    return $articulos;
}

Una vez que tengamos nuestros métodos en el repositorio lo accedemos de la misma forma que los métodos find() o findAll() ya vistos dentro de nuestros actions en los controladores:

$em = $this->getDoctrine()->getEntityManager();
$articulos = $em->getRepository('MDWDemoBundle:Articles')->findArticlesByAuthorAndTitle('John Doe', 'ejemplo 1');

En cada repositorio podemos tener la cantidad de métodos que necesitemos y hasta podríamos hacer que métodos genéricos que reutilicen otros métodos de la misma clase lo cual, si pensamos en una aplicación que puede llegar a utilizar muchas sentencias SQL lograríamos tener un código mucho más ordenado y por supuesto también tenemos identificado donde pueden ocurrir los errores ya que cada cosa está en su lugar.

Resumen Final

Si pensáramos nuevamente en la arquitectura Model-View-Controller (MVC) estaríamos viendo que la presentación de los datos (View) se encuentran en las plantillas, la programación del modelado de datos y trabajo con los mismos (Model) se encuentran en las Entidades y Repositorios.

Por último nos queda la lógica de la aplicación (Controller) que se mantendrá dentro de los actions programados dentro de cada Controlador. Esto al mirarlo desde un enfoque de arquitectura de software demuestra un proyecto muy ordenado y por sobre todo con facilidades de mantener y escalar a futuro, con un equipo de desarrollo donde cada quién maneja su parte. Algo que siempre suelo decir es que si una página llega a tener más de 20 líneas de código es porque nos estamos olvidando de modularizar y es probable que una de las partes del MVC no se encuentre en el lugar correcto.

En el siguiente capítulo, el último orientado al modelado, hablaremos sobre la validación de los datos que Doctrine nos proporciona y sobre como Symfony nos ayuda a crear objetos de tipo formularios para que los usuarios finales interactuen con la aplicación.