Web hosting

«  [Parte 4] — El modelo de comentarios: Agregando comentarios, repositorios y migraciones de Doctrine   ::   Contenido   ::   [Parte 6] — Pruebas: Unitarias y funcionales con PHPUnit  »

[Parte 5] — Personalizando la vista: extensiones Twig, la barra lateral y Assetic

Descripción

En este capítulo continuaremos construyendo la interfaz de usuario para symblog. Vamos a modificar la página inicial para mostrar información acerca de los comentarios de un blog publicado y abordaremos el SEO añadiendo el título del blog a la URL. También vamos a comenzar a trabajar en la barra lateral para agregar 2 componentes comunes en sitios de blog; La nube de etiquetas y comentarios recientes. Vamos a explorar los diferentes entornos con que contamos en Symfony2 y aprenderemos a manejar symblog en el entorno de producción. El motor de plantillas Twig será ampliado para proporcionar un nuevo filtro, e introduciremos Assetic para gestionar los archivos de activos del sitio web. Al final de este capítulo habremos integrado los comentarios a la página principal, tendremos una nube de etiquetas y el componente de comentarios recientes en la barra lateral y habremos utilizado Assetic para gestionar los archivos de nuestros activos web. También habremos visto cómo ejecutar symblog en el entorno de producción.

La página inicial — Blogs y Comentarios

Hasta ahora, la página inicial muestra las entradas más recientes del blog, pero no proporciona ninguna información respecto a los comentarios de los blogs. Ahora que hemos construido la entidad Comentario podemos volver a la página inicial y proporcionarle esta información. Puesto que hemos establecido la relación entre las entidades Blog y Comentario sabemos que Doctrine 2 será capaz de recuperar los comentarios de un blog (recuerda que hemos añadido un miembro $comments a la entidad Blog). Actualicemos la plantilla de la vista de la página inicial situada en src/Blogger/BlogBundle/Resources/views/Page/index.html.twig con lo siguiente:

{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}

{# .. #}

<footer class="meta">
    <p>Comments: {{ blog.comments|length }}</p>
    <p>Posted by <span class="highlight">{{ blog.author }}</span> at {{ blog.created|date('h:iA') }}</p>
    <p>Tags: <span class="highlight">{{ blog.tags }}</span></p>
</footer>

{# .. #}

Hemos utilizado el captador comments para recuperar los comentarios del blog y luego depuramos la colección con el filtro length de Twig. Si echas un vistazo a la página inicial vía http://symblog.dev/app_dev.php/ verás que ahora exhibe el número de comentarios de cada blog.

Como se explicó anteriormente, ya informamos a Doctrine 2 que el miembro $comments de la entidad Blog está relacionado a la entidad Comment. Habíamos logrado esto en el capítulo anterior con los siguientes metadatos en la entidad Blog ubicada en src/Blogger/BlogBundle/Entity/Blog.php.

// src/Blogger/BlogBundle/Entity/Blog.php

/**
 * @ORM\OneToMany(targetEntity="Comment", mappedBy="blog")
 */
protected $comments;

Por lo tanto, sabemos que Doctrine 2 está consciente de la relación entre blogs y comentarios, pero ¿cómo poblamos el miembro $comments con las entidades Comments relacionadas? Si recuerdas de nuevo el método BlogRepository que hemos creado (mostrado a continuación) obtiene la página inicial con los blogs sin haber hecho ninguna selección para recuperar las entidades Comments relacionadas.

// src/Blogger/BlogBundle/Repository/BlogRepository.php

public function getLatestBlogs($limit = null)
{
    $qb = $this->createQueryBuilder('b')
               ->select('b')
               ->addOrderBy('b.created', 'DESC');

    if (false === is_null($limit))
        $qb->setMaxResults($limit);

    return $qb->getQuery()
              ->getResult();
}

Sin embargo, Doctrine 2 utiliza un proceso llamado carga diferida donde las entidades Comment se recuperan de la base de datos hasta cuando sean necesarias, en nuestro caso, en cuanto invocamos a {{ blog.comments|length }}. Podemos demostrar este proceso utilizando la barra de depuración web. Ya hemos comenzado a explorar los fundamentos de la barra de depuración web y ahora es tiempo de introducir una de sus características más útiles, el generador de perfiles de Doctrine 2. Puedes acceder al generador de perfiles de Doctrine 2 haciendo clic en el último icono de la barra de depuración web. El número al lado de este icono indica la cantidad de consultas que se ejecutaron en la base de datos para la petición HTTP actual.

Barra de depuración web - icono de Doctrine 2

Si haces clic en el icono de Doctrine 2 se te presentará información relacionada a las consultas que Doctrine 2 ejecutó en la base de datos para la petición HTTP actual.

Barra de depuración web - consultas de Doctrine 2

Como puedes ver en la captura de pantalla anterior, hay una serie de consultas que se ejecutan para una petición a la página principal. La segunda consulta ejecutada recupera de la base de datos las entidades Blog y se ejecuta como resultado del método getLatestBlogs() en la clase BlogRepository. Siguiendo esta consulta te darás cuenta de una serie de consultas que obtienen los comentarios desde la base de datos, un blog a la vez. Podemos ver esto a causa de la WHERE t0.blog_id = ? en cada una de las consultas, donde la ? se sustituye por el valor del parámetro (el id del blog) en la siguiente línea. Cada una de estas consultas son el resultado de las llamadas a {{ blog.comments }} en la plantilla de la página inicial. Cada vez que se ejecuta esta función, Doctrine 2 tiene que cargar —de manera diferida— la entidad Comentario relacionada con la entidad Blog.

Si bien la carga diferida es muy eficiente recuperando entidades relacionadas desde la base de datos, no siempre es el camino más eficaz para lograr esta tarea. Doctrine 2 ofrece la posibilidad de unir las entidades relacionadas entre sí al consultar la base de datos. De esta manera podemos extraer desde la base de datos las entidades Blog y Comentarios relacionadas en una única consulta. Actualiza el código del QueryBuilder situado en src/Blogger/BlogBundle/Repository/BlogRepository.php para unirlo con los comentarios.

// src/Blogger/BlogBundle/Repository/BlogRepository.php

public function getLatestBlogs($limit = null)
{
    $qb = $this->createQueryBuilder('b')
               ->select('b, c')
               ->leftJoin('b.comments', 'c')
               ->addOrderBy('b.created', 'DESC');

    if (false === is_null($limit))
        $qb->setMaxResults($limit);

    return $qb->getQuery()
              ->getResult();
}

Si ahora actualizas la página web y examinas la salida de Doctrine 2 en la barra de depuración web notarás que el número de consultas se ha reducido. También puedes ver que la tabla de comentarios se ha unido a la tabla blog.

La carga diferida y la unión de entidades relacionadas, ambos son conceptos muy poderosos, pero se tienen que usar correctamente. Tienes que encontrar el balance correcto entre los dos para que tu aplicación se ejecute con la mayor eficiencia posible. Al principio puede parecer grandioso unir todas las entidades relacionadas para que nunca tengas que cargar de manera diferida y siempre mantener al mínimo la cantidad de consultas a la base de datos. Sin embargo, es importante recordar que mientras más información recuperes de la base de datos, más procesamiento tiene que hacer Doctrine 2 para hidratar los objetos entidad. Más datos también significa utilizar más memoria del servidor para almacenar los objetos entidad.

Antes de continuar hagamos una pequeña adición a la plantilla de la página inicial para agregar el número de comentarios que acabamos de añadir. Actualiza la plantilla de la página inicial ubicada en src/Blogger/BlogBundle/Resources/views/Page/index.html.twig añadiéndole un enlace para mostrar los comentarios del blog.

{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}

{# .. #}

<footer class="meta">
    <p>Comments: <a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id }) }}#comments">{{ blog.comments|length }}</a></p>
    <p>Posted by <span class="highlight">{{ blog.author }}</span> at {{ blog.created|date('h:iA') }}</p>
    <p>Tags: <span class="highlight">{{ blog.tags }}</span></p>
</footer>

{# .. #}

La barra lateral

Actualmente la barra lateral de symblog se está viendo un tanto vacía. Vamos a actualizarla con dos componentes de blog comunes, una nube de etiquetas y una lista con los comentarios recientes.

Nube de etiquetas

La nube de etiquetas muestra las etiquetas de cada blog enfatizando en ciertas formas audaces para mostrar las etiquetas más comunes. Para lograr esto, necesitamos una manera de recuperar todas las etiquetas de todos los blogs. Para ello, vamos a crear algunos nuevos métodos en la clase BlogRepository. Actualiza la clase BlogRepository situada en src/Blogger/BlogBundle/Repository/BlogRepository.php con lo siguiente:

// src/Blogger/BlogBundle/Repository/BlogRepository.php

public function getTags()
{
    $blogTags = $this->createQueryBuilder('b')
                     ->select('b.tags')
                     ->getQuery()
                     ->getResult();

    $tags = array();
    foreach ($blogTags as $blogTag)
    {
        $tags = array_merge(explode(",", $blogTag['tags']), $tags);
    }

    foreach ($tags as &$tag)
    {
        $tag = trim($tag);
    }

    return $tags;
}

public function getTagWeights($tags)
{
    $tagWeights = array();
    if (empty($tags))
        return $tagWeights;

    foreach ($tags as $tag)
    {
        $tagWeights[$tag] = (isset($tagWeights[$tag])) ? $tagWeights[$tag] + 1 : 1;
    }
    // Revuelve las etiquetas
    uksort($tagWeights, function() {
        return rand() > rand();
    });

    $max = max($tagWeights);

    // un peso máximo de 5
    $multiplier = ($max > 5) ? 5 / $max : 1;
    foreach ($tagWeights as &$tag)
    {
        $tag = ceil($tag * $multiplier);
    }

    return $tagWeights;
}

Dado que las etiquetas se almacenan en la base de datos como valores separados por comas (CSV) necesitamos una manera de dividirlo y devolverlo como una matriz. Esto se logra mediante el método getTags(). El método getTagWeights() es capaz de utilizar una matriz de etiquetas para calcular el peso de cada etiqueta en base a su popularidad dentro de la matriz. Las etiquetas también se barajan para determinar su aleatoriedad de exhibición en la página.

Ahora que somos capaces de generar la nube de etiquetas, tenemos que mostrarla. Crea una nueva acción en el PageController que se encuentra en src/Blogger/BlogBundle/Controller/PageController.php para manejar la barra lateral.

// src/Blogger/BlogBundle/Controller/PageController.php

public function sidebarAction()
{
    $em = $this->getDoctrine()
               ->getEntityManager();

    $tags = $em->getRepository('BloggerBlogBundle:Blog')
               ->getTags();

    $tagWeights = $em->getRepository('BloggerBlogBundle:Blog')
                     ->getTagWeights($tags);

    return $this->render('BloggerBlogBundle:Page:sidebar.html.twig', array(
        'tags' => $tagWeights
    ));
}

La acción es muy simple, esta utiliza los dos nuevos métodos del BlogRepository para generar la nube de etiquetas, y la pasa a la vista. Ahora vamos a crear esa vista situada en src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig.

{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}

<section class="section">
    <header>
        <h3>Tag Cloud</h3>
    </header>
    <p class="tags">
        {% for tag, weight in tags %}
            <span class="weight-{{ weight }}">{{ tag }}</span>
        {% else %}
            <p>There are no tags</p>
        {% endfor %}
    </p>
</section>

La plantilla también es muy simple. Esta sólo itera en las distintas etiquetas ajustando una clase para el peso de la etiqueta. El bucle for introduce la forma de acceder a los pares clave y valor de la matriz, donde tag es la clave y weight es el valor. Hay una serie de variaciones sobre el uso del bucle for provistas en la documentación de Twig.

Si vuelves a mirar en la plantilla del diseño principal de BloggerBlogBundle, ubicada en src/Blogger/BlogBundle/Resources/views/base.html.twig te darás cuenta de que pusimos un marcador de posición para el bloque de la barra lateral. Vamos a sustituirlo ahora reproduciendo la nueva acción sidebar. Recuerda del capítulo anterior que el método render de Twig reproducirá el contenido de una acción del controlador, en este caso la acción sidebar del controlador Page.

{# src/Blogger/BlogBundle/Resources/views/base.html.twig #}

{# .. #}

{% block sidebar %}
    {% render "BloggerBlogBundle:Page:sidebar" %}
{% endblock %}

Por último vamos a añadir el CSS necesario para la nube de etiquetas. Añade una nueva hoja de estilos situada en src/Blogger/BlogBundle/Resources/public/css/sidebar.css.

.sidebar .section { margin-bottom: 20px; }
.sidebar h3 { line-height: 1.2em; font-size: 20px; margin-bottom: 10px; font-weight: normal; background: #eee; padding: 5px;  }
.sidebar p { line-height: 1.5em; margin-bottom: 20px; }
.sidebar ul { list-style: none }
.sidebar ul li { line-height: 1.5em }
.sidebar .small { font-size: 12px; }
.sidebar .comment p { margin-bottom: 5px; }
.sidebar .comment { margin-bottom: 10px; padding-bottom: 10px; }
.sidebar .tags { font-weight: bold; }
.sidebar .tags span { color: #000; font-size: 12px; }
.sidebar .tags .weight-1 { font-size: 12px; }
.sidebar .tags .weight-2 { font-size: 15px; }
.sidebar .tags .weight-3 { font-size: 18px; }
.sidebar .tags .weight-4 { font-size: 21px; }
.sidebar .tags .weight-5 { font-size: 24px; }

Debido a que hemos añadido una nueva hoja de estilos necesitamos incluirla. Actualiza la plantilla del diseño principal de BloggerBlogBundle, ubicada en src/Blogger/BlogBundle/Resources/views/base.html.twig con lo siguiente:

{# src/Blogger/BlogBundle/Resources/views/base.html.twig #}

{# .. #}

{% block stylesheets %}
    {{ parent() }}
    <link href="{{ asset('bundles/bloggerblog/css/blog.css') }}" type="text/css" rel="stylesheet" />
    <link href="{{ asset('bundles/bloggerblog/css/sidebar.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}

{# .. #}

Nota

Si no estás usando el método de enlace simbólico para hacer referencia a los activos de tu paquete en el directorio web, ahora debes volver a ejecutar la tarea de instalación de activos para copiar el nuevo archivo CSS.

$ php app/console assets:install web

Si ahora actualizas la página de symblog en tu navegador verás la nube de etiquetas en la barra lateral. A fin de obtener las etiquetas para reproducirlas con diferentes pesos, posiblemente tengas que actualizar los accesorios del blog para que algunas etiquetas se utilicen más que otras.

Comentarios recientes

Ahora la nube de etiquetas está en su lugar, vamos a agregar también el componente de comentarios recientes a la barra lateral.

Primero necesitamos una manera de recuperar los comentarios más recientes de los blogs. Para ello vamos a añadir un nuevo método al CommentRepository situado en src/Blogger/BlogBundle/Repository/CommentRepository.php.

<?php
// src/Blogger/BlogBundle/Repository/CommentRepository.php

public function getLatestComments($limit = 10)
{
    $qb = $this->createQueryBuilder('c')
                ->select('c')
                ->addOrderBy('c.id', 'DESC');

    if (false === is_null($limit))
        $qb->setMaxResults($limit);

    return $qb->getQuery()
              ->getResult();
}

A continuación actualiza la acción de la barra lateral situada en src/Blogger/BlogBundle/Controller/PageController.php para recuperar los comentarios más recientes y pasarlos a la vista.

// src/Blogger/BlogBundle/Controller/PageController.php

public function sidebarAction()
{
    // ..

    $commentLimit   = $this->container
                           ->getParameter('blogger_blog.comments.latest_comment_limit');
    $latestComments = $em->getRepository('BloggerBlogBundle:Comment')
                         ->getLatestComments($commentLimit);

    return $this->render('BloggerBlogBundle:Page:sidebar.html.twig', array(
        'latestComments'    => $latestComments,
        'tags'              => $tagWeights
    ));
}

Notarás que hemos utilizado un nuevo parámetro denominado blogger_blog.comments.latest_comment_limit para limitar el número de comentarios recuperados. Para crear este parámetro actualiza tu archivo de configuración ubicado en src/Blogger/BlogBundle/Resources/config/config.yml con lo siguiente:

# src/Blogger/BlogBundle/Resources/config/config.yml

parameters:
    # ..

    # Blogger máximo de comentarios recientes
    blogger_blog.comments.latest_comment_limit: 10

Por último, debemos reproducir los comentarios recientes en la plantilla de la barra lateral. Actualiza la plantilla ubicada en src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig con lo siguiente:

{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}

{# .. #}

<section class="section">
    <header>
        <h3>Latest Comments</h3>
    </header>
    {% for comment in latestComments %}
        <article class="comment">
            <header>
                <p class="small"><span class="highlight">{{ comment.user }}</span> commented on
                    <a href="{{ path('BloggerBlogBundle_blog_show', { 'id': comment.blog.id }) }}#comment-{{ comment.id }}">
                        {{ comment.blog.title }}
                    </a>
                    [<em><time datetime="{{ comment.created|date('c') }}">{{ comment.created|date('Y-m-d h:iA') }}</time></em>]
                </p>
            </header>
            <p>{{ comment.comment }}</p>
            </p>
        </article>
    {% else %}
        <p>There are no recent comments</p>
    {% endfor %}
</section>

Si ahora actualizas la página de symblog en tu navegador verás que se muestran los comentarios recientes en la barra lateral bajo la nube de etiquetas.

Barra lateral - Nube de etiquetas y comentarios recientes

Extensiones Twig

Hasta ahora hemos estado mostrando las fechas de los comentarios del blog en un formato de fecha estándar, tal como 04/21/2011. Un enfoque mucho mejor sería mostrar las fechas de los comentarios en términos de cuánto tiempo hace que fue publicado el comentario, tal como publicado hace 3 horas. Podríamos añadir un método a la entidad Comentario para lograrlo y cambiar las plantillas para utilizar este método en lugar de {{ comment.created | date('Ymd h: iA') }}.

Debido a que posiblemente desees utilizar esta funcionalidad en otro lugar tendría más sentido ponerla fuera de la entidad Comentario. Puesto que la transformación de fechas es una tarea específica para la capa de la vista, la debemos implementar utilizando el motor de plantillas Twig. Twig nos proporciona esta habilidad, proveyendo una interfaz Extensión.

Podemos utilizar la Interfaz extensión en Twig para extender la funcionalidad predeterminada que ofrece. Vamos a crear una nueva extensión de filtro Twig que podamos utilizar de la siguiente manera.

{{ comment.created|created_ago }}

Esta devolverá la fecha de creación del comentario en un formato como publicado hace 2 días.

La extensión

Crea un archivo para la extensión de Twig situado en src/Blogger/BlogBundle/Twig/Extensions/BloggerBlogExtension.php y actualízalo con el siguiente contenido:

<?php
// src/Blogger/BlogBundle/Twig/Extensions/BloggerBlogExtension.php

namespace Blogger\BlogBundle\Twig\Extensions;

class BloggerBlogExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
            'created_ago' => new \Twig_Filter_Method($this, 'createdAgo'),
        );
    }

    public function createdAgo(\DateTime $dateTime)
    {
        $delta = time() - $dateTime->getTimestamp();
        if ($delta < 0)
            throw new \Exception("createdAgo is unable to handle dates in the future");

        $duration = "";
        if ($delta < 60)
        {
            // Segundos
            $time = $delta;
            $duration = $time . " second" . (($time > 1) ? "s" : "") . " ago";
        }
        else if ($delta <= 3600)
        {
            // Minutos
            $time = floor($delta / 60);
            $duration = $time . " minute" . (($time > 1) ? "s" : "") . " ago";
        }
        else if ($delta <= 86400)
        {
            // Horas
            $time = floor($delta / 3600);
            $duration = $time . " hour" . (($time > 1) ? "s" : "") . " ago";
        }
        else
        {
            // Días
            $time = floor($delta / 86400);
            $duration = $time . " day" . (($time > 1) ? "s" : "") . " ago";
        }

        return $duration;
    }

    public function getName()
    {
        return 'blogger_blog_extension';
    }
}

La creación de la extensión es bastante simple. Redefinimos el método getFilters() para devolver cualquier cantidad de filtros que deses estén disponibles. En este caso, estamos creando el filtro created_ago. Entonces registramos este filtro para usar el método createdAgo, el cual simplemente transforma un objeto DateTime en una cadena que representa el lapso de tiempo transcurrido desde que el valor fue almacenado en el objeto DateTime.

Registrando la extensión

Para que la extensión de Twig esté disponible tenemos que actualizar el archivo de servicios que se encuentra en src/Blogger/BlogBundle/Resources/config/services.yml con lo siguiente:

services:
    blogger_blog.twig.extension:
        class: Blogger\BlogBundle\Twig\Extensions\BloggerBlogExtension
        tags:
            - { name: twig.extension }

Como puedes ver estamos registrando un nuevo servicio usando la clase BloggerBlogExtension de la extensión Twig que acabamos de crear.

Actualizando la vista

El nuevo filtro de Twig está listo para utilizarlo. Actualicemos la lista de comentarios recientes en la barra lateral para usar el filtro created_ago. Actualiza la plantilla de la barra lateral situada en src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig con lo siguiente:

{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}

{# .. #}

<section class="section">
    <header>
        <h3>Latest Comments</h3>
    </header>
    {% for comment in latestComments %}
        {# .. #}
        <em><time datetime="{{ comment.created|date('c') }}">{{ comment.created|created_ago }}</time></em>
        {# .. #}
    {% endfor %}
</section>

Si ahora diriges tu navegador a http://symblog.dev/app_dev.php/ verás que las fechas de los comentarios recientes utilizan el filtro Twig para pintar el lapso transcurrido desde que fue publicado el comentario.

También debemos actualizar los comentarios que aparecen en la página show del blog para mostrarte cómo se usa ahí el nuevo filtro. Remplaza el contenido en la plantilla ubicada en src/Blogger/BlogBundle/Resources/views/Comment/index.html.twig con lo siguiente:

{# src/Blogger/BlogBundle/Resources/views/Comment/index.html.twig #}

{% for comment in comments %}
    <article class="comment {{ cycle(['odd', 'even'], loop.index0) }}" id="comment-{{ comment.id }}">
        <header>
            <p><span class="highlight">{{ comment.user }}</span> commented <time datetime="{{ comment.created|date('c') }}">{{ comment.created|created_ago }}</time></p>
        </header>
        <p>{{ comment.comment }}</p>
    </article>
{% else %}
    <p>There are no comments for this post. Be the first to comment...</p>
{% endfor %}

Truco

Hay una serie de útiles extensiones de Twig disponibles a través de la biblioteca Twig-Extensions en GitHub. Si creas una extensión útil envíala a través de una petición de extracción a este depósito y posiblemente sea incluida para que otras personas la usen.

Autodenominando la URL

Actualmente la URL de cada entrada del blog sólo muestra el id del blog. Si bien esto es perfectamente aceptable desde el punto de vista funcional, no es el ideal para SEO. Por ejemplo, la URL http://symblog.dev/1 no da ninguna información sobre el contenido del blog, algo como http://symblog.dev/1/a-day-with-symfony2 sería mucho mejor. Para lograr esto se autodenomina el título del blog y lo utilizamos como parte de esta URL. Al autodenominar el título se eliminarán todos los caracteres no ASCII y los reemplazará con un -.

Actualizando el enrutado

Para comenzar vamos a modificar la regla de enrutado para mostrar la página del blog agregando el componente slug. Actualiza la regla de enrutado ubicada en src/Blogger/BlogBundle/Resources/config/routing.yml para que tenga esta apariencia:

# src/Blogger/BlogBundle/Resources/config/routing.yml

BloggerBlogBundle_blog_show:
    pattern:  /{id}/{slug}
    defaults: { _controller: BloggerBlogBundle:Blog:show }
    requirements:
        _method:  GET
        id: \d+

El controlador

Como ocurre con el componente id existente, el nuevo componente slug se pasa como argumento a la acción del controlador, por lo tanto vamos a actualizar el controlador que se encuentra en src/Blogger/BlogBundle/Controller/BlogController.php para reflejar esto:

// src/Blogger/BlogBundle/Controller/BlogController.php

public function showAction($id, $slug)
{
    // ..
}

Truco

El orden en que se pasan los argumentos a la acción del controlador no importa, sólo importa el nombre. Symfony2 es capaz de hacer coincidir los argumentos de enrutado con la lista de parámetros por nosotros. A pesar de que todavía no hemos utilizado los valores predeterminados del los componentes vale la pena mencionarlos aquí. Si añadimos otro componente a la regla de enrutado podemos especificar un valor predeterminado para el componente usando la opción defaults.

BloggerBlogBundle_blog_show:
    pattern:  /{id}/{slug}/{comments}
    defaults: { _controller: BloggerBlogBundle:Blog:show, comments: true }
    requirements:
        _method:  GET
        id: \d+
public function showAction($id, $slug, $comments)
{
    // ..
}

Usando este método, una petición a http://symblog.dev/1/symfony2-blog resultaría en establecer $comments a true en el showAction.

Autodenominando desde el título

Debido a que deseamos generar el slug a partir del título del blog, generaremos dicho valor automáticamente. Simplemente podríamos realizar esta operación en tiempo de ejecución en el campo del título, pero vamos a guardar el slug en la entidad Blog y la persistiremos en la base de datos.

Actualizando la entidad Blog

Vamos a añadir una nueva propiedad a la entidad Blog para almacenar el slug. Actualiza la entidad Blog ubicada en src/Blogger/BlogBundle/Entity/Blog.php:

// src/Blogger/BlogBundle/Entity/Blog.php

class Blog
{
    // ..

    /**
     * @ORM\Column(type="string")
     */
    protected $slug;

    // ..
}

Ahora genera los captadores para la nueva propiedad $slug. Al igual que antes, ejecuta la siguiente tarea:

$ php app/console doctrine:generate:entities Blogger

A continuación, actualicemos el esquema de la base de datos:

$ php app/console doctrine:migrations:diff
$ php app/console doctrine:migrations:migrate

Para generar el valor del slug vamos a utilizar el método slugify de la guía Jobeet de Symfony 1. Añade el método slugify a la entidad Blog localizada en src/Blogger/BlogBundle/Entity/Blog.php

// src/Blogger/BlogBundle/Entity/Blog.php

public function slugify($text)
{
    // sustituye caracteres de espaciado o dígitos con un -
    $text = preg_replace('#[^\\pL\d]+#u', '-', $text);

    // recorta espacios en ambos extremos
    $text = trim($text, '-');


    // translitera
    if (function_exists('iconv'))
    {
        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
    }

    // cambia a minúsculas
    $text = strtolower($text);

    // elimina caracteres indeseables
    $text = preg_replace('#[^-\w]+#', '', $text);

    if (empty($text))
    {
        return 'n-a';
    }

    return $text;
}

Puesto que deseamos generar automáticamente el slug del título podemos generar el slug al establecer el valor del título. Para ello podemos actualizar el accessor setTitle para que también fije el valor del slug. Actualiza la entidad blog ubicada en src/Blogger/BlogBundle/Entity/Blog.php con lo siguiente:

// src/Blogger/BlogBundle/Entity/Blog.php

public function setTitle($title)
{
    $this->title = $title;

    $this->setSlug($this->title);
}

A continuación actualiza el método setSlug para autodenominar el slug antes de establecerlo:

// src/Blogger/BlogBundle/Entity/Blog.php

public function setSlug($slug)
{
    $this->slug = $this->slugify($slug);
}

Ahora recarga los accesorios para generar los slugs del blog:

$ php app/console doctrine:fixtures:load

Actualizando las rutas generadas

Finalmente necesitamos actualizar las llamadas existentes para generar rutas a la página para mostrar el blog. Hay una serie de ubicaciones que necesitas actualizar.

Abre la plantilla de la página inicial situada en src/Blogger/BlogBundle/Resources/views/Page/index.html.twig y reemplaza su contenido con lo siguiente. Ha habido 3 ediciones para generar la ruta BloggerBlogBundle_blog_show en esta plantilla. Al editar sencillamente pasas el slug del blog a la función path de Twig:

{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}

{% extends 'BloggerBlogBundle::base.html.twig' %}

{% block body %}
    {% for blog in blogs %}
        <article class="blog">
            <div class="date"><time datetime="{{ blog.created|date('c') }}">{{ blog.created|date('l, F j, Y') }}</time></div>
            <header>
                <h2><a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id, 'slug': blog.slug }) }}">{{ blog.title }}</a></h2>
            </header>

            <img src="{{ asset(['images/', blog.image]|join) }}" />
            <div class="snippet">
                <p>{{ blog.blog(500) }}</p>
                <p class="continue"><a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id, 'slug': blog.slug }) }}">Continue reading...</a></p>
            </div>

            <footer class="meta">
                <p>Comments: <a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id, 'slug': blog.slug }) }}#comments">{{ blog.comments|length }}</a></p>
                <p>Posted by <span class="highlight">{{ blog.author }}</span> at {{ blog.created|date('h:iA') }}</p>
                <p>Tags: <span class="highlight">{{ blog.tags }}</span></p>
            </footer>
        </article>
    {% else %}
        <p>There are no blog entries for symblog</p>
    {% endfor %}
{% endblock %}

También, necesitas hacer una actualización para la sección de Comentarios de la barra lateral en la plantilla situada en src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig:

{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}

{# .. #}

<a href="{{ path('BloggerBlogBundle_blog_show', { 'id': comment.blog.id, 'slug': comment.blog.slug }) }}#comment-{{ comment.id }}">
    {{ comment.blog.title }}
</a>

{# .. #}

Finalmente, necesitas modificar la función createAction del CommentController para que redirijas a la página show del blog cuando ocurra un envío de comentario exitoso. Actualiza el CommentController situado en src/Blogger/BlogBundle/Controller/CommentController.php con lo siguiente:

// src/Blogger/BlogBundle/Controller/CommentController.php

public function createAction($blog_id)
{
    // ..

    if ($form->isValid()) {
        // ..

        return $this->redirect($this->generateUrl('BloggerBlogBundle_blog_show', array(
            'id'    => $comment->getBlog()->getId(),
            'slug'  => $comment->getBlog()->getSlug())) .
            '#comment-' . $comment->getId()
        );
    }

    // ..
}

Ahora si navegas a la página inicial de symblog en http://symblog.dev/app_dev.php/ y haces clic en el título de un blog verás que se ha añadido el slug del blog al final de la URL.

Entornos

Los entornos son muy potentes, incluso las más sencillas características proporcionadas por Symfony2. Posiblemente no te hayas dado cuenta, pero has estado usando entornos desde la parte 1 de esta guía. Con entornos podemos configurar varios aspectos de Symfony2 y la aplicación se ejecuta de manera diferente según las necesidades específicas durante el ciclo de vida de las aplicaciones. De manera predeterminada Symfony2 viene configurado con 3 entornos:

  1. dev — Desarrollo
  2. test — Prueba
  3. prod — Producción

El propósito de estos entornos se explica automáticamente, pero deberías configurar estos entornos de manera diferente para cubrir tus necesidades individuales. Cuando desarrolles tu aplicación es útil tener en pantalla la barra de depuración web mostrándote las excepciones y errores descriptivos, mientras que en producción no deseas nada de esto. De hecho, exhibir esta información sería un riesgo de seguridad debido a que expone una gran cantidad de detalles internos de tu aplicación y el servidor. En producción es mejor mostrar páginas de error personalizadas con mensajes simplificados, mientras silenciosamente registra esta información en archivos de texto. También sería útil tener activada la capa de caché para asegurarte que la aplicación está corriendo lo mejor optimizada posible. Tener habilitada la capa de caché en el entorno de desarrollo sería molesto puesto que necesitarías vaciar la caché cada vez que hicieras cambios en los archivos de configuración, etc.

El otro entorno es el entorno de prueba. Este se utiliza cuándo ejecutas pruebas en tu aplicación, tanto pruebas unitarias como funcionales. No hemos cubierto las pruebas todavía, pero el resto te lo aseguró, será cubierto a fondo en los capítulos venideros.

Controladores frontales

Hasta ahora, a través de esta guía sólo hemos utilizado el entorno de desarrollo. Hemos especificado la ejecución del entorno de desarrollo al utilizar el controlador frontal app_dev.php cuándo hacemos peticiones a symblog, por ejemplo, http://symblog.dev/app_dev.php/about. Si le damos un vistazo al controlador localizado en web/app_dev.php notarás la siguiente línea:

$kernel = new AppKernel('dev', true);

Esta línea es la qué arranca el núcleo de Symfony2. Esta inicia una nueva instancia del AppKernel de Symfony2 y configura el entorno a dev.

En contraste, si miramos el controlador frontal para el entorno de producción situado en web/app.php veremos lo siguiente:

$kernel = new AppKernel('prod', false);

Puedes ver que el entorno prod se pasa a la instancia del AppKernel.

El entorno de prueba no se puede ejecutar vía un navegador web puesto que no existe un controlador frontal app_test.php.

Opciones de configuración

Hemos visto cómo utilizar los controladores frontales para cambiar el entorno bajo el que se ejecuta la aplicación. Ahora exploraremos cómo modificar varias opciones mientras las ejecutamos bajo cada entorno. Si le das una mirada al directorio app/config verás una serie de archivos config.yml. Específicamente hay uno principal, llamado config.yml Y otros 3 sufijados con el nombre de un entorno; config_dev.yml, config_test.yml y config_prod.yml. Cada uno de estos archivos se carga según el entorno actual. Si exploramos el archivo config_dev.yml, verás las siguientes líneas en la parte superior:

imports:
    - { resource: config.yml }

La directiva imports provocará que se incluya el archivo config.yml a este archivo. Puedes encontrar la misma directiva imports en la parte superior de los otros 2 archivos de configuración de entorno, config_test.yml y config_prod.yml. Al incluir un conjunto de opciones de configuración comunes definidas en config.yml podremos redefinir opciones específicas para cada entorno. Podemos ver en el archivo de configuración del entorno desarrollo ubicado en app/config/config_dev.yml las siguientes líneas configurando el uso de la barra de depuración web.

# app/config/config_dev.yml

web_profiler:
    toolbar: true

Este ajuste está ausente en el archivo de configuración del entorno de producción, puesto que no queremos mostrar la barra de depuración web.

Ejecutando en producción

Para aquellos que están ansiosos de ver su sitio ejecutándose en el entorno de producción, ahora es el momento.

Primero necesitamos limpiar la caché utilizando una de las tareas de Symfony2:

$ php app/console cache:clear --env=prod

Ahora apunta tu nevegador a http://symblog.dev/. Notarás la ausencia del controlador frontal app_dev.php.

Nota

Para aquellos que están utilizando la configuración de servidor virtual dinámico como lo describimos en la parte 1, deberán añadir lo siguiente al archivo .htaccess localizado en web/.htaccess.

<IfModule mod_rewrite.c>
    RewriteBase /
    # ..
</IfModule>

Notarás que la página se ve más o menos igual, pero algunas importantes características sustanciales son diferentes. Ahora se ha ido la barra de depuración web y ya no se muestran los detallados mensajes de excepción, intenta ir a http://symblog.dev/999.

Producción - Error 404

El detallado mensaje de excepción se tiene que reemplazar por un mensaje simplificado que informa del problema al usuario. Puedes personalizar estas pantallas de excepción para adaptarlas a la apariencia y sentido de tu aplicación. Exploraremos esto más adelante en otros capítulos.

Además notarás que los archivos de registro app/logs/prod.log se rellenan con registros relacionados a la ejecución de tu aplicación. Este es un punto útil para revisar cuándo tienes cuestiones con tu aplicación en producción, tal como errores y excepciones que no se ven más en pantalla.

Truco

¿Cómo hace la petición a http://symblog.dev/ para terminar siendo enrutada a través del archivo app.php? Estoy seguro que todos habrán utilizado archivos como índex.html e índex.php para que actúen como el índice de sus sitios, pero cómo hizo esto app.php. Esto es gracias a una RewriteRule en el archivo web/.htaccess:

RewriteRule ^(.*)$ app.php [QSA,L]

Podemos ver que esta línea tiene una expresión regular que coincide con cualquier texto, denotada por ^(.*)$ y lo pasa a app.php.

Quizá en tu servidor Apache no tengas habilitado el mod_rewrite.c. Si este es el caso sencillamente puedes añadir app.php a la URL, tal como esta http://symblog.dev/app.php/.

Si bien, hemos cubierto las bases del entorno de producción, no hemos cubierto muchas otras tareas relacionadas al entorno de producción, tal como personalizar páginas de error, y desplegar al servidor de producción utilizando herramientas como capifony. Estos temas serán cubiertos en capítulos posteriores.

Creando nuevos entornos

Finalmente vale la pena resaltar que en Symfony2 puedes configurar tus propios entornos fácilmente. Por ejemplo, posiblemente desees un entorno para escenificar la ejecución en el servidor de producción, pero produciendo alguna información para depuración, tal como excepciones. Esto permitiría probar manualmente la plataforma en el servidor de producción real cuando la configuración de los servidores de producción y el entorno de desarrollo pueden diferir.

Si bien, crear un nuevo entorno es una tarea sencilla, está fuera del alcance de esta guía. Aquí tienes un excelente artículo en el recetario de Symfony2 que cubre el tema.

Assetic

La Edición estándar de Symfony2 viene con una biblioteca para gestionar activos llamada Assetic. La biblioteca fue desarrollada por Kris Wallsmith y fue inspirada en la biblioteca webassets de Python.

Assetic trata con 2 partes de la gestión de activos, los activos, tal como imágenes, hojas de estilo, JavaScript, y los filtros que puedes aplicar a estos activos. Estos filtros son capaces de realizar tareas útiles como minimizar (minifying) tus CSS y JavaScript, pasando archivos CoffeeScript a través del compilador CoffeeScript, y combinando los archivos de activos para reducir la cantidad de peticiones HTTP hechas al servidor.

Hasta ahora, hemos estado utilizando la función asset de Twig para incluir activos en las plantillas como sigue:

<link href="{{ asset('bundles/bloggerblog/css/blog.css') }}" type="text/css" rel="stylesheet" />

Las llamadas a la función asset serán reemplazadas por Assetic.

Assets

La biblioteca Assetic describe un activo de la siguiente manera:

Un activo de Assetic es algo con contenido filtrable que se puede cargar y verter. Un activo también incluye metadatos, algunos de los cuales se pueden manipular y algunos son inmutables.

En pocas palabras, los activos son los recursos que usa tu aplicación, tal como hojas de estilo e imágenes.

Stylesheets

Empecemos reemplazando las llamadas actuales a asset para las hojas de estilo en la plantilla del diseño principal del BloggerBlogBundle. Actualiza el contenido de la plantilla ubicada en src/Blogger/BlogBundle/Resources/views/base.html.twig con lo siguiente:

{# src/Blogger/BlogBundle/Resources/views/base.html.twig #}

{# .. #}

{% block stylesheets %}
    {{ parent() }}

    {% stylesheets
        '@BloggerBlogBundle/Resources/public/css/*'
    %}
        <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
    {% endstylesheets %}
{% endblock %}

{# .. #}

Hemos reemplazado los 2 enlaces anteriores de archivos CSS con alguna funcionalidad Assetic. Utilizando stylesheets desde Assetic especificamos que todos los archivos CSS ubicados en src/Blogger/BlogBundle/Resources/public/css se tendrán que combinar en 1 archivo el cual será servido. Combinar archivos es una muy sencilla pero eficaz manera de optimizar la interfaz de usuario de tu sitio web al reducir el número de archivos necesarios. Menos archivos significa menos peticiones HTTP al servidor. Mientras utilizamos el * para especificar todos los archivos en el directorio css, sencillamente, podríamos haber listado cada archivo individualmente de la siguiente manera:

{# src/Blogger/BlogBundle/Resources/views/base.html.twig #}

{# .. #}

{% block stylesheets %}
    {{ parent() }}

    {% stylesheets
        '@BloggerBlogBundle/Resources/public/css/blog.css'
        '@BloggerBlogBundle/Resources/public/css/sidebar.css'
    %}
        <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
    {% endstylesheets %}
{% endblock %}

{# .. #}

El resultado final en ambos casos es el mismo. La primer opción utilizando el * garantiza que cuándo añadas nuevos archivos CSS al directorio, Assetic siempre los incluirá al combinar los archivos CSS. Esta posiblemente no sea la funcionalidad deseada para tu sitio web, así que utiliza cualquier método de los anteriores que más convenga a tus necesidades.

Si le das una mirada al HTML producido vía http://symblog.dev/app_dev.php/ verás que el CSS ha incluido algo como esto (Aviso, otra vez volvimos a ejecutar el entorno de desarrollo).

<link href="/app_dev.php/css/d8f44a4_part_1_blog_1.css" rel="stylesheet" media="screen" />
<link href="/app_dev.php/css/d8f44a4_part_1_sidebar_2.css" rel="stylesheet" media="screen" />

En primer lugar quizás estés preguntándote por qué hay 2 archivos. Arriba estamos declarando que Assetic combinaría los archivos CSS en uno solo. Esto es porque estamos ejecutando symblog en el entorno de desarrollo. Podemos provocar que Assetic no se ejecute en modo de depuración poniendo el indicador de depuración en falso de la siguiente manera:

{# src/Blogger/BlogBundle/Resources/views/base.html.twig #}

{# .. #}

{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    debug=false
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}

{# .. #}

Ahora, si miras el HTML producido verás algo parecido a esto:

<link href="/app_dev.php/css/3c7da45.css" rel="stylesheet" media="screen" />

Si ves el contenido de este archivo notarás que los dos archivos CSS, blog.css y sidebar.css se han combinado en un solo archivo. El nombre de archivo dado al archivo CSS generado es generado aleatoriamente por Assetic. Si quisieras controlar el nombre del archivo generado utiliza la opción output de la siguiente manera:

{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    output='css/blogger.css'
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}

Antes de continuar remueve el fragmento del indicador de depuración anterior cuando queramos resumir el comportamiento predeterminado en los activos.

También necesitamos actualizar la plantilla base de la aplicación localizada en app/Resources/views/base.html.twig.

{# app/Resources/views/base.html.twig #}

{# .. #}

{% block stylesheets %}
    <link href='http://fonts.googleapis.com/css?family=Irish+Grover' rel='stylesheet' type='text/css'>
    <link href='http://fonts.googleapis.com/css?family=La+Belle+Aurore' rel='stylesheet' type='text/css'>
    {% stylesheets
        'css/*'
    %}
        <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
    {% endstylesheets %}
{% endblock %}

{# .. #}

JavaScript

Si bien, actualmente no tenemos ningún archivo JavaScipt en nuestra aplicación, su uso en Assetic es muy similar al utilizado con las hojas de estilo.

{% javascripts
    '@BloggerBlogBundle/Resources/public/js/*'
%}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}

Filtros

El poder real de Assetic viene de los filtros. Puedes aplicar filtros a activos o colecciones de activos. Hay una gran cantidad de filtros proporcionados dentro del núcleo de la biblioteca que incluye los siguientes filtros comunes:

  1. CssMinFilter: minimiza archivos CSS
  2. JpegoptimFilter: optimiza tus imágenes JPEG
  3. Yui\CssCompressorFilter: comprime CSS usando el compresor YUI
  4. Yui\JsCompressorFilter: comprime JavaScript usando el compresor YUI
  5. CoffeeScriptFilter: compila CoffeeScript a JavaScript

Hay una completa lista de filtros disponibles en el Assetic Readme.

Muchos de estos filtros pasan la tarea real a otro programa o biblioteca, tal como YUI Compresor, así que posiblemente necesites instalar/configurar las bibliotecas apropiadas para utilizar algunos filtros.

Descarga el YUI Compresor, extrae su contenido y copia el archivo localizado en el directorio build a app/Resources/java/yuicompressor-2.4.6.jar.
Esto, asumiendo que hayas descargado la versión 2.4.6 del YUI Compressor. De lo contrario, cambia tu número de versión consecuentemente.

Luego configuraremos un filtro Assetic para minimizar CSS utilizando el YUI Compresor. Actualiza el archivo de configuración de tu aplicación localizado en app/config/config.yml con el siguiente contenido:

# app/config/config.yml

# ..

assetic:
    filters:
        yui_css:
            jar: %kernel.root_dir%/Resources/java/yuicompressor-2.4.6.jar

# ..

Hemos configurado un filtro llamado yui_css que utilizará el ejecutable YUI Compresor de Java que colocamos en el directorio de recursos de la aplicación. A fin de utilizar el filtro, necesitas especificar a qué activos quieres aplicar el filtro. Actualiza la plantilla situada en src/Blogger/BlogBundle/Resources/views/base.html.twig para aplicar el filtro yui_css:

{# src/Blogger/BlogBundle/Resources/views/base.html.twig #}

{# .. #}

{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    output='css/blogger.css'
    filter='yui_css'
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}

{# .. #}

Ahora si actualizas en tu navegador el sitio web de symblog y ves el archivo producido por Assetic notarás que se ha minimizado. Mientras que la minimización es buena para servidores de producción, puede dificultar considerablemente la depuración, especialmente cuándo minimizas JavaScript. Podemos inhabilitar la minimización cuándo ejecutas el entorno de desarrollo prefijando el filtro con un ? de la siguiente manera:

{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    output='css/blogger.css'
    filter='?yui_css'
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}

Volcando los activos para producción

En producción podemos volcar los archivos de activos utilizando Assetic para convertirlos en los recursos en disco reales listos para servirlos en el sitio web. El proceso de crear los activos a través de Assetic en cada petición de página puede llegar a ser bastante lento, especialmente cuándo los filtros se están aplicando a los activos. Volcar los activos para producción garantiza que Assetic no se utiliza para servir activos y en cambio los archivos de activos preprocesados se están sirviendo directamente al sitio web. Ejecuta la siguiente tarea para crear los archivos de activos:

$ app/console --env=prod assetic:dump

Notarás que se ha creado una serie de archivos CSS en web/css incluyendo el archivo combinado blogger.css. Ahora si actualizas en tu navegador el sitio web de symblog en el entorno de producción vía http://symblog.dev/ se servirán los archivos directamente de este directorio.

Nota

Si vuelcas los archivos de activos a disco y quieres regresar al entorno de desarrollo, necesitarás limpiar los archivos de activos creados en web/ para permitir que Assetic los vuelva a crear.

Lectura adicional

Sólo hemos arañado la superficie de lo que Assetic puede hacer. Hay mucho más recursos disponibles en línea, especialmente en el recetario de Symfony2 incluyendo:

Cómo usar Assetic para gestionar activos

Cómo minimizar JavaScript y hojas de estilo con YUI Compressor

Cómo usar Assetic para optimizar imágenes con funciones Twig

Cómo aplicar un filtro Assetic a una extensión de archivo específica

Hay una gran colección de artículos escritos por Richard Miller incluyendo:

Symfony2: Usando CoffeeScript con Assetic

Symfony2: Algunas notas sobre Assetic

Symfony2: Funciones de Assetic para Twig

Truco

Vale la pena mencionar aquí, que Richard Miller tiene una colección de excelentes artículos relacionados a una serie de áreas en tu sitio Symfony2, incluyendo la Inyección de Dependencias, Servicios y las guías de Assetic mencionadas anteriormente. Solo busca correos etiquetados con symfony2

Conclusión

Hemos cubierto una serie de áreas nuevas concernientes a Symfony2 incluyendo los entornos de Symfony2 y cómo utilizar la biblioteca Assetic para gestionar nuestros activos. También hicimos mejoras a la página inicial y añadimos algunos componentes a la barra lateral.

En el siguiente capítulo cubriremos las pruebas. Exploraremos ambas, pruebas unitarias y pruebas funcionales usando PHPUnit. Vamos a ver cómo Symfony2 viene con una serie de clases para ayudarte a escribir pruebas funcionales que simulan peticiones web, nos permiten llenar formularios y hacer clic en enlaces y luego inspeccionar la respuesta devuelta.

comentarios del blog accionados por Disqus

«  [Parte 4] — El modelo de comentarios: Agregando comentarios, repositorios y migraciones de Doctrine   ::   Contenido   ::   [Parte 6] — Pruebas: Unitarias y funcionales con PHPUnit  »