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




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


Проект на Github
Дізнатися як встановити потрібну вам частину керівництва, можна в описі до дерева з посилання. (Наприклад, якщо ви хочете почати це з уроку не проходячи попередній)


Домашня сторінка

Ми почнемо цю частину створення домашньої сторінки. У звичайному блозі запису зазвичай відсортовані від нових до старих. Запис повністю буде доступна по посиланню зі сторінки блогу. Так як ми вже створили маршрут, контролер і відображення для домашньої сторінки, ми можемо легко їх відновити.

Отримання записів. Виконання запитів до бази даних.

Щоб відобразити записи блогу нам необхідно їх отримати з бази даних. Doctrine 2 надає нам Doctrine Query Language (DQL) іQueryBuilder для досягнення цієї мети Ви можете також зробити звичайний SQL запит через Doctrine 2, але такий метод не рекомендується, оскільки це веде нас від абстракції бази даних, яку надає нам Doctrine 2. Ми будемо використовувати QueryBuilder, оскільки він забезпечує гарний об'єктно-орієнтований спосіб для нас, щоб згенерувати DQL, який ми можемо використовувати для запитів до бази. Давайте поновимо метод indexAction в контролері Page розташованого
src/Blogger/BlogBundle/Controller/PageController.php 
для отримання записів з бази даних. Вставити наступний код:
// src/Blogger/BlogBundle/Controller/PageController.php
class PageController extends Controller
{
public function indexAction()
{
$em = $this->getDoctrine()
->getManager();

$blogs = $em->createQueryBuilder()
->select('b')
->from('BloggerBlogBundle:Blog', 'b')
->addOrderBy('b.created', 'DESC')
->getQuery()
->getResult();

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

// ..
}


Ми почали з отримання примірника QueryBuilder з Manager. Це дозволить почати створювати запит використовуючи безліч методів, які надає нам QueryBuilder. Повний список доступних методів можна подивитися у документації до QueryBuilder. Краще всього почати з допоміжних методів. Це такі методи як select(), from() і addOrderBy(). Як і у випадку попередніх взаємодій з Doctrine 2, ми можемо використовувати короткі анотації для звернення до сутності Blog через BloggerBlogBundle:Blog(врахуйте, це теж саме що і Blogger\BlogBundle\Entity\Blog). Коли ми закінчили, вказувати критерії для запиту, ми викликаємо метод getQuery(), який повертає екземпляр DQL. Ми не можемо отримати результати з об'єкта QueryBuilder, ми завжди повинні спочатку перетворити це в примірник DQL. Екземпляр об'єкта DQL надає нам метод getResult (), який повертає колекцію записів Блогу. Пізніше ми побачимо, що екземпляр об'єкта DQL має цілий ряд методів для повернення результатів, включаючи getSingleResult() і getArrayResult().

Показати

Тепер у нас є колекція записів і нам потрібно відобразити їх. Замініть контент домашньої сторінки, розташованої
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 }) }}">{{ 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 }) }}">Continue reading...</a></p>
</div>

<footer class="meta">
<p>Comments: </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 %}



Ми ввели одну з керуючих структур Twig,for..else..endfor. Якщо ви раніше не використовували шаблонизаторы, ймовірно, буде знаком наступний PHP код.
<?php if (count($blogs)): ?>
<?php foreach ($blogs as $blog): ?>
<h1><?php echo $blog->getTitle() ?><?h1>
<!-- rest of content -->
<?php endforeach ?>
<?php else: ?>
<p>There are no blog entries</p>
<?php endif ?>


Керуюча структура Twig for..else..endfor представляє собою більш простий спосіб досягнення цього завдання. Велика частина коду шаблону домашньої сторінки стосується виведення інформації блогу у форматі HTML. Однак, тут є кілька моментів, які нам потрібно врахувати. По-перше, ми використовуємо Twig path функцію для створення маршрутів. Так як сторінка блогу вимагає ID запису переданої в URL, ми повинні вставити його як аргумент у функцію path. Це можна побачити в цьому фрагменті коду:
<h2><a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id }) }}">{{ blog.title }}</a></h2>


По-друге ми повертаємо контент, використовуючи
<p>{{blog.blog(500) }}</p> 
Число 500 є максимальною довжиною поста який ми хочемо отримати назад з функції. Для цього нам необхідно оновити метод getBlog, який Doctrine 2 згенерувало для нас раніше. Оновіть метод getBlog в сутності Blog, розташований
src/Blogger/BlogBundle/Entity/Blog.php 

// src/Blogger/BlogBundle/Entity/Blog.php
public function getBlog($length = null)
{
if (false === is_null($length) && $length > 0)
return substr($this->blog, 0, $length);
else
return $this->blog;
}


Так як метод getBlog повинен повернути весь пост у блозі, ми встановимо параметр $length , який буде мати значення за замовчуванням null. Якщо передається значення null, повертається вся запис.
Тепер, якщо ви введете в ваш браузерlocalhost:8000 ви повинні побачити домашню сторінку, що відображає останні записи в блозі. У вас також повинна бути можливість перейти до повного запису блогу, натиснувши на назву запису або посилання на "Продовжити читання… (Continue reading)".



У той час як ми можемо запросити запису в контролері, це не найкраще місце для цього. Запит буде краще розмістити за межами контролера з-за цілого ряду причин:
Ми не зможемо повторно використовувати запит в іншому місці програми, без дублювання коду QueryBuilder.
Якщо б ми продублювали код QueryBuilder, ми повинні були б зробити декілька змін у майбутньому, якщо запит потребував би зміну.
Поділ запиту і контролера дозволить нам протестувати запит незалежно від контролера.
Doctrine 2 надає класи Репозиторію для полегшення цієї задачі.

Репозиторії Doctrine 2

Ми вже трохи поговорили про класах Репозиторію Doctrine 2 у попередній главі, коли ми створювали сторінку блогу. Ми використовували реалізацію класу за замовчуванням Doctrine\ORM\EntityRepository для вилучення записи з бази даних за допомогою методу find(). Так як нам потрібен користувальницький запит до бази, нам потрібно створити власний Репозиторій. Doctrine 2 може допомогти в рішенні цієї задачі. Оновіть метадані сутності Blog у файлі src/Blogger/BlogBundle/Entity/Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
/**
* @ORM\Entity(repositoryClass="Blogger\BlogBundle\Entity\Repository\BlogRepository")
* @ORM\Table(name="blog")
* @ORM\HasLifecycleCallbacks()
*/
class Blog
{
// ..
}


Ви можете бачити, що ми визначили простір імен для класу BlogRepository з яким ця сутність асоційована. Так як ми оновили метадані Doctrine 2 для сутності Blog, нам необхідно запустити команду doctrine:generate:entities наступним чином.
$ php app/console doctrine:generate:entities Blogger\BlogBundle

Doctrine 2 створить оболонку класу BlogRepository, розташованого
src/Blogger/BlogBundle/Entity/Repository/BlogRepository.php 


<?php

namespace Blogger\BlogBundle\Entity\Repository;

/**
* BlogRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class BlogRepository extends \Doctrine\ORM\EntityRepository
{
}



Клас BlogRepository розширює клас EntityRepository який надає метод find(), який ми використовували раніше. Давайте поновимо клас BlogRepository, перемістивши в нього код QueryBuilder з контролера Page.

<?php

namespace Blogger\BlogBundle\Entity\Repository;

/**
* BlogRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class BlogRepository extends \Doctrine\ORM\EntityRepository
{
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();
}
}



Ми створили метод getLatestBlogs який буде повертати останні записи в блозі, таким же чином як код QueryBuilder робив це в контролері. У класі репозиторію ми маємо прямий доступ до QueryBuilder з допомогою методу createQueryBuilder(). Ми також додали параметр за замовчуванням $limit, таким чином ми можемо обмежити кількість повернутих результатів. Результатом запиту буде те ж, що було у випадку з контролером. Ви, можливо, помітили, що нам немає потреби вказувати об'єкт за допомогою методу from(). Це пов'язано з тим, що ми знаходимося в BlogRepository, який пов'язаний із сутністю Blog. Якщо ми подивимося на реалізацію методу createQueryBuilder в класі EntityRepository ми можемо побачити, що метод from() був викликаний для нас.
// Doctrine\ORM\EntityRepository
public function createQueryBuilder($alias, $indexBy = null)
{
return $this->_em->createQueryBuilder()
->select($alias)
->from($this->_entityName, $alias, $indexBy);
}

Нарешті давайте поновимо метод indexAction в контролері Page для використання BlogRepository.
// src/Blogger/BlogBundle/Controller/PageController.php
class PageController extends Controller
{
public function indexAction()
{
$em = $this->getDoctrine()
->getManager();

$blogs = $em->getRepository('BloggerBlogBundle:Blog')
->getLatestBlogs();

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

// ..
}

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

Детальніше про моделі: Створення сутності Comment

Записи — це тільки половина історії, коли йдеться про ведення блогу. Ми також повинні дозволити читачам можливість коментувати записи в блозі. Ці коментарі повинні бути збережені і пов'язані з сутністю Blog так як запис може містити багато коментарів.
Ми почнемо з визначення основи, класу сутності Comment. Створіть новий файл, розташований в
src/Blogger/BlogBundle/Entity/Comment.php 
та вставте наступне
<?php
// src/Blogger/BlogBundle/Entity/Comment.php

namespace Blogger\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity(repositoryClass="Blogger\BlogBundle\Entity\Repository\CommentRepository")
* @ORM\Table(name="comment")
* @ORM\HasLifecycleCallbacks
*/
class Comment
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="АВТО")
*/
protected $id;

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

/**
* @ORM\Column(type="text")
*/
protected $comment;

/**
* @ORM\Column(type="boolean")
*/
protected $approved;

/**
* @ORM\ManyToOne(targetEntity="Blog", inversedBy="comments")
* @ORM\JoinColumn(name="blog_id", referencedColumnName="id")
*/
protected $blog;

/**
* @ORM\Column(type="datetime")
*/
protected $created;

/**
* @ORM\Column(type="datetime")
*/
protected $updated;

public function __construct()
{
$this->setCreated(new \DateTime());
$this->setUpdated(new \DateTime());

$this->setApproved(true);
}

/**
* @ORM\preUpdate
*/
public function setUpdatedValue()
{
$this->setUpdated(new \DateTime());
}
}


Більшу частину того, що ви тут бачите, ми вже розглянули в попередній частині, однак ми використовували метадані для створення посилання на сутність Blog. Так як коментар відноситься до записи, ми встановили посилання в сутності Comment до сутності Blog до якої вона належить. Ми зробили це вказавши посилання ManyToOne до сутності Blog. Ми також вказали, що зворотний зв'язок для цього посилання буде доступна через коментарі. Щоб інвертувати, нам потрібно оновити сутність Blog так Doctrine 2 буде знати, що запис може містити багато коментарів. Оновіть сутність Blog
src/Blogger/BlogBundle/Entity/Blog.php 

Вставте код
<?php
// src/Blogger/BlogBundle/Entity/Blog.php

namespace Blogger\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
* @ORM\Entity(repositoryClass="Blogger\BlogBundle\Entity\Repository\BlogRepository")
* @ORM\Table(name="blog")
* @ORM\HasLifecycleCallbacks
*/
class Blog
{
// ..

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

// ..

public function __construct()
{
$this->comments = new ArrayCollection();

$this->setCreated(new \DateTime());
$this->setUpdated(new \DateTime());
}

// ..
}


Є кілька змін на які потрібно вказати. По-перше, ми додали метадані до об'єкта $comments. Пам'ятаєте, у попередній главі ми не додавали метадані для цього об'єкта, тому що ми не хотіли, щоб Doctrine 2 його зберігала? Це так, проте, ми хочемо, щоб Doctrine 2, мала можливість заповнити цей об'єкт відповідними записами Comment. Тобто те, що дозволяють досягти метадані. По-друге, Doctrine 2 вимагає, щоб ми встановили значення за замовчуванням для об'єкта $comments у ArrayCollection. Ми зробимо це в конструкторі. Крім того, зверніть увагу на заяву use імпортує клас ArrayCollection.

Так як ми створили сутність Comment та оновили сутність Blog, давайте створимо методи доступу. Виконайте наступну команду Doctrine 2.
$ php app/console doctrine:generate:entities Blogger\BlogBundle


Обидві сутності будуть оновлені з коректними методами доступу. Ви також помітите, що
src/Blogger/BlogBundle/Entity/Repository/CommentRepository.php 
 

був створений клас CommentRepository, так як ми це вказали у метаданих.

Нарешті, ми повинні оновити базу даних, щоб відобразити зміни в наших сутності. Ми могли б скористатися командою doctrine:schema:update яка показана нижче, щоб зробити це, але замість цього ми розповімо про Міграції Doctrine 2.
$ php app/console doctrine:schema:update --force

Міграції Doctrine 2

Розширення Міграцій Doctrine 2 і бандл не поставляється з Symfony2, ми повинні вручну їх встановити. Відкрийте файл composer.json розташований в корені проекту та вставте залежності Міграцій Doctrine 2 і бандл як показано нижче.
"require": {
// ...
"doctrine/doctrine-migrations-bundle": "dev-master",
"doctrine/migrations": "dev-master"
}

Далі оновіть бібліотеки командою.
$ composer update

Це оновить всі бібліотеки з Github та встановить їх в необхідні директорії.
Тепер давайте зареєструємо бандл в kernel розташованого вapp/AppKernel.php
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
// ...
);
// ...
}


Тепер ми готові оновити базу даних, щоб відобразити зміни в сутності, цей процес пройде в 2 етапи. По-перше, ми повинні доручити Міграцій Doctrine 2 попрацювати з відмінностями між сутностями і поточною схемою бази даних. Це робиться командою doctrine:migrations:diff. По-друге, ми повинні виконати міграцію, засновану на даних створених першою командою. Це робиться командою doctrine:migrations:migrate.
Виконайте наступні 2 команди для того, щоб відновити схему бази даних.
$ php app/console doctrine:migrations:diff

$ php app/console doctrine:migrations:migrate

На попередження, показане нижче відповідаємо yes.
WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n): yes

Тепер ваша база даних буде відображати останні зміни сутностей і містити нову таблицю коментарів.

Примітка

Ви також побачите нову таблицю в базі даних під назвою migration_versions. Вона зберігає номери версій міграцій тому є команда, за допомогою якої можна дізнатися яка на даний момент версія бази даних.


Підказка

Міграції Doctrine 2 є відмінним способом для оновлення бази даних на production, оскільки зміни можна зробити програмно. Це означає, що ми можемо інтегрувати цю задачу в сценарій розгортання проекту, тому база даних оновлюється автоматично при розгортанні нової версії додатка. Міграції Doctrine 2 також дозволяють відкотити зміни, так як кожна міграція має up і down метод. Щоб повернутися до попередньої версії, вам необхідно вказати номер версії, яку ви хотіли б повернутися, зробити це можна, так як показано нижче.
$ php app/console doctrine:migrations:migrate 20110806183439

Фікстури даних



Тепер у нас є сутність Comment, давайте додамо Фікстури даних. Це хороший момент, в той час, коли ви створюєте сутність. Ми знаємо, що коментар повинен мати зв'язок з сутністю Blog, як ми це вказали у метаданих, тому при створенні фікстур для сутностей Comment нам потрібно буде вказати сутність Blog. Ми вже створили фікстури для сутності Blog таким чином, ми могли б просто оновити цей файл, щоб додати сутності Comment. Це може бути керованим зараз, але що станеться, коли ми додамо пізніше користувачів і інші сутності в наш бандл? Найкраще буде створити новий файл для фікстур сутності Comment. Проблемою в цьому підході є те, як ми отримаємо доступ до записів Blog з фікстур блогу.

На щастя, це може бути легко досягнуто шляхом додавання посилання на об'єкти в файлі фікстур до якого інші файли фікстур мають доступ. Оновіть Фікстури даних суті Blog розташованої
src/Blogger/BlogBundle/DataFixtures/ORM/BlogFixtures.php 

наступним
<?php
// src/Blogger/BlogBundle/DataFixtures/ORM/BlogFixtures.php

namespace Blogger\BlogBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Blogger\BlogBundle\Entity\Blog;

class BlogFixtures extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $manager)
{
// ..

$manager->flush();

$this->addReference('blog-1', $blog1);
$this->addReference('blog-2', $blog2);
$this->addReference('blog-3', $blog3);
$this->addReference('blog-4', $blog4);
$this->addReference('blog-5', $blog5);
}

public function getOrder()
{
return 1;
}
}


Зміни, які тут варто зазначити, розширення класу AbstractFixture і реалізація OrderedFixtureInterface. Також зверніть увагу на 2 новихuse оператора імпортують ці класи.

Ми додаємо посилання на сутності блог за допомогою методу addReference(). Цей перший параметр є ідентифікатором посилання, яку ми можемо використати для вилучення об'єкта пізніше. В кінці ми повинні реалізувати метод getOrder (щоб змінити порядок завантаження фікстур. Записи повинні бути завантажені до коментарів тому ми повертаємо 1.

Фікстури Comment



Тепер ми готові визначити фікстури для сутності Comment. Створіть файл фікстур
src/Blogger/BlogBundle/DataFixtures/ORM/CommentFixtures.php 

та вставте наступне
<?php
// src/Blogger/BlogBundle/DataFixtures/ORM/CommentFixtures.php

namespace Blogger\BlogBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Blogger\BlogBundle\Entity\Comment;
use Blogger\BlogBundle\Entity\Blog;

class CommentFixtures extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $manager)
{
$comment = new Comment();
$comment->setUser('symfony');
$comment->setComment('To make a long story short. You can\'t go wrong by choosing Symfony! And no one has ever been fired for using Symfony.');
$comment->setBlog($manager->merge($this->getReference('blog-1')));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('David');
$comment->setComment('To make a long story short. Choosing a framework must not be taken lightly; it is a long-term commitment. Make sure that you make the right selection!');
$comment->setBlog($manager->merge($this->getReference('blog-1')));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Dade');
$comment->setComment('Anything else, mom? You want me to mow the lawn? Oops! I forgot, New York, No grass.');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Kate');
$comment->setComment('Are you challenging me? ');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 06:15:20"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Dade');
$comment->setComment('Name your stakes.');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 06:18:35"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Kate');
$comment->setComment('If I win, you become my slave.');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 06:22:53"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Dade');
$comment->setComment('Your SLAVE?');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 06:25:15"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Kate');
$comment->setComment('You wish! You\'ll do shitwork, scan, crack copyrights...');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 06:46:08"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Dade');
$comment->setComment('And if I win?');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 10:22:46"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Kate');
$comment->setComment('Make it my first-born!');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-23 11:08:08"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Dade');
$comment->setComment('Make it our first-date!');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-24 18:56:01"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Kate');
$comment->setComment('I don\'t DO dates. But I don\'t lose either, so you\'re on!');
$comment->setBlog($manager->merge($this->getReference('blog-2')));
$comment->setCreated(new \DateTime("2011-07-25 22:28:42"));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Stanley');
$comment->setComment('It\'s not gonna end like this.');
$comment->setBlog($manager->merge($this->getReference('blog-3')));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Gabriel');
$comment->setComment('Oh, come on, Stan. Not everything ends the way you think it should. Besides, audiences love happy endings.');
$comment->setBlog($manager->merge($this->getReference('blog-3')));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Mile');
$comment->setComment('Doesn\'t Bill Gates have something like that?');
$comment->setBlog($manager->merge($this->getReference('blog-5')));
$manager->persist($comment);

$comment = new Comment();
$comment->setUser('Gary');
$comment->setComment('Bill Who?');
$comment->setBlog($manager->merge($this->getReference('blog-5')));
$manager->persist($comment);

$manager->flush();
}

public function getOrder()
{
return 2;
}
}



C змінами які ми зробили в класі BlogFixtures, клас CommentFixtures також розширює клас AbstractFixture і реалізує OrderedFixtureInterface. Це означає, що ми повинні також реалізувати метод getOrder(). На цей раз ми повертаємо значення 2, забезпечуючи цим завантаження фікстур після фікстур записів.

Ми також можемо побачити, як використовуються посилання на сутності Blog які ми створили раніше.
$comment->setBlog($manager->merge($this->getReference('blog-2')));

Тепер ми готові завантажити фікстури в базу даних
$ php app/console doctrine:fixtures:load

На попередження відповідаємо: yes
Careful, database will be purged. Do you want to continue y/N ?yes

Відображення Коментарів

Тепер ми можемо відобразити коментарі, які пов'язані з кожним повідомленням у блозі. Ми почнемо з оновлення ContentRepository методом, який отримує самі останні схвалені коментарі для блогу.

Репозиторій Comment

Відкрийте клас CommentRepository розташований
src/Blogger/BlogBundle/Entity/Repository/CommentRepository.php 
та вставте
наступне
<?php
// src/Blogger/BlogBundle/Entity/Repository/CommentRepository.php

namespace Blogger\BlogBundle\Entity\Repository;

use Doctrine\ORM\EntityRepository;

/**
* CommentRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class CommentRepository extends EntityRepository
{
public function getCommentsForBlog($blogId, $approved = true)
{
$qb = $this->createQueryBuilder('c')
->select('c')
->where'c.blog = :blog_id')
->addOrderBy('c.created')
->setParameter('blog_id', $blogId);

if (false === is_null($approved))
$qb->andWhere('c.approved = :approved')
->setParameter('approved', $approved);

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


Метод, який ми створили буде отримувати коментарі до запису блогу. Для цього нам потрібно додати where умова до нашого запитом. Умова where використовує іменований параметр, що задається з допомогою методу setParameter(). Ви повинні завжди використовувати параметри замість того, щоб встановлювати значення безпосередньо у запиті
->where'c.blog = ' . blogId)

У цьому прикладі значення $blogId не буде безпечно і може залишити запит відкритим для атаки SQL injection.

Контролер Blog

Далі нам потрібно оновити showAction метод контролера Blog для отримання коментарів. Оновіть контролер Blog
src/Blogger/BlogBundle/Controller/BlogController.php

Вставити
// src/Blogger/BlogBundle/Controller/BlogController.php

public function showAction($id)
{
// ..

if (!$blog) {
throw $this->createNotFoundException('Unable to find Blog post.');
}

$comments = $em->getRepository('BloggerBlogBundle:Comment')
->getCommentsForBlog($blog->getId());

return $this- > render('BloggerBlogBundle:Blog:show.html.twig', array(
'blog' => $blog,
'comments' => $comments
));
}


Ми використовуємо новий метод CommentRepository для отримання схвалених коментарів. Колекція $comments також передається в шаблон.

Шаблон Blog show

Тепер у нас є список коментарів для блогу ми можемо оновити шаблон Blog show для показу коментарів. Ми могли б просто помістити висновок коментарів безпосередньо у шаблон Blog show, але так як коментарі мають свою власну сутність, було б краще, відокремити відображення в інший шаблон, і включити в нього цей. Це дозволить нам повторно використовувати шаблон виводу коментарів в будь-якому місці програми. Оновіть шаблон Blog
show src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig 
вставити
{# src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig #}

{# .. #}

{% block body %}
{# .. #}

<section class="comments" id="comments">
<section class="previous-comments">
<h3>Comments</h3>
{% include 'BloggerBlogBundle:Comment:index.html.twig' with { 'comments': comments } %}
</section>
</section>
{% endblock %}


Ви можете побачити новий тег Twig include. Він буде включати вміст шаблону BloggerBlogBundle:Comment:index.html.twig. Ми також можемо передати будь-яку кількість аргументів в шаблон. У цьому випадку нам потрібно пройти через колекцію Comment сутностей для візуалізації.

Шаблон Comment show

BloggerBlogBundle:Comment:index.html.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|date('l, F j, Y') }}</time></p>
</header>
<p>{{ comment.comment }}</p>
</article>
{% else %}
<p>There are no comments for this post. Be the first to comment...</p>
{% endfor %}



Як ви можете бачити, ми итерируем колекцію сутностей Comment і виводимо коментарі. Розповімо і про ще однією доброю функції Twig, cycle. Ця функція буде перебирати значення в масиві, який ви передаєте, під час кожної ітерації циклу. Поточне значення ітерації циклу виходить через спеціальну зміннуloop.index0. Це веде підрахунок ітерацій циклу, починаючи з 0. Є цілий ряд інших доступних спеціальних змінних, коли ми знаходимося в межах циклу. Ви можете також зауважити установку HTML-ID до article елементу. Це дозволить нам створити посилання на створені коментарі.

Comment show CSS



І нарешті, давайте додамо трохи CSS, щоб коментарі виглядали стильно. Оновіть стилі, розташовані в
src/Blogger/BlogBundle/Resouces/public/css/blog.css 

/** src/Blogger/BlogBundle/Resorces/public/css/blog.css **/
.comments { clear: both; }
.comments .odd { background: #eee; }
.comments .comment { padding: 20px; }
.comments .comment p { margin-bottom: 0; }
.comments h3 { background: #eee; padding: 10px; font-size: 20px; margin-bottom: 20px; clear: both; }
.comments .previous-comments { margin-bottom: 20px; }


Примітка

Якщо ви не використовуєте метод символічних посилань для звернення до assets бандла в папці web, ви повинні повторно запустити команду установки assets щоб скопіювати зміни.
$ php app/console assets:install web



Якщо тепер подивимося на одну з show pages, наприклад, http://localhost:8000/2 ви повинні побачити висновок коментарів до запису.



Додавання коментарів

Остання частина цього розділу буде присвячена розширенню функціональності для користувачів, додавання коментарів до запису в блозі. Це стане можливим завдяки формі на сторінці blog show. Ми вже говорили про створення форм в Symfony 2 коли створювали форму на сторінці контактів. Замість того щоб створювати форму коментаря вручну, ми можемо використовувати Symfony2, щоб він зробив це за нас. Запустіть наступну команду для генерації класу CommentType для сутності Comment.
$ php app/console generate:doctrine:form BloggerBlogBundle:Comment


Ви знову побачите використання скорочення щоб визначити сутність Comment.

Підказка

Ви, можливо, помітили, що також доступна команда doctrine:generate:form. Це та ж команда названа по-іншому.


Команда створила клас CommentType розташований
src/Blogger/BlogBundle/Form/CommentType.php


Подивитися код
<?php

namespace Blogger\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CommentType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user')
->add('comment')
->add('approved')
->add('created', 'datetime')
->add('updated', 'datetime')
->add('blog')
;
}

/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Blogger\BlogBundle\Entity\Comment'
));
}
}




Ми вже вивчили, що відбувається тут, в попередньому класі Enquiry Type. Ми могли б почати з установки цього класу зараз, але давайте займемося спочатку відображенням форми.

форми Відображення коментарів.

Так як ми хочемо, щоб користувач додавав коментарі зі сторінки blog show, ми могли б створити форму в методі showAction контролера Blog і вивести форму безпосередньо в шаблоні show. Однак було б краще відокремити цей код, як ми це робили з відображенням коментарів. Різниця між відображенням коментарів і відображенням форми коментарів в тому, що форма коментаря потребує в обробці, тому потрібно контролер.

Маршрут

Нам потрібно створити новий маршрут для обробки форм. Додайте новий маршрут, розташований
src/Blogger/BlogBundle/Resources/config/routing.yml

BloggerBlogBundle_comment_create:
path: /comment/{blog_id}
defaults: { _controller: "BloggerBlogBundle:Comment:create" }
вимога:
methods: POST
blog_id: \d+


Контролер

Далі, нам необхідно створити новий CommentControler який ми згадували вище. Створіть новий файл, розташований в src/Blogger/BlogBundle/Controller/CommentController.php та вставте наступне
<?php
// src/Blogger/BlogBundle/Controller/CommentController.php

namespace Blogger\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Blogger\BlogBundle\Entity\Comment;
use Blogger\BlogBundle\Form\CommentType;
use Symfony\Component\HttpFoundation\Request;

/**
* Comment controller.
*/
class CommentController extends Controller
{
public function newAction($blog_id)
{
$blog = $this->getBlog($blog_id);

$comment = new Comment();
$comment->setBlog($blog);
$form = $this->createForm(CommentType::class, $comment);

return $this- > render('BloggerBlogBundle:Comment:form.html.twig', array(
'comment' => $comment,
'form' => $form->createView()
));
}

public function createAction(Request $request, $blog_id)
{
$blog = $this->getBlog($blog_id);

$comment = new Comment();
$comment->setBlog($blog);
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);

if ($form->isValid()) {
// TODO: Persist the comment entity

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

return $this- > render('BloggerBlogBundle:Comment:create.html.twig', array(
'comment' => $comment,
'form' => $form->createView()
));
}

protected function getBlog($blog_id)
{
$em = $this->getDoctrine()
->getManager();

$blog = $em->getRepository('BloggerBlogBundle:Blog')->find($blog_id);

if (!$blog) {
throw $this->createNotFoundException('Unable to find Blog post.');
}

return $blog;
}

}





Ми створили 2 методу в контролері Comment, один для new і один для create. Метод new пов'язаний з відображенням форми для коментаря, метод create пов'язаний з обробкою форми коментаря. Хоча це може здатися великим шматком коду, тут немає нічого нового, все було розказано в другій частині, коли ми створювали контактну форму. Однак, перш ніж піти далі переконайтеся, що ви в повній мірі зрозуміли, що відбувається в контролері Comment.

Валідація Форми



Ми не хочемо, щоб у користувачів була можливість залишати коментарі з порожніми значеннями параметрів user та comment. Для досягнення цього, згадаймо Валідацію яку ми розглядали у другій частині при створенні форми запиту. Оновіть сутність Comment розташовану
src/Blogger/BlogBundle/Entity/Comment.php 

Подивитися код
<?php
// src/Blogger/BlogBundle/Entity/Comment.php

// ..

use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;

// ..
class Comment
{
// ..

public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('user', new NotBlank(array(
'message' => 'You must enter your name'
)));
$metadata->addPropertyConstraint('comment', new NotBlank(array(
'message' => 'You must enter a comment'
)));
}

// ..
}


Тут перевіряється заповнені поля user і comment. Також ми переопределили повідомлення за замовчуванням. Не забудьте додати простір імен ClassMetadata і NotBlank, як показано вище.

Показати



Далі нам потрібно створити 2 шаблону для методів і create new контролера. Створіть новий файл, розташований в
src/Blogger/BlogBundle/Resources/views/Comment/form.html.twig 
та вставте наступне
{# src/Blogger/BlogBundle/Resources/views/Comment/form.html.twig #}
{{ form_start(form { 'action': path('BloggerBlogBundle_comment_create' , { 'blog_id' : comment.blog.id }), 'method': 'POST', 'attr': {'class': 'blogger'} }) }}

{{ form_widget(form) }}
<p>
<input type="submit" value="Submit">
</p>




Мета цього шаблону досить проста, він просто відображає форму коментаря. Ви також помітите, що метод action форми є POST і відноситься до нового маршруту, який ми створили BloggerBlogBundle_comment_create.

Тепер давайте створимо шаблон для create методу. Створіть новий файл, розташований в
src/Blogger/BlogBundle/Resources/views/Comment/create.html.twig 
та вставте наступне
{% extends 'BloggerBlogBundle::layout.html.twig' %}

{% block title %}Add Comment{% endblock%}

{% block body %}
<h1>Add comment for blog post "{{ comment.blog.title }}"</h1>
{% include 'BloggerBlogBundle:Comment:form.html.twig' with { 'form': form } %}
{% endblock %}



Так як метод createAction контролера Comment має справу з обробкою форми, він також повинен бути в змозі відображати її, так як там можуть виникнути помилки. Ми повторно скористаємося BloggerBlogBundle:Comment:form.html.twig для відображення форми щоб не дублювати код.

Давайте тепер оновимо шаблон blog show для відображення форми. Оновіть шаблон
src/Blogger/BlogBundle/Resources/views/Blog/show.html

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

{# .. #}

{% block body %}

{# .. #}

<section class="comments" id="comments">
{# .. #}

<h3>Add Comment</h3>
{{ render(controller('BloggerBlogBundle:Comment:new',{ 'blog_id': blog.id })) }} 
</section>
{% endblock %}


Ми використовували тут інший тег Twig, render. Цей тег виводить вміст контролера в шаблон. У нашому випадку ми виводимо вміст BloggerBlogBundle:Comment:new

Якщо ми подивимося тепер на одну зі сторінок блогу, таку якhttp://localhost:8000/2 ви побачите повідомлення, показане нижче.


Це повідомлення викликано шаблоном BloggerBlogBundle:Blog:show.html.twig. Якщо ми подивимося на рядок 23 шаблону BloggerBlogBundle:Blog:show.html.twig ми побачимо, що цей рядок показує, що проблема насправді в процесі вбудовування BloggerBlogBundle:Comment:create контролера.

{{ render(controller('BloggerBlogBundle:Comment:new',{ 'blog_id': blog.id })) }}


Якщо ми подивимося на повідомлення про помилку уважніше це дасть нам більше інформації про причини, чому помилка була викликана.
Вона говорить нам про те, що поле, яке ми намагаємося викликати не має методу __toString () для сутності, пов'язаної з ним. Поле вибору є елементом форми, яке дає користувачу вибір декількох варіантів, наприклад, елемент select (випадаючий список). Ви можете бути здивовані, де ми виводимо таке поле у формі коментаря? Якщо ви подивитеся на шаблон форми коментаря знову, ви помітите, що ми виводимо форму з допомогою функції Twig {{form_widget(form)}}. Ця функція виводить всю форму. Давайте повернемося до класу форми створену з класу Content Type. Ми можемо бачити, що ряд полів додаються у форму з допомогою об'єкта FormBuilder. Зокрема, ми додаємо поле blog.

Якщо ви пам'ятаєте, у другій частині посібника, ми говорили про те, як FormBuilder буде намагатися вгадати тип поля для виведення на основі метаданих, що належать до поля. Так як ми встановили зв'язок між сутностями Comment та Blog, FormBuilder припустив, що коментар повинен бути choice полем, яке дозволить користувачеві вказати запис, до якої треба прикріпити коментар. Ось чому у нас є choice поле у формі і ось чому була викликана помилка Symfony 2. Ми можемо вирішити цю проблему шляхом реалізації __toString() методу в сутності Blog.

// src/Blogger/BlogBundle/Entity/Blog.php
public function __toString()
{
return $this->getTitle();
}


Підказка

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


Тепер, коли ви поновіть сторінку, ви повинні побачити висновок форми коментаря. Ви також помітите, що деякі небажані поля були виведені такі як approved, created, updated і blog. Це відбувається тому, що ми не налаштували згенерований клас ContentType раніше.

Підказка

Всі поля, які були виведені мають коректний тип. Поле користувача text, поле коментаря textarea, 2 поля DateTime дозволяють вказати час, і т. д

Це відбувається із-за здатності FormBuilder вгадувати тип поля, яке повинно бути виведено. Він здатний робити це на основі метаданих, які ви надаєте. Так як ми визначили цілком конкретні метадані для сутності Comment, то FormBuilder здатний робити точні припущення про типи полів.


Давайте тепер оновимо клас, розташований в src/Blogger/BlogBundle/Form/CommentType.php для виводу тільки тих полів, які нам потрібні, вставити
<?php

namespace Blogger\BlogBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CommentType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('user');
$builder->add('comment');
}

/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Blogger\BlogBundle\Entity\Comment'
));
}

public function getBlockPrefix()
{
return 'blogger_blogbundle_commenttype';
}
}



Тепер, коли ви поновіть сторінку будуть виведені тільки поле для імені користувача і поле для коментарів. Якщо ви відправите форму зараз, коментар буде збережений в базі даних. Тому, що контролер форми нічого не робить з сутністю коментаря, якщо форма проходить перевірку. Так як же ми збережемо коментар до бази даних? Ви вже бачили, як це робиться при створенні Фікстур даних. Оновіть метод createAction як показано нижче.
// src/Blogger/BlogBundle/Controller/CommentController.php

public function createAction(Request $request, $blog_id)
{
//..

if ($form->isValid()) {
$em = $this->getDoctrine()
->getManager();
$em->persist($comment);
$em->flush();

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

}

Збереження сутності Comment відбувається завдяки викликом методівpersist() flush(). Пам'ятайте, що форма має справу з PHP-об'єктами, а Doctrine 2 управляє і зберігає ці об'єкти. Там немає прямого зв'язку між відправкою форми і збереженням представлених даних в базі.

Тепер ви повинні мати можливість додавати коментарі до повідомлень в блозі.



Висновок



Ми досягли значного прогресу в цій частині. Наш блог починає функціонувати так як ми і очікували. Тепер користувачі можуть залишати коментарі до записів блогу і читати коментарі, залишені іншими користувачами. Ми побачили, як створити фікстури, які можуть посилатися на декілька файлів фікстур і використовували Doctrine 2 Міграції щоб зберегти вбудовану схему бази даних при зміні сутностей.

Далі ми розглянемо створення бічній панелі (sidebar), щоб помістити в неї хмара тегів і нещодавні коментарі. Ми також розширимо наші знання в Twig і побачимо як за допомогою нього робити користувальницькі фільтри. На закінчення ми розглянемо використання Assetic бібліотеку, яка допоможе нам в управлінні нашими assets.

Джерела і допоміжні матеріали:

https://symfony.com/
http://tutorial.symblog.co.uk/
http://twig.sensiolabs.org/
http://www.doctrine-project.org/
http://odiszapc.ru/doctrine/

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

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


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

0 коментарів

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