Creating a blog on Symfony 2.8 lts [Part 5]

Published on June 11, 2016

Creating a blog on Symfony 2.8 lts [Part 5]

  • Tutorial





Github Project
You can find out how to install the part of the manual you need in the description of the repository by the link . (For example, if you want to start with this lesson without going through the previous one)


Homepage. Posts and comments.



Now the main page displays a list of recent entries, but there is no information regarding the comments on these entries. We have the Essence Comment, thanks to which we can return to the main page to provide this information. Since we have established a relationship between the Blog and Comment entities, we know that Doctrine 2 will be able to receive comments on the post (remember, we added the $ comments object to the Blog entity). Let's update the home page template
src / Blogger / BlogBundle / Resources / views / Page / index.html.twig
{# 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>
{# .. #}



We used the comments getter to get comments, and then passed the collection through a Twig length filter. If you look at the home page now by going to the address http: // localhost: 8000 / you will see the number of comments for each entry.

As explained above, we already told Doctrine 2 that the $ comments object of the Blog entity will have a relationship with the Comment entity. We achieved this in the previous part using metadata in the Blog entity.
src / Blogger / BlogBundle / Entity / Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
/**
 * @ORM\OneToMany(targetEntity="Comment", mappedBy="blog")
 */
protected $comments;



So, we know that Doctrine 2 is aware of the relationship between posts and comments, but how does this populate the $ comments object with the corresponding Comment entities? If you remember the method in BlogRepository that we created (shown below) that gets posts on the home page, we did not make a selection to retrieve related Comment entities.
src / Blogger / BlogBundle / Repository / BlogRepository.php
// 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();
}



However, Doctrine 2 uses a process called deferred loading, where Comment entities are retrieved from the database if necessary, in our case, when {{blog.comments | length}} . We can demonstrate this process using the developer toolbar. We have already begun to learn the basics of the developer toolbar and it is time to introduce one of its most useful features, Doctrine 2 profilers. You can open the Doctrine 2 profiler by clicking the icon on the developer's toolbar ... The number next to this icon shows the number of queries made to the database for the current HTTP request.



If you click the Doctrine 2 icon, you will be presented with information about the requests that Doctrine 2 made to the database for the current HTTP request.



As you can see in the above screenshot, there are a number of requests to display on the main page. The second request, extracts the essence of records from the database and is performed as a result of the method getLatestBlogs () class BlogRepository . After this query, you will notice a series of queries that receive comments from the database, one entry at a time. We can see it here. WHERE t0.blog_id =? in each of the requests, where ? replaced with the parameter value ( blog id ). Each of these queries is the result of a call to {{blog.comments}}in the home page template. Each time this function is executed, Doctrine 2 must lazily load the Comment entities that are related to the Blog entity.

While lazy loading is very effective at retrieving related entities from the database, this is not always the most efficient way to accomplish this task. Doctrine2 provides the ability to combine related objects together when querying a database. Thus, we can return the Blog entity and its associated Comment entities from the database in one query. Update the QueryBuilder code in BlogRepository.
src / Blogger / BlogBundle / Repository / BlogRepository.php
// 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();
}



Now, if you refresh the main page and look at the output of Doctrine 2 in the developer toolbar, you will notice that the number of requests has decreased. You will also see that the comment table has been attached to the blog table.

Lazy loading and combining related objects are very powerful concepts, but they must be used correctly. The right balance between the 2 concepts must be found in order to ensure your application is as efficient as possible. At first glance, it might seem like a good idea to combine every related entity, so you never have to use a lazy load and the number of database queries will always remain low. However, it is important to remember that the more information you retrieve from the database, the more Doctrine 2 will have to process. More data also means more memory used by the server to store entity objects.

Before moving on, let's make one small addition to the homepage template for the amount of comments we just added. Refresh Home Page Template
src / Blogger / BlogBundle / Resources / views / Page / index.html.twig
{# 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>
{# .. #}


so that when you click on the number of comments, we go to the comments of the corresponding entry.

Sidebar


The symblog sidebar currently looks blank. We will update it by adding common elements that are common to most Blogs Tag Cloud and Recent Comments .

Tag Cloud

The tag cloud shows tags for each entry, paying attention to more popular tags, by highlighting them in bold. To achieve this, we need a way to get all the tags for all the records. Let's create some new methods in the BlogRepository class for this. Update the BlogRepository Class
src / Blogger / BlogBundle / Repository / BlogRepository.php
// 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;
    }
    // Shuffle the tags
    uksort($tagWeights, function() {
        return rand() > rand();
    });
    $max = max($tagWeights);
    // Max of 5 weights
    $multiplier = ($max > 5) ? 5 / $max : 1;
    foreach ($tagWeights as &$tag)
    {
        $tag = ceil($tag * $multiplier);
    }
    return $tagWeights;
}


Since tags are stored in the database as comma separated values ​​(CSV), we need a way to separate them and return them as an array. This is achieved using the getTags () method . The getTagWeights () method can use an array of tags to calculate the "weight" of each tag based on its popularity in the array. Tags are shuffled to appear randomly on the page.

We have the opportunity to generate a tag cloud, now we need to display it. Create a new method in the controller
src / Blogger / BlogBundle / Controller / PageController.php
// src/Blogger/BlogBundle/Controller/PageController.php
public function sidebarAction()
{
    $em = $this->getDoctrine()
               ->getManager();
    $tags = $em->getRepository('BloggerBlogBundle:Blog')
               ->getTags();
    $tagWeights = $em->getRepository('BloggerBlogBundle:Blog')
                     ->getTagWeights($tags);
    return $this->render('BloggerBlogBundle:Page:sidebar.html.twig', array(
        'tags' => $tagWeights
    ));
}


The method is very simple, it uses 2 new BlogRepository methods to create a tag cloud, and passes this to the display. Now let's create a template
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>



The template is also very simple. It simply iterates over the various tags and sets the class according to the "weight" of the tag. The for loop shows how to access the keys and values ​​of pairs in an array, where tag is the key and weight is the value. There are several uses of the for loop given in the Twig documentation .

If you look at the main BloggerBlogBundle layout template located src / Blogger / BlogBundle / Resources / views / layout.html.twig you will notice that we place a placeholder for the sidebar block. Let's now replace this with the output of a new method for the sidebar. Recall from the previous part that the Twig render method will output the contents of the controller method, in this case the sidebar method of the Page controller.

{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% block sidebar %}
    {{ render(controller('BloggerBlogBundle:Page:sidebar' ))}} 
{% endblock %}


Finally, let's add styles for the Tag Cloud . Add new styles to the file
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; }


Since we added new styles, let's connect them. Update BloggerBlogBundle main template
src / Blogger / BlogBundle / Resources / views / layout.html.twig
{# src/Blogger/BlogBundle/Resources/views/layout.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 %}
{# .. #}



Note

If you do not use the symbolic link method to access the bundle assets in the web folder, you must run the assets installation command again.
$ php app/console assets:install web



If you now refresh the main page of the blog, you will see a Tag Cloud in the sidebar. In order to get tags with different “weights” (popularity), you may need to update your blog fixtures.

Latest comments


Now we have the Tag Cloud , let's add the Recent Comments component to the sidebar.

First, we need a way to get the latest comments on posts. To do this, we will add a new method to CommentRepository
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();
}



Next, update the sidebar method to get the latest comments and transfer them to the template.
src / Blogger / BlogBundle / Controller / PageController.php
// 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
    ));
}



You will notice that we used the new blogger_blog.comments.latest_comment_limit parameter to limit the number of comments received. Update the configuration file to create this parameter.
src / Blogger / BlogBundle / Resources / config / config.yml

# src/Blogger/BlogBundle/Resources/config/config.yml
parameters:
    # ..
    # Blogger max latest comments
    blogger_blog.comments.latest_comment_limit: 10



Finally, we need to display the latest comments in the sidebar template. Update Template
src / Blogger / BlogBundle / Resources / views / Page / sidebar.html.twig
{# 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>



If you now refresh the page, you will see Recent comments that appear under the Tag Cloud block .



Twig Extensions



So far, we have displayed the date the blog comment was added in the standard date format, 2011-04-21 . A much better approach would be to display not the date, but the time that has passed since the comment was posted , for example, Published 3 hours ago . We could add a method to the Comment entity to achieve this and change the templates to use this method instead of the {{comment.created | date ('h: iA Ym-d')}} method .

But since we might need to use this functionality elsewhere, it would be better to pull the method out of the Comment entity. Since date conversion is the task of the template itself, we must implement this using Twig. Twig gives us this opportunity by providing an Extension interface.

We can use the extension interface in Twig to extend the functionality that it provides by default. We are going to create a new Twig extension filter, which can be used as follows.
{{comment.created | created_ago}} . This will return the date the comment was created in the format Published 2 days ago .

Expansion


Create a Twig file with the following contents
src / Blogger / BlogBundle / Twig / Extensions / BloggerBlogExtension.php
<?php
// src/Blogger/BlogBundle/Twig/Extensions/BloggerBlogExtension.php
namespace Blogger\BlogBundle\Twig\Extensions;
class BloggerBlogExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
new \Twig_SimpleFilter('created_ago', array($this, 'createdAgo')),        
);
    }
    public function createdAgo(\DateTime $dateTime)
    {
        $delta = time() - $dateTime->getTimestamp();
        if ($delta < 0)
            throw new \InvalidArgumentException("createdAgo is unable to handle dates in the future");
        $duration = "";
        if ($delta < 60)
        {
            // Seconds
            $time = $delta;
            $duration = $time . " second" . (($time > 1) ? "s" : "") . " ago";
        }
        else if ($delta <= 3600)
        {
            // Mins
            $time = floor($delta / 60);
            $duration = $time . " minute" . (($time > 1) ? "s" : "") . " ago";
        }
        else if ($delta <= 86400)
        {
            // Hours
            $time = floor($delta / 3600);
            $duration = $time . " hour" . (($time > 1) ? "s" : "") . " ago";
        }
        else
        {
            // Days
            $time = floor($delta / 86400);
            $duration = $time . " day" . (($time > 1) ? "s" : "") . " ago";
        }
        return $duration;
    }
    public function getName()
    {
        return 'blogger_blog_extension';
    }
}



Creating an extension is a simple task. We will rewrite the getFilters () method and return any number of filters that we want. In this case, we create a created_ago filter . This filter will then register the use of the createdAgo method , which simply converts the DateTime object to a string representing the length of time that has passed since the value was saved to the DateTime object.

Register Extensions


In order for the Twig extension to become available to us, you need to update the services file
src / Blogger / BlogBundle / Resources / config / services.yml
services:
    blogger_blog.twig.extension:
        class: Blogger\BlogBundle\Twig\Extensions\BloggerBlogExtension
        tags:
            - { name: twig.extension }



As you can see, this is registering a new service using the BloggerBlogExtension Twig extension class that we just created.

Display



Now the new Twig filter is ready to use. Let's update the list of Recent Comments in the sidebar using the created_at filter. Update sidebar template
src / Blogger / BlogBundle / Resources / views / Page / sidebar.html.twig
{# 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>



If you now enter http: // localhost: 8000 / in your browser, you will see the dates of the last comments using the Twig filter.

Let's also update the comments on the blog page using the new filter. Replace the content in the template
src / Blogger / BlogBundle / Resources / views / Comment / index.html.twig
{# 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 %}



Tip

There are a number of useful Twig extensions available through the Twig-Extensions libraryon GitHub. If you created a useful extension, you can create a pull request for this repository, and an extension can be added so that other people can use it.


Url


Currently, the URL for each blog post displays only the id of the post. Although this is quite acceptable from a functional point of view, it is not a good solution for SEO. For example, the URL http: // localhost: 8000/1 does not provide any information about the contents of the entry, something like http: // localhost: 8000/1 / a-day-with-symfony2 would be much better. To achieve this, we add slug to the blog name and use it as part of this URL. Adding slug will remove all non-ASCII characters and replace them with a hyphen - .

Update Route


Let's modify the route rule for the entry page and add the slug component. Update Route Rule
src / Blogger / BlogBundle / Resources / config / routing.yml
# src/Blogger/BlogBundle/Resources/config/routing.yml
BloggerBlogBundle_blog_show:
    path:  /{id}/{slug}
    defaults: { _controller: "BloggerBlogBundle:Blog:show" }
    requirements:
        methods:  GET
        id: \d+



Controller

As with the existing id component , the new slug component will be passed to the controller action as an argument, so let's update the controller
src / Blogger / BlogBundle / Controller / BlogController.php
// src/Blogger/BlogBundle/Controller/BlogController.php
public function showAction($id, $slug)
{
    // ..
}


to reflect this.

Note

The order in which arguments are passed to the controller action does not matter; only names matter. Symfony2 is able to match routing arguments with a list of options. Since we have not used the default values ​​of the components, it is worth mentioning them here. If we add another component to the routing rule, we can specify the default value using the defaults option.

BloggerBlogBundle_blog_show:
    path:  /{id}/{slug}
    defaults: { _controller: "BloggerBlogBundle:Blog:show", comments: true }
    requirements:
        methods:  GET
        id: \d+


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

Using this method, a request to http: // localhost: 8000/1 / symfony2-blog will cause $ comments to be set to true in the showAction method


Slug


Since we want to create slug from the name of the record, we will automatically generate the slug value. We could just perform this action while displaying the header field, but instead we will store the slug in the Blog entity and save it to the database.

Blog Entity Update

Let's add a new property to the Blog entity for storing slug. Update entity
src / Blogger / BlogBundle / Entity / Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
class Blog
{
    // ..
    /**
     * @ORM\Column(type="string")
     */
    protected $slug;
    // ..
}



Now create access methods for the new $ slug property . As before, we execute the command.

$ php app/console doctrine:generate:entities Blogger


Next, update the database schema.

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


To generate the slug value, we will use the slugify method from the symfony1 Jobeet manual. Add slugify method for Blog entity
src / Blogger / BlogBundle / Entity / Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
public function slugify($text)
{
    // replace non letter or digits by -
    $text = preg_replace('#[^\\pL\d]+#u', '-', $text);
    // trim
    $text = trim($text, '-');
    // transliterate
    if (function_exists('iconv'))
    {
        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
    }
    // lowercase
    $text = strtolower($text);
    // remove unwanted characters
    $text = preg_replace('#[^-\w]+#', '', $text);
    if (empty($text))
    {
        return 'n-a';
    }
    return $text;
}



Since we want slug to be generated automatically from the header, we can generate slug when the header value is set. To do this, we can update the setTitle method to set the slug value. Update Blog Entity
src / Blogger / BlogBundle / Entity / Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
public function setTitle($title)
{
    $this->title = $title;
    $this->setSlug($this->title);
}



Next, update the setSlug method .
src / Blogger / BlogBundle / Entity / Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
public function setSlug($slug)
{
    $this->slug = $this->slugify($slug);
}



Next, reload the fixture data to generate slug.

$ php app/console doctrine:fixtures:load


Route Update



Finally, we need to update existing calls to create routes to the blog page. There are several places where we need to do this.
Open the home page template and add the following
src / Blogger / BlogBundle / Resources / views / Page / index.html.twig
{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}
{% extends 'BloggerBlogBundle::layout.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 %}


In addition, one update should be made in the Recent Comments section of the sidebar template.
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>
{# .. #}



Finally, the createAction method from the Comment of the controller must be updated when redirecting to the blog page when the comment is successfully posted . Update Comment Controller
src / Blogger / BlogBundle / Controller / CommentController.php
// 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()
        );
    }
    // ..
}


Now, if you go to the homepage http: // localhost: 8000 / and click on one of the record headers, you will see that the slug of the record has been added to the end of the URL.

Environment


Environments are very powerful and at the same time simple Symfony2 features. With the help of environments, we can configure various aspects of Symfony2 to run depending on specific needs in different ways throughout the application life cycle. By default, Symfony2 is configured to work with 3 Environments:

dev - Development
test - Testing
prod - Work

The purpose of these environments is self-explanatory, but what if these environments need to be configured differently depending on individual needs. When developing an application, it is useful to have a toolbar for the developer with displayed errors, while in the working environment you do not want to display this. In fact, if this information is displayed, it will pose a security risk since many details will be available regarding the inside of the application and the server. In a working environment, it would be better to display custom error pages using simplified messages, while the information would be written to text files. It would also be useful to enable caching to ensure the application performs well.
The test environment is a test environment. It is used in tests such as unit or functional testing. We have not considered testing yet, it will be discussed in the next part ...

Front controller


Until this moment, we worked in the environment of the developer. If we look at the front controller located in web / app_dev.php you will see the following line:
$kernel = new AppKernel('dev', true);

In contrast, if we look at the front controller for the working environment located in web / app.php we will see the following:
$kernel = new AppKernel('prod', false);

You can see that in this case, the working environment is transferred to AppKernel.
The test environment should not be launched in the browser and therefore there is no front controller app_test.php.

Configuration options

Above, we saw how front controllers are used to change the environment in which the application works. Now we look at how the various settings change at runtime in each environment. If you look at the files in app / config you will see several config.yml files . In particular, there is one main config.yml and 3 others all with an environment suffix; config_dev.yml, config_test.yml and config_prod.yml . Each of these files is loaded depending on the current environment. If we examine the config_dev.yml file , you will see the following lines at the top.

imports:
    - { resource: config.yml }


The imports directive will force config.yml to be included in this file. The same imports directive can be found at the top of 2 other environment configuration files, config_test.yml and config_prod.yml . By including a common set of configuration parameters defined in config.yml, we are able to override specific parameters for each environment. We can see in the development configuration file located at app / config / config_dev.yml the following lines, configurations of using the developer toolbar.
# app/config/config_dev.yml
web_profiler:
    toolbar: true

This setting is missing in the configuration file of the working environment since we do not want the developer toolbar to be displayed.

Starting a work environment

For those of you who wanted to see how the site works in a working environment, now is the time.
First, we must clear the cache using one of the Symfony2 commands.

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


Now enter http: // localhost: 8000 / app.php into your browser .

You will notice that the site looks the same, but there are several important differences. The developer toolbar and the detailed error message no longer appear, try going to http: // localhost: 8000 / app.php / 999 .



The detailed error message has been replaced by a simplified message informing the user about the problem. These pages can be customized to suit the look of your application.

In addition, you will notice that the app / logs / prod.log file is populated with data regarding execution. This can be useful when you have problems with the application in your work environment, since errors can no longer be displayed on the screen.

Note

How does the request to http: // localhost: 8000 / app.php ultimately go through the app.php file? I'm sure all of you created files like index.html and index.php that act like a site index, but how can app.php become like this? This was made possible thanks to the RewriteRule in the web / .htaccess file

RewriteRule ^ (. *) $ App.php [QSA, L]
We can see that this line has a regular expression that matches any text, ^ (. *) $ And passed in app.php.

If you are running on an Apache server that does not have mod_rewrite.c enabled, you can simply add app.php to the URL, http: // localhost: 8000 / app.php / .


Creating a new environment


Creating your own environment in Symfony2 may be necessary, for example, when you want to create an intermediate environment that will work on a production server, but will output some information for debugging. This would allow testing the platform manually on the actual production server, since the configurations of the production server and the development server may differ.
Since creating a new environment is a simple task, it is beyond the scope of this guide. There is an excellent article on the symfony2 cookbook that describes this.

Assetic



Since Assetiс library is not supplied by default since Symfony2.8 , we must install the assetic bundle ourselves. Add a line to the composer.json file in the project root

"require": {
      //...
       "symfony/assetic-bundle": "dev-master"
    },

and enter the command
composer update 

in the console.

Next, register the bundle in the file
app / AppKernel.php
//app/AppKernel.php
  public function registerBundles()
    {
        $bundles = array(
            // ...
            new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        );
        // ...
    }



And in the end, add the minimum necessary settings to the file
app / config / config.yml
# app/config/config.yml
assetic:
    debug:          '%kernel.debug%'
    use_controller: '%kernel.debug%'
    filters:
        cssrewrite: ~
# ...



The library was developed by Kris Wallsmith under the inspiration of the Python webassets library.

Assetic deals with 2 parts of managing assets, assets such as images, stylesheets, JavaScript, and filters that can be applied to them. These filters can perform useful tasks, such as minifying your CSS and JavaScript, transferring CoffeeScript files through the CoffeeScript compiler, and combining assets files together to reduce the number of HTTP requests made to the server.

We are currently using the Twig asset function to include assets in the template as follows.
<link href="{{ asset('bundles/bloggerblog/css/blog.css') }}" type="text/css" rel="stylesheet" />


Assets


The Assetic library describes an asset as follows:
An Assetic asset is something with filtered content that can be loaded. Asset also includes metadata, some of which can be manipulated.
Simply put, assets are the resources that your application uses, such as stylesheets and images.
To enable Assetic for BloggerBlogBundle we need to change
app / config / config.yml
# ..
assetic:
    bundles:    [BloggerBlogBundle]
    # ..



This will enable Assetic for BloggerBlogBundle only and will require adjustment whenever a new bundle needs to use Assetic. We can completely remove the bundles line and enable it for all future bundles.

Style sheets


Let's start by replacing the current asset calls for stylesheets in the main BloggerBlogBundle template. Update the content in the template
src / Blogger / BlogBundle / Resources / views / layout.html.twig
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% block stylesheets %}
    {{ parent () }}
    {% stylesheets
        '@BloggerBlogBundle/Resources/public/css/*'
    %}
        <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
    {% endstylesheets %}
{% endblock %}
{# .. #}



We replaced the previous 2 links to CSS files with some Assetic functionality. Using Assetic style sheets, we determined that all CSS files in src / Blogger / BlogBundle / Resources / public / css should be combined into 1 file and then output. Combining files is a very simple but effective way to optimize your site by reducing the number of files needed. Fewer files means fewer HTTP requests to the server. While we used * to list all the files in the css directory, we could simply list each file individually.
in the following way
{# src/Blogger/BlogBundle/Resources/views/layout.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 %}
{# .. #}



The end result is the same in both cases. The first option using * ensures that when new CSS files are added to the directory, they will always be included in the combined CSS Assetic file. This may not be what you require, so use any method described above to meet your needs.

If you look at the HTML code http: // localhost: 8000 / you will see that the CSS files were included in this way (Note, we are in the developer's environment).

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


You may be wondering why there are 2 files here. After all, it was indicated above that Assetic should combine files into 1 CSS file. This is because we started symblog in a developer environment. We can ask Assetic to work in non-debug mode by setting the debug flag to false
in the following way
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    debug=false
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{# .. #}



Now, if you look at the HTML code you will see something like this.
<link href="/css/3c7da45.css" rel="stylesheet" media="screen" />

If you look at the contents of this file, you will see that 2 CSS files, blog.css and sidebar.css were combined into 1 file. The name of the generated CSS file is randomly generated. If you want to control the name given to the generated file use the output option
in the following way
{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    output='css/blogger.css'
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}



Before continuing, remove the debug flag from the previous snippet, since we want to resume the default behavior for assets.
We also need to update the base application template.
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 %}
{# .. #}



Javascripts

Since there are currently no JavaScript files in our application, their use in Assetic is the same as for
style sheets
{% javascripts
    '@BloggerBlogBundle/Resources/public/js/*'
%}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}



Filters



Assetic's real power is filters. Filters can be applied to assets or the assets collection. There are a large number of filters located inside the library core, including the following:

CssMinFilter: minifies CSS
JpegoptimFilter: optimizes JPEG images
Yui \ CssCompressorFilter: compresses CSS files using a YUI compressor
Yui \ JsCompressorFilter: compresses JavaScript files using a YUI compressor
CoffeeScriptFilter: compiles CoffeeScript in

Full list of available filters in Assetic Readme .

Many of these filters transfer the task to another program or library, such as a YUI compressor, so you may need to install / configure the appropriate libraries in order to use some of the filters.

Download the YUI compressor version 2.4.7 (note that if you download the compressor from another source, the compressor version 2.4.8 will not work), unzip the archive and copy the yuicompressor-2.4.7.jar file from the build folder to the app / Resources directory / java / .

Note:

You have your computer must be installed Java technology can download it at the link


Next, we will configure the Assetic filter to compress CSS using the YUI compressor. Update application configuration
app / config / config.yml
# app/config/config.yml
# ..
assetic:
    filters:
        cssrewrite: ~
        yui_css:
            jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar"
# ..



We set up a filter called yui_css that will use the YUI Compressor Java executable that we placed in the application’s resource directory. In order to use a filter, you need to specify to which assets you want the filter applied. Refresh the template to apply yui_css filter
src / Blogger / BlogBundle / Resources / views / layout.html.twig
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% stylesheets
    '@BloggerBlogBundle/Resources/public/css/*'
    output='css/blogger.css'
    filter='yui_css'
%}
    <link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{# .. #}



Now, if you refresh the page and view the Assetic output files, you will notice that they have been minified. While minification is great for production servers, it can complicate the debugging process, especially when JavaScript is compressed. Can we disable minification when working in the developer's environment, preceding a filter with a question mark ?
in the following way

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



Dump assets for the working environment



In a working environment, Assetic generates paths to css and javascript files that are physically absent on the computer. Nevertheless, they are output because the Symfony internal controller opens these files and sends the contents back (after the filters are applied).
This type of dynamic dispatch of processed assets is good, because it means that you can immediately see the new state of any assets files that you changed. The downside is that this can happen rather slowly. If you use a lot of filters, this process can slow down very significantly.
Fortunately, Assetic provides a way to dump assets into real files, rather than generating them dynamically.
Enter the following command to dump asset files.
$ php app/console assetic:dump


You will notice that several CSS files were created in web / css including the combined blogger.css file. Now, if you run the symblog website in the working environment http: // localhost: 8000 / app.php, the files will be served directly from this folder.

Note

If you dump asset files to disk and want to return to the developer's environment, you will need to clear the created asset files in web / to allow Assetic to recreate them.


Conclusion



We examined a number of new areas of Symfony2, including Symfony2 environments and how to use the Assetic asset library. We also refined the homepage and added some components to the sidebar.

In the next chapter, we will move on to testing. We will consider functional and unit testing. We will see that Symfony2 comes with several classes to help write functional tests that simulate web requests, allow us to fill out forms, follow links, and then check the response returned.

Sources and supporting materials:

https://symfony.com/
http://tutorial.symblog.co.uk/
http://twig.sensiolabs.org/
How to Use Assetic for Asset Management
How to Minify JavaScripts and Stylesheets with YUI Compressor
How to Use Assetic For Image Optimization with Twig Functions
How to Apply an Assetic Filter to a Specific File Extension

Post scriptum
Всем спасибо за внимание и замечания сделанные по проекту, если у вас возникли сложности или вопросы, отписывайтесь в комментарии или личные сообщения, добавляйтесь в друзья.


Part 1 - Configuring Symfony2 and templates
Part 2 - Contact page: validators, forms and email
Part 3 - Doctrine 2 and Data Fixtures
Part 4 - Comment Model, Doctrine 2 Repository and Migrations
Part 6 - Unit and Functional Testing


Also, if you liked the manual, you can put a star in the project repository or subscribe. Thank.

Only registered users can participate in the survey. Please come in.

Please rate the quality of the manual.