larablog 系列文章 04 - 评论模型:添加评论和数据迁移

这一章我们将基于博客文章模型进行延伸,建立评论模型,处理文章的评论功能。我们使用 ORM 进行数据检索,并介绍两个模型之间的关联。还将探索如何通过数据库迁移对数据库进行更改操作,创建首页和提供用户为博客文章发表评论的能力。

关于首页

我们先开始建立了首页,一般博客会在这里显示每片文章的摘要,从最新的向最旧的排序显示,完整的博客文章会在点击链接后进入文章的展示页。
由于我们已经建立首页的路由,控制器和视图,我们只要更新它们就好。

获取文章数据

为了显示博客文章,我们需要从数据库读取他们,Eloquent 提供了基于模型的查询构造器。我们可以利用基于模型的 QueryBuilder 来读取数据。
现在找到位于 app/Http/Controllers/PageController.phpindex 方法,更新为如下内容:

public function index()
{
    $posts = Post::orderBy('id', 'DESC')->get();

    return view('pages.index', ['posts' => $posts]);
}

这里按照 id 排序是因为它在数据库是自增长字段和添加时间字段在排序上作用一样。

显示视图

@extends('layouts.app')

@section('title', 'larablog')

@section('body')
    @forelse($posts as $post)
        <article class="blog">
            <div class="date">{{ $post->created_at->toDateTimeString() }}</div>
            <header>
                <h2><a href="/posts/{{ $post->id }}">{{ $post->title }}</a></h2>
            </header>

            <img src="{{ asset('images/'.$post->image) }}" />
            <div class="snippet">
                <p>{!! str_limit(strip_tags($post->content), 500) !!}</p>
                <p class="continue"><a href="/posts/{{ $post->id }}">Continue reading...</a></p>
            </div>

            <footer class="meta">
                <p>Comments: -</p>
                <p>Posted by <span class="highlight">{{ $post->author }}</span> at {{ $post->created_at->toDateTimeString() }}</p>
                <p>Tags: <span class="highlight">{{ $post->tags }}</span></p>
            </footer>
        </article>
    @empty
        <p>There are no blog entries for larablog</p>
    @endforelse
@endsection

我们在这里使用了一个模板引擎的控制结构 forelse..empty..endforelse,如果你过去没有使用模板引擎,你也应该熟悉这样的代码:

<?php if (count($posts)): ?>
    <?php foreach ($posts as $post): ?>
        <h1><?php echo $post->getTitle() ?><?h1>
        <!-- rest of content -->
    <?php endforeach ?>
<?php else: ?>
    <p>There are no post entries</p>
<?php endif ?>

在 Laravel 中 Blade 模型引擎的 forelse..empty..endforelse 控制结构可以简洁的完成上面的任务。

接着,我们用 <p>{{ str_limit(strip_tags($post->content), 500) }}</p> 输出文章摘要,传入的参数 500 是摘要字符的限制长度。
我们通过 strip_tags 剥除字符串中的 HTML、XML 以及 PHP 的标签,这样截取长度时不会将 HTML 标记计算在内。

打开浏览器重新访问 http://localhost:8000/ 你将会看到显示最新文章的首页,也能够点击文章标题或 Continue reading... 链接来查看整篇文章的内容。

首页效果

评论模型

除了文章,我们还希望读者能够对文章评论。这些评论需要和关联在一起,因为一篇文章可以包含多个评论内容。

在命令行中进入项目所在目录,执行命令:

php artisan make:model Comment -m

该命令将会生成模型 app/Comment.php 和数据迁移文件 database/migrations/xxxx_xx_xx_xxxxxx_create_comments_table.php

编辑 app/Comment.php,修改为如下内容:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * 获取拥有此评论的文章。
     */
    public function post()
    {
        return $this->belongsTo(App\Post::class);
    }
}

这里是用了 Eloquent 关联的关系来定义两个模型之间的联系。

一旦关联被定义之后,则可以通过 post「动态属性」来获取 CommentPost 模型,如下:

$comment = App\Comment::find(1);

echo $comment->post->title;

详细的关联关系内容可以查阅文档 Eloquent 定义关联

接下来,我们在 app/Post.php 中也定义相对对应的关系。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 获取博客文章的评论。
     */
    public function comments()
    {
        return $this->hasMany(App\Comment::class);
    }
}

用简单的话来描述即是:一篇文章有多条评论,一条评论属于某篇文章。

数据迁移

在我们定义模型以及关联关系后,接下来我们要创建评论表,打开我们生成的迁移文件 database/migrations/xxxx_xx_xx_xxxxxx_create_comments_table.php。编辑内容如下:

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id')->unsigned();
            $table->string('user');
            $table->text('comment');
            $table->boolean('approved');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('comments');
    }
}

这里的 post_id 将于 posts 表中的 id 关联,approved 表示评论是否通过审核,user 为评论发布人的名称。
接下来,执行命令 php artisan migrate 让这个新的数据库迁移脚本来创建我们需要的 comments 表。

comments 表结构

填充数据

和之前一样,我们对文章进行过数据填充一样,我们需要填充一些评论数据来进行模拟和测试。

评论数据模板

我们打开 database/factories/ModelFactory.php,新增评论模型数据模板:

$factory->define(App\Comment::class, function (Faker\Generator $faker) {
    return [
        'user' => $faker->name,
        'comment' => $faker->paragraphs(rand(1, 3), true),
        'approved' => rand(0, 1),
    ];
});

然后我们建立一个评论填充类,项目所在目录下执行命令 php artisan make:seed CommentsTableSeeder,打开文件 database/seeds/CommentsTableSeeder.php,编辑内容如以下所示:

<?php

use Illuminate\Database\Seeder;

class CommentsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $posts = App\Post::where('id', '<=', 10)->get();

        foreach ($posts as $post) {
            factory(App\Comment::class, rand(10, 20))->create()->each(function($c) use ($post) {
                $c->post()->associate($post);
                $c->save();
            });
        }
    }
}

在这个数据填充类中,我们的任务是,获取 id 小于等于前 10 的文章,为它们产生评论数据,每篇产生不等于 1020 条评论记录。完成了数据填充类,接下来执行数据填充命令将测试数据添加到数据库中:

php artisan db:seed --class=CommentsTableSeeder

这个命令指定了仅执行 CommentsTableSeeder 类指定的数据填充任务。执行完毕后,我们可以访问 http://localhost/phpmyadmin 来查看我们最终添加的实际测试数据了。

显示评论

有了之前的工作,接下来我们要为每一篇文章显示其评论。

控制器

接着我们需要更新 PostsControllershow 方法来获取文章的评论。打开 app/Http/Controllers/PostsController.php 更改为如下内容:

public function show($id)
{
    $post = Post::findOrFail($id);

    $comments = $post->comments;

    return view('posts.show', [
        'post' => $post, 
        'comments' => $comments
    ]);
}

这里的 $comments = $post->comments; 获取某篇文章的评论数据是依靠 Post.php 中定义的 comments() 模型关系实现的。

更多内容可以查阅 模型的查找关联

文章显示视图

我们现在获取到了评论的数据,可以更新博客的视图来显示评论信息。我们可以在博客模板页面直接放入评论内容,不过由于评论相对于文章有一定的独立性,比较建议将评论部分的内容单独作为模板,然后再引入到博客模板页面,这样我们可以在应用中重复使用评论的显示模板。打开 resources/views/posts/show.blade.php,更新 body 块中的内容,如下所示:

@section('body')
    <article class="blog">
        <header>
            <div class="date">{{ $post->created_at->format('l, F j, Y') }}</div>
            <h2>{{ $post->title }}</h2>
        </header>
        <img src="{{ asset('images/'.$post->image) }}" alt="{{ $post->title }} image not found" class="large" />
        <div>
            <p>{{ $post->content }}</p>
        </div>
    </article>

    <section class="comments" id="comments">
        <section class="previous-comments">
            <h3>Comments</h3>
            @include('comments.index', ['comments' => $comments])
        </section>
    </section>
@endsection

你可以看到我们通过模板标签 @include 引入 comments/index.blade.php 文件中的内容。同时我们向引入的模板传入了参数 comments,它是评论的数据。

评论显示视图

新建 resources/views/comments/index.blade.php,内容如下:

@forelse($comments as $i => $comment)
    <article class="comment {{ $i % 2 == 0 ? 'odd' : 'even' }}" id="comment-{{ $comment->id }}">
        <header>
            <p><span class="highlight">{{ $comment->user }}</span> commented {{ $comment->created_at->format('l, F j, Y') }}</p>
        </header>
        <p>{{ $comment->comment }}</p>
    </article>
@empty
    <p>There are no comments for this post. Be the first to comment...</p>
@endforelse()

如你所见,我们通过迭代循环输出了每一条评论的信息。

评论显示 CSS

最后我们增加一些 CSS 来让评论看起来会更漂亮,打开 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; }

如果你通过浏览器打开 http://localhost:8000/posts/1 页面,文章的评论将会呈现在你的面前。

文章评论显示效果

新增评论

这个章节的最后一个部分会加入一个功能让用户能对文章进行评论,我们通过博客文章的详情页表单来进行处理。我们构建联系页面时已介绍过如何提交表单和处理表单,接下来也会按照之前的思路来建立这个表单功能。

路由

我们需要建立一个新的路由来处理表单请求的提交。打开 app/Http/routes.php,添加如下内容:

Route::post('posts/{post_id}/comments', 'CommentsController@store');

我们建立一了一个接受 POST 请求的路由配置信息,它通过的 CommentsControllerstore 进行处理。

表单请求处理

接下来,我们建立表单请求,在命令行中进入项目目录执行如下命令 php artisan make:request CommentRequest
打开表单请求文件 app/Http/Requests/CommentRequest.php,编辑内容如下所示:

<?php

namespace App\Http\Requests;

use App\Http\Requests\Request;

class CommentRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'user' => 'required',
            'comment' => 'required'
        ];
    }
}

由于我们不要需要用户授权认证,所以 authorize 方法直接返回 true,我们在 rules 中定义了表单字段的要求,姓名 user 和评论内容 comment 为必填内容。

控制器处理

定义了路由和表单请求,我们需要建立控制器来处理请求,在命令行中国年进入项目目录,执行 php artisan make:controller CommentsController,修改内容如下所示:

<?php

namespace App\Http\Controllers;

use App\Post;
use App\Comment;
use App\Http\Requests\CommentRequest;

class CommentsController extends Controller
{
    
    public function store(CommentRequest $request, $postId)
    {
        $post = Post::findOrFail($postId);

        $comment = new Comment;
        $comment->user = $request->get('user');
        $comment->comment = $request->get('comment');
        $comment->post()->associate($post);
        $comment->save();

        return redirect()->back()->with('message', 'Success!');
    }
}

请求的 URL(posts/{post_id}/comments) 中包含的 post_id 可以通过控制器方法的参数 $postId 获取到相应的值。
其次,我们之前提到建立的 CommentRequest 会先处理表单提交过来的请求,表单合法后才会执行控制器方法中的内容。
我们在 store 中方法实例化了 Comment,获取请求的数据设置给评论模型的实例,还为评论实例设置了关联对象实例 $post,最后通过 save 方法保存实例到数据库,整个处理过程就是这样。

视图显示

接着我们来建立评论的表单模板,位于 resources/views/comments/new.blade.php,内容如下:

<form action="/posts/{{ $post->id }}/comments" method="post" class="blogger">
    {!! csrf_field() !!}

    <div>
        <label for="user" class="required">User</label>
        <input type="text" id="user" name="user" required="required" />
    </div>
    <div>
        <label for="comment" class="required">Comment</label>
        <textarea id="comment" name="comment" required="required"></textarea>
    </div>
    <p><input type="submit" value="Submit"></p>
</form>

同时,我还要修改博客文章的显示模板 resources/views/posts/show.blade.php, 修改为以下内容:

@extends('layouts.app')

@section('title', $post->title)

@section('body')
    <article class="blog">
        <header>
            <div class="date">{{ $post->created_at->format('l, F j, Y') }}</div>
            <h2>{{ $post->title }}</h2>
        </header>
        <img src="{{ asset('images/'.$post->image) }}" alt="{{ $post->title }} image not found" class="large" />
        <div>
            <p>{{ $post->content }}</p>
        </div>
    </article>

    <section class="comments" id="comments">
        <section class="previous-comments">
            <h3>Comments</h3>
            @include('comments.index', ['comments' => $comments])
        </section>

        <h3>Add Comment</h3>
        @include('comments.new', ['post' => $post])
    </section>
@endsection

我们在文章详情的模板页面引入了添加评论的表单视图模板 comments.new,同时传入当前文章对象。通过片段组合的方式我们将文章模板和评论模板联系在一了一起。

打开浏览器,访问网站中其中一篇文章的详情页面,你现在应该可以对文章进行评论了,试试看。

文章添加评论

总结

我们在这个章节有不错的进展,我们的博客开始像预期一样的运作。我们现在建立了基本的首页与评论模型,用户可以给某篇文章发表评论以及阅读其它用户留下的评论,我们看到如何建立多个数据模型之间的关系,并使用模型来建立查询和存储操作。

接下来我们会建立一些页面片段,包含标签云和最新评论,我们也会来了解如何扩展模版引擎,最后我们会看看如何管理静态资源库。

推荐阅读更多精彩内容