Створення блогу на Symfony 2.8 lts [ Частина 5]




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


Проект на 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

/**
* @ORM\OneToMany(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-запиту.



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



Як ви можете бачити у наведеному вище скріншоті, там є цілий ряд виконуваних запитів для виведення на головну сторінку. Другий запит, отримує сутності записів з бази даних і виконується в результаті методу 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.2 em; font-size: 20px; margin-bottom: 10px; font-weight: normal; background: #eee; padding: 5px; }
.sidebar p { line-height: 1.5 em; margin-bottom: 20px; }
.sidebar ul { list-style: none }
.sidebar ul li { line-height: 1.5 em }
.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; }


Так як ми додали нові стилі, давайте підключимо їх. Оновіть BloggerBlogBundlemain шаблон
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>



Якщо тепер ви поновіть сторінку, ви побачите Останні коментарі, які виводяться під блоком Хмара тегів.



Розширення 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 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';
}
}



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

Реєстрація Розширення


Для того, щоб розширення Twig стало нам доступно, необхідно оновити файл служб
src/Blogger/BlogBundle/Resources/config/services.yml
services:
blogger_blog.twig.extension:
class: Blogger\BlogBundle\Twig\Extensions\BloggerBlogExtension
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" }
вимога:
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 }
вимога:
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
{
// ..

/**
* @ORM\Column(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 or letter 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;
}



Оскільки ми хочемо, щоб 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.



Докладний протокол imap про помилку було замінено спрощеним повідомленням, информирующим користувача про проблему. Ці сторінки можуть бути налаштовані у відповідності з зовнішнім виглядом вашого додатка.

Крім того, ви помітите що файл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 Symfony\Bundle\AsseticBundle\AsseticBundle(),
);

// ...
}



І в кінці додамо мінімально необхідні налаштування в файл
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 %}

{# .. #}



Javascript
Так як в даний час немає якихось файлів JavaScript в нашому додатку, їх використання в Assetic є таким же, як і для таблиці стилів
{% javascript
'@BloggerBlogBundle/Resources/public/js/*'
%}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}




Фільтри



Реальна сила Assetic — це фільтри. Фільтри можуть бути застосовані до assets або колекції assets. Є велика кількість фільтрів, розташованих усередині ядра бібліотеки, включаючи наступні:

CssMinFilter: минифицирует CSS
JpegoptimFilter: оптимізує зображення формату JPEG
Yui\CssCompressorFilter: стискає CSS файли використовуючи YUI компресор
Yui\JsCompressorFilter: стискає 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 Javascript 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


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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.