{{ blog.title }}
{{ blog.blog(500) }}
Навигация по частям руководства
Проект на 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 %}
{{ blog.title }}
{{ blog.blog(500) }}
{% else %}
There are no blog entries for symblog
{% endfor %}
{% endblock %}
Мы ввели одну из управляющих структур Twig, for…else…endfor. Если вы ранее не использовали шаблонизаторы, вероятно вам будет знаком следующий PHP код.
getTitle() ?>
There are no blog entries
Управляющая структура Twig for…else…endfor представляет собой более простой способ достижения этой задачи. Большая часть кода в шаблоне домашней страницы касается вывода информации блога в формате HTML. Однако, здесь есть несколько моментов которые нам нужно учесть. Во-первых, мы используем Twig path функцию для создания маршрутов. Так как страница блога требует ID записи переданной в URL, мы должны вставить его в качестве аргумента в функцию path. Это можно увидеть в этом фрагменте кода:
{{ blog.title }}
Во-вторых мы возвращаем контент используя
{{blog.blog(500) }}
Число 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\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
Класс BlogRepository расширяет класс EntityRepository который предоставляет метод find (), который мы использовали ранее. Давайте обновим класс BlogRepository, переместив в него код QueryBuilder из контроллера Page.
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
));
}
// ..
}
Теперь, когда мы обновим домашнюю страницу будет выведено точно тоже самое, что и раньше. Все, что мы сделали, это реорганизовали код так, чтобы правильно оформленные классы выполняли задачи правильно.
Записи — это только половина истории, когда речь идет о ведении блога. Мы также должны позволить читателям возможность комментировать записи в блоге. Эти комментарии также должны быть сохранены и связаны с сущностью Blog так как запись может содержать много комментариев.
Мы начнем с определения основы, класса сущности Comment. Создайте новый файл, расположенный в src/Blogger/BlogBundle/Entity/Comment.php
и вставьте
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
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 и бандл не поставляется с 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
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. Создайте файл фикстур src/Blogger/BlogBundle/DataFixtures/ORM/CommentFixtures.php
и вставьте
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 методом, который получает самые последние одобренные комментарии для блога.
Откройте класс CommentRepository расположенный src/Blogger/BlogBundle/Entity/Repository/CommentRepository.php
и вставьте
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.
Далее нам нужно обновить 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 src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig
{# src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig #}
{# .. #}
{% block body %}
{# .. #}
Вы можете увидеть новый тег Twig include. Он будет включать содержимое шаблона BloggerBlogBundle: Comment: index.html.twig. Мы также можем передать любое количество аргументов в шаблон. В этом случае нам нужно пройти через коллекцию Comment сущностей для визуализации.
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 %}
{{ comment.user }} commented
{{ comment.comment }}
{% else %}
There are no comments for this post. Be the first to comment...
{% endfor %}
Как вы можете видеть, мы итерируем коллекцию сущностей Comment и выводим комментарии. Расскажем и о ещё одной хорошей функции Twig, cycle. Эта функция будет перебирать значения в массиве, который вы передаете, во время каждой итерации цикла. Текущее значение итерации цикла получается через специальную переменную loop.index0. Это ведет подсчет итераций цикла, начиная с 0. Есть целый ряд других доступных специальных переменных, когда мы находимся в пределах цикла. Вы можете также заметить установку HTML-ID к article элементу. Это позволит нам позже создать ссылки на созданные комментарии.
И наконец, давайте добавим немного 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
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" }
requirements:
methods: POST
blog_id: \d+
Далее, нам необходимо создать новый CommentControler который мы упомянули выше. Создайте новый файл, расположенный в src/Blogger/BlogBundle/Controller/CommentController.php и вставьте
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
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 шаблона для методов new и create контроллера. Создайте новый файл, расположенный в 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) }}
Цель этого шаблона довольно простая, он просто отображает форму комментария. Вы также заметите, что метод 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 %}
Add comment for blog post "{{ comment.blog.title }}"
{% 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 %}
{# .. #}
{# .. #}
Add Comment
{{ render(controller('BloggerBlogBundle:Comment:new',{ 'blog_id': blog.id })) }}
{% 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 очень информа
Comments
{% include 'BloggerBlogBundle:Comment:index.html.twig' with { 'comments': comments } %}