12/08/2018, 10:12

Symfony Blog Tutorial: The Comments Model, Doctrine Repositories and Migrations

Tiếp tục chuỗi bài viết giới thiệu về Symfony 2 framework thông qua việc xây dựng một blog website. Bài viết này sẽ trình bày về thêm bình luận, Doctrine Repositories và migrations. Các bạn có thể theo dõi các bài viết trước về Validators, Forms and Emailing và Doctrine Model và Data Fixtures. ...

Tiếp tục chuỗi bài viết giới thiệu về Symfony 2 framework thông qua việc xây dựng một blog website. Bài viết này sẽ trình bày về thêm bình luận, Doctrine Repositories và migrations. Các bạn có thể theo dõi các bài viết trước về Validators, Forms and Emailing và Doctrine Model và Data Fixtures.

Tổng quan Bài viết này sẽ tiếp tục xây dựng blog model từ các bài viết trước và tạo thêm comment blog - xử lý bình luận cho bài viết. Bài viết này sẽ giới thiệu về mối quan hệ giữa các models, ví dụ 1 bài viết có nhiều bình luận. Chúng ta sẽ sử dụng Doctrine 2 QueryBuilder và Doctrine 2 Repository để lấy về các entities từ database. Bên cạnh đó, bài viết sẽ giới thiệu về Doctrine 2 Migration - một cách quản lý thay đổi database. Cuối bài viết, chúng ta có thể thực hiện được việc bình luận cho mỗi bài viết.

Querying the model

Để hiển thị các bài viết, chúng ta cần lấy từ trong database. Doctrine 2 cung cấp Doctrine Query Language (DQL) và QueryBuilder để thực hiện việc này. Chúng ta sẽ sử dụng QueryBuilder để sinh ra DQL giúp chúng ta query database. Hãy cập nhật trong action index của PageController như sau để lấy ra tất cả các bài viết trong database.

// src/Blogger/BlogBundle/Controller/PageController.php
class PageController extends Controller
{
    public function indexAction()
    {
        $em = $this->getDoctrine()
                   ->getEntityManager();

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

    // ..
}

The View

Bây giờ sẽ hiển thị các bài viết. Thay thế nội dung hiển thị của trang chủ như sau:

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

Screenshot from 2015-06-27 14:58:49.png

Trong khi query entities trong controller, điều này không được tốt cho lắm. Việc query có thể tốt hơn nếu như đặt bên ngoài controller vì một vài lý do sau:

  • Chúng ta không thể sử dụng query này cho bất kỳ chỗ nào nữa.
  • Nếu chúng ta lặp code QueryBuilder, chúng ta sẽ phải sửa nhiều chỗ nếu như có sự thay đổi trong query.
  • Tách biệt query với controller sẽ giúp chúng ta có thể kiểm tra quey một cách độc lập với controller. Doctrine 2 cung cấp lớp Repository giúp chúng ta làm được điều này.

Doctrine 2 Repositories

Ở bài trước cũng đã giới thiệu về Doctrine 2 Repository khi tạo trang hiển thị bài viết. Chúng ta sử dụng cài đặt mặc định của DoctrineORMEntityRepository để nhận về 1 bài viết thông qua phương thức find(). Nhưng chúng ta muốn tạo một query mới thì chúng ta cần tạo 1 repository khác kế thừa từ EntityRepository. Cập nhật lại Blog entity để tạo ra BlogRepository

// src/Blogger/BlogBundle/Entity/Blog.php
/**
 * @ORMEntity(repositoryClass="BloggerBlogBundleEntityRepositoryBlogRepository")
 * @ORMTable(name="blog")
 * @ORMHasLifecycleCallbacks()
 */
class Blog
{
    // ..
}

Sau khi đăng ký xong chúng ta cần chạy lại task sau:

php app/console doctrine:generate:entities BloggerBlogBundle

Doctrine 2 sẽ tạo ra class BlogRepository như sau:

<?php
// src/Blogger/BlogBundle/Entity/Repository/BlogRepository.php

namespace BloggerBlogBundleEntityRepository;

use DoctrineORMEntityRepository;

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

}

Bây giờ hãy chuyển QueryBuilder từ PageController vào trong BlogRepository

<?php
// src/Blogger/BlogBundle/Entity/Repository/BlogRepository.php

namespace BloggerBlogBundleEntityRepository;

use DoctrineORMEntityRepository;

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

Lúc này chúng ta gọi hàm getLastestBlogs() sẽ trả về các bài viết mới nhất. Việc này tương tự với việc sử dụng QueryBuilder trong controller. Cuối cùng, cập nhật lại PageController lại như sau để sử dụng repository.

// src/Blogger/BlogBundle/Controller/PageController.php
class PageController extends Controller
{
    public function indexAction()
    {
        $em = $this->getDoctrine()
                   ->getEntityManager();

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

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

    // ..
}

Creating Comment Entity

Chúng ta bắt đầu định nghĩa cơ bản cho class Comment entity và tạo quan hệ với Blog entity.

<?php
// src/Blogger/BlogBundle/Entity/Comment.php

namespace BloggerBlogBundleEntity;

use DoctrineORMMapping as ORM;
use SymfonyComponentValidatorMappingClassMetadata;
use SymfonyComponentValidatorConstraintsNotBlank;

/**
 * @ORMEntity(repositoryClass="BloggerBlogBundleEntityRepositoryCommentRepository")
 * @ORMTable(name="comment")
 * @ORMHasLifecycleCallbacks
 */
class Comment
{
    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORMColumn(type="string")
     */
    public $user;

    /**
     * @ORMColumn(type="text")
     */
    public $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);
    }

    public function setCreated($created)
    {
        $this->created = $created;
    }

    public function setUpdated($updated)
    {
        $this->updated = $updated;
    }

    public function setApproved($approved)
    {
        $this->approved = $approved;
    }

    public function setBlog($blog)
    {
        $this->blog = $blog;
    }

    public function setUser($user)
    {
        $this->user = $user;
    }

    public function setComment($comment)
    {
        $this->comment = $comment;
    }

    public function blog()
    {
        return $this->blog;
    }

    public function getBlog()
    {
        return $this->blog;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getCreated()
    {
        return $this->created;
    }

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

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

Sau đó cập nhật lại Blog entity để khai báo quan hệ giữa 2 entity.

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

    // ..
}

Sau khi đã thay đổi 2 entity. Chạy lại Doctrine 2 task sau để Doctrine 2 sinh ra quan hệ.

php app/console doctrine:generate:entities BloggerBlogBundle

Doctrine 2 Migrations

Doctrine 2 Migrations là 1 extension và cài đặt tương tự với Data Fixtures. Mở file composer.json và cập nhật lại như sau:

"require": {
    // ...
    "doctrine/doctrine-migrations-bundle": "dev-master",
    "doctrine/migrations": "dev-master"
}

Tiếp theo, cập nhật lại các thư viện.

php composer.phar update

Sau khi các thư viện cập nhật xong thì hãy đăng ký bundle vào trong app/AppKernel.php

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
        new DoctrineBundleMigrationsBundleDoctrineMigrationsBundle(),
        // ...
    );
    // ...
}

Bây giờ để thực sự cập nhật lại thay đổi cho database. Chúng ta cần 2 bước. Đầu tiên, doctrine:migrations:diff để kiểm tra sự khác nhau giữa các thực thể và cơ sở dữ liệu hiện tại. Thứ 2 là doctrine:migrations:migrate để thực thi việc thay đổi dự trên sự khác nhau đó.

php app/console doctrine:migrations:diff
php app/console doctrine:migrations:migrate

Display Comments

Chúng ta hiển thị comments liên quan đến mỗi bài viết. Đầu tiên cập nhật lại CommentRepository để lấy ra các comments của bài viết.

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

Blog Controller

Tiếp theo, cập nhật lại BlogController để lấy lại comment của bài viết sử dụng hàm trong CommentRepository vừa viết.

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

Blog show template

Bây giờ chúng ta hiển thị danh sách các comments trong template show của bài viết. Chúng ta có thể hiển thị đơn giản bằng cách render trực tiếp trong template show. Nhưng comments là của entity khác vì vậy tốt hơn là tách riêng ra template khác và include template đó vào.

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

Comment show template

Chúng ta đang include file mà chưa tồn tại. Vì vậy, chúng ta cần tạo nó với nội dung như sau:

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

Screenshot from 2015-06-27 14:57:51.png

Adding Comments

Chúng ta sẽ thực hiện việc tạo comments ở trong trang show bài viết. Chúng ta đã được giới thiệu về form trong Symfony2 ở trong bài viết về contact form. Tương tự như vậy, chạy task sau để sinh ra CommentType.

php app/console generate:doctrine:form BloggerBlogBundle:Comment

Display Comment Form

Routing

Định nghĩa routing cho việc hiển thị và thêm comment.

BloggerBlogBundle_comment_create:
    pattern:  /comment/{blog_id}
    defaults: { _controller: BloggerBlogBundle:Comment:create }
    requirements:
        _method:  POST
        blog_id: d+
BloggerBlogBundle_comment_new:
    pattern:  /comment/{blog_id}
    defaults: { _controller: BloggerBlogBundle:Comment:new }
    requirements:
        _method:  GET
        blog_id: d+

Controller

Tiếp theo tạo file Commentcontroller

<?php
// src/Blogger/BlogBundle/Controller/CommentController.php

namespace BloggerBlogBundleController;

use SymfonyBundleFrameworkBundleControllerController;
use BloggerBlogBundleEntityComment;
use BloggerBlogBundle
            
            
            
         
0