A free and open-source book on ZF3 for beginners

Translation into this language is not yet finished. You can help this project by translating the chapters and contributing your changes.

12.14. Implementar la Nube de Etiquetas

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:

Figure 12.11. Nube de Etiquetas Figure 12.11. Nube de Etiquetas

Para esta característica necesitamos las siguientes cosas:

12.14.1. Agregar el Post Repository Hecho a la Medida

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étodos findBy(), 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:

<?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.

12.14.2. Calcular Etiquetas de la Nube

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).

12.14.3. Modificar la Acción en el Controlador

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.

12.14.4. Imprimir la Nube de Etiquetas

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>

Top