Создание блога на Symfony 2.8 lts [ Часть 5]

в 15:43, , рубрики: Doctrine ORM, php, sql, symfony, symfony2, ооп

Создание блога на Symfony 2.8 lts [ Часть 5] - 1

Проект на Github

Узнать как установить нужную вам часть руководства, можно в описании к репозиторию по ссылке. (Например, если вы хотите начать с это урока не проходя предыдущий)

Домашняя страница. Записи и комментарии.

Теперь на главной странице выводится список последних записей, но нет никакой информации относительно комментариев к этим записям. У нас есть Сущность Comment благодаря которой мы можем вернуться к главной странице, чтобы предоставить эту информацию. Так как мы установили связь между сущностями Blog и Comment мы знаем, что Doctrine 2 будет иметь возможность получать комментарии к записи (помните, мы добавили объект $comments к сущности Blog). Давайте обновим шаблон главной страницы

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>

{# .. #}

Мы использовали comments getter для получения комментариев, а затем передали коллекцию через Twig length фильтр. Если вы посмотрите на домашнюю страницу сейчас, перейдя по адресу http://localhost:8000/ вы увидите число комментариев к каждой записи.

Как объяснялось выше, мы уже сообщили Doctrine 2 что объект $comments сущности Blog будет иметь связь с сущностью Comment. Мы добились этого в предыдущей части с помощью метаданных в сущности Blog.

src/Blogger/BlogBundle/Entity/Blog.php

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

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

Итак, мы знаем, что Doctrine 2 известно о взаимосвязи между записями и комментариями, но как это заполняет объект $comments соответствующими сущностями Comment? Если вы помните метод в BlogRepository, который мы создали (показан ниже) получающий на домашней странице записи, мы не делали выборку для извлечения связанных Comment сущностей.

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();
}

Однако, Doctrine 2 использует процесс, называемый отложенной загрузкой, где сущности Comment извлекаются из базы данных в случае необходимости, в нашем случае, когда вызывается {{blog.comments | length}}. Мы можем продемонстрировать этот процесс, с помощью панели инструментов разработчика. Мы уже начали изучать основы панели инструментов разработчика и настало время, чтобы представить одну из самых полезных её функций, Doctrine 2 профайлера. Doctrine 2 профайлер можно открыть, щелкнув значок на панели инструментов разработчика… Число рядом с этой пиктограммой показывает количество запросов, выполненных к базе данных для текущего HTTP-запроса.

Создание блога на Symfony 2.8 lts [ Часть 5] - 2

Если щелкнуть значок Doctrine 2 вам будет представлена информация о запросах, которые были выполнены Doctrine 2 к базе данных для текущего запроса HTTP.

Создание блога на Symfony 2.8 lts [ Часть 5] - 3

Как вы можете видеть в приведенном выше скриншоте, там есть целый ряд выполняемых запросов для вывода на главную страницу. Второй запрос, извлекает сущности записей из базы данных и выполняется в результате метода getLatestBlogs() класса BlogRepository. После этого запроса вы заметите ряд запросов, которые получают комментарии из базы данных, одну запись за один раз. Мы можем увидеть это здесь WHERE t0.blog_id = ? в каждом из запросов, где ? заменяется на значение параметра (id блога). Каждый из этих запросов является результатом вызова {{blog.comments}} в шаблоне домашней страницы. Каждый раз, когда эта функция выполняется, Doctrine 2 должен лениво(lazily) загрузить сущности Comment, которые относятся к сущности Blog.

В то время как отложенная загрузка очень эффективна при извлечении связанных сущностей из базы данных, это не всегда является наиболее эффективным способом для выполнения этой задачи. Doctrine2 предоставляет возможность объединить связанные объекты вместе, когда выполняются запросы к базе данных. Таким образом, мы можем вернуть сущность Blog и связанные с ней сущности Comment из базы данных в одном запросе. Обновите код QueryBuilder в 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();
}

Теперь, если вы обновите главную страницу и посмотрите на вывод Doctrine 2 в панели инструментов разработчика вы заметите, что количество запросов сократилось. Вы также увидите, что таблица comment была присоединена к таблице blog.

Ленивая загрузка и объединение связанных объектов являются очень мощными концепциями, но они должны быть использованы правильно. Правильный баланс между 2 концепциями должен быть найден для того, чтобы обеспечить вашему приложению эффективность, настолько насколько это возможно. На первый взгляд может показаться хорошей идеей объединять каждые связанные сущности, так что вам никогда не придется пользоваться ленивой нагрузкой и количество запросов к базе данных всегда будет оставаться низким. Однако важно помнить, что чем больше информации вы извлекаете из базы данных, тем больше придётся обрабатывать Doctrine 2. Больше данных означает также больший объем памяти, используемый сервером для хранения объектов сущностей.

Прежде чем двигаться дальше давайте сделаем одно небольшое дополнение к шаблону домашней страницы, для количества комментариев, которые мы только что добавили. Обновите шаблон домашней страницы

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>

{# .. #}

чтобы при нажатии на количество комментариев мы переходили к комментариям соответствующей записи.

Боковая панель (sidebar)

В настоящее время боковая панель symblog выглядит пустой. Мы обновим её добавив распространённые элементы присущие большинству блогов Облако тегов и Последние комментарии.

Облако тегов

Облако тегов показывает теги для каждой записи обращая внимание на более популярные теги, путем выделения их более жирным шрифтом. Для достижения этой цели нам нужен способ для получения всех тегов для всех записей. Давайте создадим несколько новых методов в классе BlogRepository для этого. Обновите класс BlogRepository

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;
}

Так как теги хранятся в базе данных в виде значений, разделенных запятыми (CSV) нам нужен способ, чтобы разделить их и вернуть в виде массива. Это достигается с помощью метода getTags(). Метод getTagWeights() может использовать массив тегов, чтобы вычислить «вес» каждого тега на основе его популярности, в массиве. Тэги перемешиваются, чтобы отображаться в случайном порядке на странице.

У нас есть возможность сгенерировать облако тегов, теперь нам надо его отобразить. Создайте новый метод в контроллере

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
    ));
}

Метод очень прост, он использует 2 новых метода BlogRepository для создания облака тегов, и передает это в отображение. Теперь давайте создадим шаблон

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>

Шаблон также очень прост. Он просто перебирает различные теги и устанавливает класс в соответствии с «весом» тега. Цикл for показывает, как получить доступ к ключам и значениям пар массива, где tag является ключом, а weight значением. Существует несколько вариантов использования цикла for, приведенных в документации Twig.

Если вы посмотрите на основной шаблон макета BloggerBlogBundle, расположенного src/Blogger/BlogBundle/Resources/views/layout.html.twig вы заметите, что мы помещаем заполнитель(placeholder) для блока боковой панели. Давайте теперь заменим это выводом нового метода для боковой панели. Вспомните из предыдущей части, что метод Twig render будет выводить содержимое метода контроллера, в этом случае метода sidebar контроллера Page.

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

{# .. #}

{% block sidebar %}
    {{ render(controller('BloggerBlogBundle:Page:sidebar' ))}} 
{% endblock %}

Наконец давайте добавим стили для Облака тегов. Добавьте новые стили в файл

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; }

Так как мы добавили новые стили, давайте подключим их. Обновите BloggerBlogBundle main шаблон

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 %}

{# .. #}

Заметка

Если вы не используете метод символических ссылок для обращения к assets бандла в папке web, вы должны повторно запустить команду установки assets.

$ php app/console assets:install web

Если теперь вы обновите главную страницу блога вы увидите Облако тегов в боковой панели. Для того, чтобы получить теги с различными «весами» (популярностью), вам может понадобиться обновить фикстуры блога.

Последние Комментарии

Теперь у нас есть Облако тегов, давайте добавим компонент Последние комментарии на боковую панель.

Во-первых, нам нужен способ для получения последних комментариев к записям. Для этого мы добавим новый метод в 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();
}

Далее обновим метод sidebar для получения последних комментариев и передачи их в шаблон

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
    ));
}

Вы заметите что мы использовали новый параметр blogger_blog.comments.latest_comment_limit для лимита количества полученных комментариев. Чтобы создать этот параметр, обновите файл конфигурации

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

Наконец, мы должны отобразить последние комментарии в шаблоне боковой панели. Обновите шаблон

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>

Если теперь вы обновите страницу вы увидите Последние комментарии, которые выводятся под блоком Облако тегов.

Создание блога на Symfony 2.8 lts [ Часть 5] - 4

Расширения Twig

До сих пор мы, отображали дату добавления комментария блога в стандартном формате даты, 2011-04-21. Гораздо лучший подход будет заключаться в том, чтобы отображать не дату, а время, которое прошло с момента публикации комментария, например, Опубликовано 3 часа назад. Мы могли бы добавить метод в сущность Comment для достижения этой цели и изменить шаблоны, чтобы использовать этот метод вместо метода {{comment.created|date (' h:iA Y-m-d')}}.

Но так как нам может понадобиться использовать эту функциональность в другом месте было бы лучше, вынести метод из сущности Comment. Так как преобразование даты задача именно шаблона, мы должны осуществить это используя Twig. Twig дает нам эту возможность, предоставляя интерфейс Extension.

Мы можем использовать интерфейс extension в Twig для расширения функциональных возможностей, которые он предоставляет по умолчанию. Мы собираемся создать новое Twig расширение фильтр, который можно будет использовать следующим образом.
{{ comment.created|created_ago }}. Это вернет дату создания комментария в формате, Опубликовано 2 дня назад.

Расширение

Создайте файл для расширения Twig со следующим содержимым

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

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

namespace BloggerBlogBundleTwigExtensions;

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';
    }
}

Создание расширения является простой задачей. Мы перезапишем метод getFilters() и вернем любое количество фильтров, которое мы хотим. В данном случае мы создаем фильтр created_ago. Этот фильтр затем зарегистрирует использование метода createdAgo, который просто преобразует объект DateTime в строку, представляющую продолжительность времени прошедшее с момента сохранения значения в объект DateTime.

Регистрация Расширения

Для того, чтобы расширение Twig стало нам доступно, необходимо обновить файл служб

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

services:
    blogger_blog.twig.extension:
        class: BloggerBlogBundleTwigExtensionsBloggerBlogExtension
        tags:
            - { name: twig.extension }

Как видите это регистрирует новый сервис используя класс расширения BloggerBlogExtension Twig который мы только что создали.

Отображение

Теперь новый Twig фильтр готов к использованию. Давайте обновим список Последних комментариев в боковой панели используя created_at фильтр. Обновите шаблон боковой панели

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>

Если теперь вы введете в ваш браузер http://localhost:8000/ вы увидите даты последних комментариев, с использованием фильтра Twig.

Давайте также обновим комментарии на странице блога с использованием нового фильтра. Замените контент в шаблоне

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 %}

Совет

Есть целый ряд полезных расширений Twig, доступных через библиотеку Twig-Extensions на GitHub. Если вы создали полезное расширение вы можете создать pull request для этого репозитория, и расширение может быть добавлено, чтобы другие люди могли его использовать.

Url

В настоящее время URL для каждой записи в блоге отображает только id записи. Хотя это вполне приемлемо с функциональной точки зрения, это не является хорошим решением для SEO. Например, URL http://localhost:8000/1 не дает никакой информации о содержании записи, что-то вроде http://localhost:8000/1/a-day-with-symfony2 было бы гораздо лучше. Для достижения этой цели мы добавим slug в название блога и используем его как часть этого URL. Добавление slug удалит все символы, не являющиеся ASCII и заменит их на дефис.

Обновите маршрут

Давайте модифицируем правило маршрута для страницы записи и добавим компонент slug. Обновите правило маршрута

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+

Контроллер

Как и в случае существующего компонента id, новый компонент slug будет передан в действие контроллера в качестве аргумента, так что давайте обновим контроллер

src/Blogger/BlogBundle/Controller/BlogController.php

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

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

чтобы это отразить.

Заметка

Порядок, в котором аргументы передаются в действие контроллера не имеет значения, имеют значение только имена. Symfony2 способен сопоставлять аргументы маршрутизации со списком параметров. Так как мы еще не использовали значения компонентов по умолчанию, стоит упомянуть их здесь. Если мы добавляем еще один компонент для правила маршрутизации мы можем указать значение по умолчанию с помощью опции defaults.

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

Используя этот метод, запрос к http://localhost:8000/1/symfony2-blog приведет к тому что в $comments будет установлено значение true в методе showAction

Slug

Поскольку мы хотим, создавать slug из названия записи, мы будем автоматически генерировать значение slug. Мы могли бы просто выполнить это действие во время вывода поля заголовка, но вместо этого мы будем хранить slug в сущности Blog и сохранять в базу данных.

Обновление сущности Blog

Давайте добавим новое свойство в сущности Blog для хранения slug. Обновите сущность

src/Blogger/BlogBundle/Entity/Blog.php

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

class Blog
{
    // ..

    /**
     * @ORMColumn(type="string")
     */
    protected $slug;

    // ..
}

Теперь создайте методы доступа для нового свойства $slug. Как и раньше выполним команду.

$ php app/console doctrine:generate:entities Blogger

Далее обновим схему базы данных.

$ php app/console doctrine:migrations:diff

$ php app/console doctrine:migrations:migrate

Для генерации значения slug мы будем использовать метод slugify из symfony1 Jobeet руководства. Добавьте метод slugify для сущности Blog

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('#[^\pLd]+#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;
}

Поскольку мы хотим, чтобы slug генерировался автоматически из заголовка мы можем генерировать slug, когда устанавливается значение заголовка. Для этого мы можем обновить метод setTitle чтобы он устанавливал значение slug. Обновите сущность Blog

src/Blogger/BlogBundle/Entity/Blog.php

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

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

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

Далее обновите метод setSlug.

src/Blogger/BlogBundle/Entity/Blog.php

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

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

Далее перезагрузим данные фикстур чтобы сгенерировать slug.

$ php app/console doctrine:fixtures:load

Обновление маршрутов

Наконец, нам нужно обновить существующие вызовы для создания маршрутов к странице блога. Есть несколько мест где нам нужно это сделать.
Откройте шаблон домашней страницы и добавьте следующее

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 %}

Кроме того, одно обновление должно быть сделано в блоке Последние комментарии шаблона боковой панели

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>

{# .. #}

Наконец должен быть обновлен метод createAction из Comment контроллера при перенаправлении на страницу блога при успешной публикации комментария. Обновите контроллер Comment

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()
        );
    }

    // ..
}

Теперь, если вы перейдете на домашнюю страницу http://localhost:8000/ и нажмёте на один из заголовков записей вы увидите, что slug записи был добавлен к концу URL.

Окружения

Окружения являются очень мощными и в то же время простыми функциями Symfony2. С помощью окружений мы можем настроить различные аспекты Symfony2 для запуска в зависимости от конкретных потребностей по-разному в течение всего жизненного цикла приложения. По умолчанию Symfony2 настроен работать с 3-мя Окружениями:

dev — Разработка
test — Тестирование
prod — Рабочее

Цель этих окружений не требует пояснений, но что, если эти окружения должны быть настроены по-разному в зависимости от индивидуальных потребностей. При разработке приложения полезно иметь панель инструментов для разработчика с отображаемыми ошибками, в то время как в рабочем окружении вы не хотите этого отображать. На самом деле, если это информация будет отображаться она будет представлять угрозу безопасности так как будет доступно много деталей относительно внутренности приложения и сервера. В рабочем окружении было бы лучше отображать пользовательские страницы ошибок с помощью упрощенных сообщений, в то время как информация будет записываться в текстовые файлы. Было бы также полезно включить кэширование, чтобы обеспечить приложению хорошую работу. Если кэширование будет включено в окружении разработки — это будет доставлять массу неудобств так как вам придётся очищать кэш каждый раз, когда вы изменяете файлы конфигураций и т.д.
Окружение test является тестовой средой. Оно используется при выполнении тестов, таких как unit или функциональное тестирование. Мы ещё не рассматривали тестирование, оно будет рассмотрено в следующей части…

Front контроллер

До этого момента мы работали в окружении разработчика. Если мы взглянем на фронт-контроллер расположенный в web/app_dev.php вы увидите следующую строку:

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

В противоположность этому, если мы посмотрим на фронт-контроллер для рабочего окружения, расположенного в web/app.php мы увидим следующее:

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

Вы можете видеть, что в данном случае в AppKernel передаётся рабочее окружение.
Тестовое окружение не должно быть запущено в браузере и поэтому отсутствует фронт-контроллер app_test.php.

Параметры конфигурации

Выше мы видели, как фронт-контроллеры используются для изменения окружения в котором, работает приложение. Теперь мы рассмотрим, как различные настройки изменяются во время выполнения в каждом окружении. Если вы посмотрите на файлы в app/config вы увидите несколько файлов config.yml. В частности, есть один основной config.yml и 3 других все суффиксом окружения; config_dev.yml, config_test.yml и config_prod.yml. Каждый из этих файлов загружается в зависимости от текущего окружения. Если мы исследуем файл config_dev.yml вы увидите следующие строки в верхней части.

imports:
    - { resource: config.yml }

Директива imports заставит включить config.yml, в этот файл Та же директива imports может быть найдена в верхней части 2 других конфигурационных файлов окружений, config_test.yml и config_prod.yml. Включив общий набор параметров конфигурации, определенных в config.yml мы имеем возможность переопределять конкретные параметры для каждой среды. Мы можем увидеть в файле конфигурации development, расположенного app/config/config_dev.yml следующие строки, конфигурации использования панели инструментов разработчика.

# app/config/config_dev.yml
web_profiler:
    toolbar: true

Эта настройка отсутствует в конфигурационном файле рабочего окружения так как мы не хотим, чтобы отображалась панель инструментов разработчика.

Запуск рабочего окружения

Для тех из вас кто хотел увидеть, как сайт работает в рабочем окружении сейчас самое время.
Во-первых, мы должны очистить кэш с помощью одной из команд Symfony2.

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

Теперь введите в ваш браузер http://localhost:8000/app.php.

Вы заметите, что сайт выглядит так же, но есть несколько важных различий. Панель инструментов разработчика и подробное сообщение об ошибке больше не отображаются, попытайтесь перейти http://localhost:8000/app.php/999.

Создание блога на Symfony 2.8 lts [ Часть 5] - 5

Подробное сообщение об ошибке было заменено упрощенным сообщением, информирующим пользователя о проблеме. Эти страницы могут быть настроены в соответствие с внешним видом вашего приложения.

Кроме того, вы заметите что файл app/logs/prod.log заполняется данными относительно выполнения. Это бывает полезным, когда у вас есть проблемы с приложением в рабочем окружении, так как ошибки не могут быть больше отображены на экране.

Заметка

Как запрос на http://localhost:8000/app.php в конечном итоге направляется через файл app.php? Я уверен, все вы создавали файлы, такие как index.html и index.php, которые действуют как индекс сайта, но как app.php может стать таким? Это стало возможным благодаря RewriteRule в файле web/.htaccess

RewriteRule ^(.*)$ app.php [QSA,L]
Мы можем видеть, что эта строка имеет регулярное выражение, которое соответствует любому тексту, ^(.*)$ и передано в app.php.

Если работаете на сервере Apache, у которого не включен mod_rewrite.c, вы можете просто добавить app.php к URL, http://localhost:8000/app.php/.

Создание нового окружения

Создание собственного окружения в Symfony2 может понадобится, например, когда вы захотите создать промежуточное окружение, которое будет работать на рабочем сервере, но будет выводить некоторую информацию для отладки. Это позволило бы тестировать платформу вручную на фактическом рабочем сервере, так как конфигурации рабочего сервера и сервера разработчика могут отличаться.
Так как создание нового окружения является простой задачей, она выходит за рамки этого руководства. Существует отличная статья в cookbook Symfony2, в которой это описано.

Assetic

Так как начиная с версии Symfony2.8 библиотека Assetiс не поставляется по умолчанию, мы должны сами установить бандл assetic. Добавьте строчку в файл composer.json в корне проекта

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

и введите команду

composer update 

в консоли.

Далее зарегистрируем бандл в файле

app/AppKernel.php

//app/AppKernel.php

  public function registerBundles()
    {
        $bundles = array(
            // ...
            new SymfonyBundleAsseticBundleAsseticBundle(),
        );

        // ...
    }

И в конце добавим минимально необходимые настройки в файл

app/config/config.yml

# app/config/config.yml
assetic:
    debug:          '%kernel.debug%'
    use_controller: '%kernel.debug%'
    filters:
        cssrewrite: ~

# ...

Библиотека была разработана Kris Wallsmith под вдохновением Python библиотеки webassets.

Assetic имеет дело с 2-мя частями управления assets, assets такие как изображения, таблицы стилей, JavaScript и фильтры, которые могут быть применены к ним. Эти фильтры могут выполнять полезные задачи, такие как минификация ваших CSS и JavaScript, передача файлов CoffeeScript через компилятор CoffeeScript и объединения assets файлов вместе, чтобы уменьшить количество запросов HTTP сделанных на сервере.

В настоящее время мы используем Twig asset функцию для включения assets в шаблон следующим образом.

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

Assets

Assetic библиотека описывает asset следующим образом:
Assetic asset это что-то с фильтруемым контентом, который может быть загружен. Asset также включает в себя метаданные, некоторыми из которых можно манипулировать.
Проще говоря, assets являются ресурсами которые использует ваше приложение, например, таблицы стилей и изображения.
Чтобы включить Assetic для BloggerBlogBundle мы должны изменить

app/config/config.yml

# ..
assetic:
    bundles:    [BloggerBlogBundle]
    # ..

Это позволит включить Assetic только для BloggerBlogBundle и потребует корректировки всякий раз, когда новый бандл должен использовать Assetic. Мы можем полностью удалить строку bundles и включить его для всех будущих бандлов.

Таблицы стилей

Давайте начнем с замены текущих вызовов asset для таблиц стилей в основном шаблоне BloggerBlogBundle. Обновите контент в шаблоне

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 %}

{# .. #}

Мы заменили предыдущие 2 ссылки на CSS-файлы, некоторой Assetic функциональностью. Использованием таблиц стилей из Assetic мы определили, что все CSS файлы в src/Blogger/BlogBundle/Resources/public/css должны быть объединены в 1 файл, а затем выведены. Объединение файлов является очень простым, но эффективным способом оптимизации вашего сайта за счет уменьшения количества необходимых файлов. Меньшее количество файлов означает меньшее количество HTTP-запросов к серверу. В то время как мы использовали * чтобы указать все файлы в каталоге css мы могли бы просто перечислить каждый файл по отдельности.

следующим образом

{# 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 %}

{# .. #}

Конечный результат в обоих случаях одинаков. Первый вариант с использованием * гарантирует, что, когда новые CSS файлы добавятся в каталог, они всегда будут включены в объединенный файл CSS Assetic. Это может быть не тем что вам требуется, так что используйте любой метод описанный выше для удовлетворения ваших потребностей.

Если вы посмотрите на код HTML http://localhost:8000/ вы увидите, что CSS файлы были включены примерно таким образом (Заметьте, мы находимся в окружении разработчика).

<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" />

Вы можете быть удивлены, почему здесь 2 файла. Ведь выше было указано, что Assetic должен объединять файлы в 1 CSS файл. Это происходит потому, что мы запустили symblog в окружении разработчика. Мы можем попросить Assetic работать в режиме без отладки, установив флаг отладки в false

следующим образом

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

{# .. #}

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

{# .. #}

Теперь, если вы посмотрите на HTML код вы увидите что-то вроде этого.

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

Если просмотреть содержимое этого файла вы увидите, что 2 CSS файла, blog.css и sidebar.css были объединены в 1 файл. Имя созданного файла CSS генерируется случайным образом. Если вы хотите контролировать имя, данное сгенерированному файлу используйте output опцию

следующим образом

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

Перед тем, как продолжить удалите флаг отладки из предыдущего фрагмента, так как мы хотим, возобновить поведение по умолчанию для assets.
Нам также необходимо обновить базовый шаблон приложения

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

Так как в настоящее время нет каких-либо файлов JavaScript в нашем приложении, их использование в Assetic является таким же, как и для

таблиц стилей

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

Фильтры

Реальная сила Assetic — это фильтры. Фильтры могут быть применены к assets или коллекции assets. Есть большое количество фильтров, расположенных внутри ядра библиотеки, включая следующие:

CssMinFilter: минифицирует CSS
JpegoptimFilter: оптимизирует изображения формата JPEG
YuiCssCompressorFilter: сжимает CSS файлы используя YUI компрессор
YuiJsCompressorFilter: сжимает JavaScript файлы используя YUI компрессор
CoffeeScriptFilter: компилирует CoffeeScript в JavaScript

Полный список доступных фильтров в Assetic Readme.

Многие из этих фильтров передают задачу в другую программу или библиотеку, такие как YUI компрессор, так что вам, возможно, потребуется установить/настроить соответствующие библиотеки, чтобы использовать некоторые из фильтров.

Скачайте YUI компрессор версии 2.4.7 (обратите внимание, если вы будете скачивать компрессор из другого источника, версия компрессора 2.4.8 работать не будет), распакуйте архив и скопируйте файл yuicompressor-2.4.7.jar из папки build в каталог app/Resources/java/.

Примечание

У вас на компьютере должна быть установлена технология Java скачать можно по ссылке

Далее мы настроим Assetic фильтр для сжатия CSS с помощью YUI компрессора. Обновите конфигурацию приложения

app/config/config.yml

# app/config/config.yml

# ..

assetic:
    filters:
        cssrewrite: ~
        yui_css:
            jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar"

# ..

Мы настроили фильтр под названием yui_css, который будет использовать исполняемый файл YUI Compressor Java который мы разместили в каталоге ресурсов приложения. Для того чтобы использовать фильтр необходимо указать к каким assets вы хотите, чтобы применялся фильтр. Обновите шаблон чтобы применить yui_css фильтр

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 %}

{# .. #}

Теперь, если вы обновите страницу и просмотрите файлы вывода Assetic вы заметите, что они были минифицированы. В то время как минификация отлично подходит для рабочих серверов, это может осложнить процесс отладки, особенно когда сжимается JavaScript. Мы можем отключить минификацию при работе в окружении разработчика, предваряя фильтр с знаком вопроса ?

следующим образом


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

Дамп assets для рабочего окружения

В рабочем окружении, Assetic генерирует пути к css и javascript файлам которые физически отсутствуют на компьютере. Но тем не менее, они выводятся, потому что внутренний контроллер Symfony открывает эти файлы и отправляет содержимое обратно (после выполнения применённых фильтров).
Этот вид динамической отправки обрабатываемых assets хорош, поскольку это означает, что вы можете сразу увидеть новое состояние любых файлов assets которые вы изменили. Минус в том, что это может происходить довольно медленно. Если вы используете много фильтров, этот процесс может замедлиться очень существенно.
К счастью, Assetic предоставляет способ сделать дамп assets в реальные файлы, а не генерировать их динамически.
Введите следующую команду чтобы создать дамп файлов asset.

$ php app/console assetic:dump

Вы заметите, что были созданы несколько CSS-файлов в web/css включая объединенный файл blogger.css. Теперь, если вы запустите веб-сайт symblog в рабочем окружении http://localhost:8000/app.php файлы будут обслуживаться непосредственно из этой папки.

Заметка

Если вы сделаете дамп файлов asset на диск и хотите вернуться к окружению разработчика, вам необходимо будет очистить созданные файлы asset в web/, чтобы позволить Assetic воссоздать их.

Вывод

Мы рассмотрели целый ряд новых областей Symfony2, включая окружения Symfony2 и как использовать Assetic asset библиотеку. Мы также усовершенствовали главную страницу и добавили некоторые компоненты на боковую панель.

В следующей главе мы перейдем к тестированию. Мы рассмотрим функциональное и unit тестирование. Мы увидим, что Symfony2 поставляется с несколькими классами, чтобы помочь в написании функциональных тестов, которые имитируют веб-запросы, позволяют нам заполнять формы, переходить по ссылкам, а затем проверять возвращаемый ответ.

Источники и вспомогательные материалы:

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

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

Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 3 — Doctrine 2 и Фикстуры данных
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2

Также, если Вам понравилось руководство вы можете поставить звезду репозиторию проекта или подписаться. Спасибо.

Автор: antoscenco-vladimir

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js