Otra característica importante que implementaremos en el Blog será la nube de etiquetas. La nube de etiquetas aparece en la página Home. La nube de etiquetas contiene las etiquetas más populares y el tamaño de la etiqueta varía dependiendo de la popularidad de la etiqueta: las etiquetas más populares son más grandes que las menos populares. Además, haciendo clic en una etiqueta de la nube de etiquetas conseguiremos una lista de publicaciones filtrada a partir de la etiqueta seleccionada.
Como un ejemplo de lo que queremos conseguir veamos el lado derecho de la figura 12.11 que se muestra abajo:
Para esta característica necesitamos las siguientes cosas:
PostRepository
, una clase tipo repositorio de entidades,
que encapsulará la lógica compleja que filtra las publicaciones a partir de
la etiqueta.PostManager
y agregar la función que calcula el tamaño de letra
para cada etiqueta de la nube.Antes mencionamos que por defecto Doctrine usa el Doctrine\ORM\EntityRepository
como la clase de tipo repositorio por defecto. Un repositorio a la medida es una
clase que extiende de la clase EntityRepository
. Se usa generalmente cuando necesitamos
encapsular consultas DQL complejas y a la lógica de búsqueda en un solo lugar de nuestro
código.
También es posible colocar las consultas DQL en la clase de tipo controlador pero esto hace "gordo" a nuestro controlador. Como usamos el patrón MVC nos esforzaremos por evitar el sobrepeso.
DQL se parece a SQL en el sentido de que ambos permiten escribir y ejecutar consultas en la base de datos, pero el resultado de una consulta DQL es un arreglo de objetos y no una arreglo de filas de una tabla. Para más información sobre DQL y ejemplos de uso vamos a revisar esta página.
Para nuestro Blog de ejemplo necesitamos un repositorio a la medida que permita
buscar las publicaciones publicadas que tengan al menos una etiqueta (para calcular
el total de publicaciones etiquetadas) y encontrar las publicaciones filtradas por
una etiqueta determinada. El plan es encapsular esta lógica de búsqueda dentro
del repositorio PostRepository
.
Doctrine trabaja con los repositorio escritos a la medida de manera trasparente. Esto significa que recuperamos el repositorio con el
EntityManager
como estamos acostumbrados y, además, podemos usar las métodosfindBy()
,findOneBy()
y los otros métodos.
Creamos el archivo PostRepository.php dentro de la carpeta Repository que esta
dentro de la carpeta fuente del módulo. Abajo encontraremos el código de las clase
PostRepository
que tiene dos métodos públicos:
findPostsHavingAnyTag()
está diseñado para seleccionar todas las
publicaciones que tienen como estado Published y, además, tienen una o más
etiquetas asignadas.findPostsByTag()
está diseñado para regresar todas las publicaciones
publicadas que tienen una etiqueta particular asignada (filtrar las publicaciones
por una etiqueta dada).<?php
namespace Application\Repository;
use Doctrine\ORM\EntityRepository;
use Application\Entity\Post;
// This is the custom repository class for Post entity.
class PostRepository extends EntityRepository
{
// Finds all published posts having any tag.
public function findPostsHavingAnyTag()
{
$entityManager = $this->getEntityManager();
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('p')
->from(Post::class, 'p')
->join('p.tags', 't')
->where('p.status = ?1')
->orderBy('p.dateCreated', 'DESC')
->setParameter('1', Post::STATUS_PUBLISHED);
$posts = $queryBuilder->getQuery()->getResult();
return $posts;
}
// Finds all published posts having the given tag.
public function findPostsByTag($tagName)
{
$entityManager = $this->getEntityManager();
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('p')
->from(Post::class, 'p')
->join('p.tags', 't')
->where('p.status = ?1')
->andWhere('t.name = ?2')
->orderBy('p.dateCreated', 'DESC')
->setParameter('1', Post::STATUS_PUBLISHED)
->setParameter('2', $tagName);
$posts = $queryBuilder->getQuery()->getResult();
return $posts;
}
}
En el código de arriba usamos el query builder para crear una consulta DQL compleja.
En las líneas 17-22 creamos una consulta que selecciona todas las entradas publicadas y las ordena de manera descendente por su fecha de creación. Como unimos las publicaciones con las etiquetas solamente estamos seleccionando las publicaciones que tienen por lo menos una etiqueta. En la línea 24 ejecutamos la consulta. Si tenemos curiosidad por ver que DQL crea el query builder veámoslo abajo:
SELECT p FROM \Application\Entity\Post p JOIN p.tags t
WHERE p.status=?1 ORDER BY p.dateCreated DESC
Entre las líneas 36-43 creamos una consulta que filtra las publicaciones a partir del nombre de la etiqueta. El DQL generado se presenta a continuación:
SELECT p FROM \Application\Entity\Post p JOIN p.tags t
WHERE p.status=?1 AND t.name=?2 ORDER BY p.dateCreated DESC
Para aprender más sobre el constructor de consultas de Doctrine podemos revisar esta página.
Para hacerle saber a Doctrine que debe usar un repositorio desarrollado a la medida
para la entidad Post
modificamos las anotaciones de la entidad de la siguiente
manera:
<?php
//...
/**
* This class represents a single post in a blog.
* @ORM\Entity(repositoryClass="\Application\Repository\PostRepository")
* @ORM\Table(name="post")
*/
class Post
{
//...
}
Arriba en la línea 6 usamos el parámetro repositoryClass
de la etiqueta @ORM\Entity
para decirle a Doctrine que debe usar el repositorio PostRepository
.
La lógica de negocio para la característica de nube de etiquetas se guardará dentro
del método PostManager::getTagCloud()
de la siguiente manera:
<?php
//...
class PostManager
{
//...
// Calculates frequencies of tag usage.
public function getTagCloud()
{
$tagCloud = [];
$posts = $this->entityManager->getRepository(Post::class)
->findPostsHavingAnyTag();
$totalPostCount = count($posts);
$tags = $this->entityManager->getRepository(Tag::class)
->findAll();
foreach ($tags as $tag) {
$postsByTag = $this->entityManager->getRepository(Post::class)
->findPostsByTag($tag->getName());
$postCount = count($postsByTag);
if ($postCount > 0) {
$tagCloud[$tag->getName()] = $postCount;
}
}
$normalizedTagCloud = [];
// Normalize
foreach ($tagCloud as $name=>$postCount) {
$normalizedTagCloud[$name] = $postCount/$totalPostCount;
}
return $normalizedTagCloud;
}
}
En el código de arriba tenemos el método getTagCloud()
que selecciona todas las
publicaciones que tienen por lo menos una etiqueta asociada y calcula la "frecuencia"
de cada etiqueta disponible (cuan a menudo una etiqueta aparece). Luego el método
normaliza la frecuencia de los valores (colocando los valores entre 0 y 1.0).
Aquí modificaremos la clase IndexController
para implementar el filtro de etiquetas.
<?php
//...
class IndexController extends AbstractActionController
{
/**
* Post manager.
* @var Application\Service\PostManager
*/
private $postManager;
// Constructor is used for injecting dependencies into the controller.
public function __construct($entityManager, $postManager)
{
$this->entityManager = $entityManager;
$this->postManager = $postManager;
}
public function indexAction()
{
$tagFilter = $this->params()->fromQuery('tag', null);
if ($tagFilter) {
// Filter posts by tag
$posts = $this->entityManager->getRepository(Post::class)
->findPostsByTag($tagFilter);
} else {
// Get recent posts
$posts = $this->entityManager->getRepository(Post::class)
->findBy(['status'=>Post::STATUS_PUBLISHED],
['dateCreated'=>'DESC']);
}
// Get popular tags.
$tagCloud = $this->postManager->getTagCloud();
// Render the view template.
return new ViewModel([
'posts' => $posts,
'postManager' => $this->postManager,
'tagCloud' => $tagCloud
]);
}
}
El método de acción recupera la etiqueta de la variable tag
que viene por GET, si
la variable no está en la petición HTTP todas las publicaciones son recuperadas
como antes. Si la variable está presente usamos el método findPostsByTag()
de nuestro repositorio creado a la medida para filtrar las publicaciones.
En la línea 36 llamamos al método PostManager::getTagCloud()
que regresa un arreglo
de etiquetas y sus frecuencias. Nosotros usamos esta información para imprimir
la nube.
Nótese que estamos usando el servicio
PostManager
en el controlador y tenemos que inyectarlo en el constructor. No olvidemos modificar el controlador tipo factory para hacer esto.
Finalmente modificamos el archivo index.phtml para que quede de la siguiente manera:
<h1>Posts</h1>
<div class="row">
<div class="col-md-8">
<?php foreach($posts as $post): ?>
<h3>
<a href="<?= $this->url('posts', ['action'=>'view', 'id'=>$post->getId()]); ?>">
<?= $this->escapeHtml($post->getTitle()); ?>
</a>
</h3>
<p>
Published: <?= $this->escapeHtml(date('jS \of F Y', strtotime($post->getDateCreated()))); ?>
| Tags: <?= $this->escapeHtml($postManager->convertTagsToString($post)); ?>
</p>
<p class="comments-header">
<?= $this->escapeHtml($postManager->getCommentCountStr($post)); ?> |
<a href="<?= $this->url('posts', ['action'=>'view', 'id'=>$post->getId()],
['fragment'=>'comment']); ?>">
Add new comment
</a>
</p>
<p>
<?= $this->escapeHtml($post->getContent()); ?>
</p>
<?php endforeach; ?>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Popular Tags</h3>
</div>
<div class="panel-body">
<?php foreach($this->tagCloud as $tagName=>$frequency): ?>
<a href="<?= $this->url('application', ['action'=>'index'],
['query'=>['tag'=>$tagName]]); ?>">
<span style="font-size:<?= $this->escapeHtml(0.9 + $frequency*3) ?>em">
<?= $this->escapeHtml($tagName); ?>
</span>
</a>
<?php endforeach; ?>
</div>
</div>
</div>
</div>