Проект на 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 (учтите, это тоже самое что и BloggerBlogBundleEntityBlog). Когда мы закончили, указывать критерии для запроса, мы вызываем метод 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 в предыдущей главе, когда мы создавали страницу блога. Мы использовали реализацию класса по умолчанию DoctrineORMEntityRepository для извлечения записи блога из базы данных с помощью метода find(). Так как нам нужен пользовательский запрос к базе, нам нужно создать пользовательский Репозиторий. Doctrine 2 может помочь в решении этой задачи. Обновите метаданные сущности Blog в файле src/Blogger/BlogBundle/Entity/Blog.php
// src/Blogger/BlogBundle/Entity/Blog.php
/**
* @ORMEntity(repositoryClass="BloggerBlogBundleEntityRepositoryBlogRepository")
* @ORMTable(name="blog")
* @ORMHasLifecycleCallbacks()
*/
class Blog
{
// ..
}
Вы можете видеть, что мы определили пространство имен для класса BlogRepository с которым эта сущность ассоциирована. Так как мы обновили метаданные Doctrine 2 для сущности Blog, нам необходимо повторно запустить команду doctrine:generate:entities следующим образом.
$ php app/console doctrine:generate:entities BloggerBlogBundle
Doctrine 2 создаст оболочку класса BlogRepository, расположенного src/Blogger/BlogBundle/Entity/Repository/BlogRepository.php
<?php
namespace BloggerBlogBundleEntityRepository;
/**
* BlogRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class BlogRepository extends DoctrineORMEntityRepository
{
}
Класс BlogRepository расширяет класс EntityRepository который предоставляет метод find(), который мы использовали ранее. Давайте обновим класс BlogRepository, переместив в него код QueryBuilder из контроллера Page.
<?php
namespace BloggerBlogBundleEntityRepository;
/**
* BlogRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class BlogRepository extends DoctrineORMEntityRepository
{
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() был вызван для нас.
// DoctrineORMEntityRepository
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 BloggerBlogBundleEntity;
use DoctrineORMMapping as ORM;
/**
* @ORMEntity(repositoryClass="BloggerBlogBundleEntityRepositoryCommentRepository")
* @ORMTable(name="comment")
* @ORMHasLifecycleCallbacks
*/
class Comment
{
/**
* @ORMId
* @ORMColumn(type="integer")
* @ORMGeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORMColumn(type="string")
*/
protected $user;
/**
* @ORMColumn(type="text")
*/
protected $comment;
/**
* @ORMColumn(type="boolean")
*/
protected $approved;
/**
* @ORMManyToOne(targetEntity="Blog", inversedBy="comments")
* @ORMJoinColumn(name="blog_id", referencedColumnName="id")
*/
protected $blog;
/**
* @ORMColumn(type="datetime")
*/
protected $created;
/**
* @ORMColumn(type="datetime")
*/
protected $updated;
public function __construct()
{
$this->setCreated(new DateTime());
$this->setUpdated(new DateTime());
$this->setApproved(true);
}
/**
* @ORMpreUpdate
*/
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 BloggerBlogBundleEntity;
use DoctrineORMMapping as ORM;
use DoctrineCommonCollectionsArrayCollection;
/**
* @ORMEntity(repositoryClass="BloggerBlogBundleEntityRepositoryBlogRepository")
* @ORMTable(name="blog")
* @ORMHasLifecycleCallbacks
*/
class Blog
{
// ..
/**
* @ORMOneToMany(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 BloggerBlogBundle
Обе сущности будут обновлены с корректными методами доступа. Вы также заметите, что в
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 DoctrineBundleMigrationsBundleDoctrineMigrationsBundle(),
// ...
);
// ...
}
Теперь мы готовы обновить базу данных чтобы отразить изменения в сущности, этот процесс, пройдёт в 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 BloggerBlogBundleDataFixturesORM;
use DoctrineCommonDataFixturesAbstractFixture;
use DoctrineCommonDataFixturesOrderedFixtureInterface;
use DoctrineCommonPersistenceObjectManager;
use BloggerBlogBundleEntityBlog;
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 BloggerBlogBundleDataFixturesORM;
use DoctrineCommonDataFixturesAbstractFixture;
use DoctrineCommonDataFixturesOrderedFixtureInterface;
use DoctrineCommonPersistenceObjectManager;
use BloggerBlogBundleEntityComment;
use BloggerBlogBundleEntityBlog;
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 BloggerBlogBundleEntityRepository;
use DoctrineORMEntityRepository;
/**
* 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 BloggerBlogBundleForm;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentOptionsResolverOptionsResolver;
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' => 'BloggerBlogBundleEntityComment'
));
}
}
Мы уже изучили, что происходит здесь, в предыдущем классе 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 и вставьте
<?php
// src/Blogger/BlogBundle/Controller/CommentController.php
namespace BloggerBlogBundleController;
use SymfonyBundleFrameworkBundleControllerController;
use BloggerBlogBundleEntityComment;
use BloggerBlogBundleFormCommentType;
use SymfonyComponentHttpFoundationRequest;
/**
* 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 SymfonyComponentValidatorMappingClassMetadata;
use SymfonyComponentValidatorConstraintsNotBlank;
// ..
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 шаблона для методов 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) }}
<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 BloggerBlogBundleForm;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentOptionsResolverOptionsResolver;
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' => 'BloggerBlogBundleEntityComment'
));
}
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/
Всем спасибо за внимание и замечания сделанные по проекту, если у вас возникли сложности или вопросы, отписывайтесь в комментарии или личные сообщения, добавляйтесь в друзья.
Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 3 — Doctrine 2 и Фикстуры данных
Автор: antoscenco-vladimir