11.根据频道来筛选话题
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频第 11 小节:A User Can Filter Threads By Channel
本节内容
上一节中我们引入了Channel
的概念:一个Thread
属于一个Channel
,一个Channel
拥有多个Thread
。现在我们来实现根据Channel
筛选Thread
的功能。
首先新建测试:
forum\tests\Feature\ReadThreadsTest.php
.
.
/** @test */
public function a_user_can_filter_threads_according_to_a_channel()
{
$channel = create('App\Channel');
$threadInChannel = create('App\Thread',['channel_id' => $channel->id]);
$threadNotInChannel = create('App\Thread');
$this->get('/threads/' . $channel->slug)
->assertSee($threadInChannel->title)
->assertDontSee($threadNotInChannel->title);
}
.
.
在测试用例中,我们新建了一个Channel
两个Thread
,其中一个Thread
的channel_id
是我们新建Channel
的id
。我们的测试是,当我们通过该Channle
来筛选Thread
,我们希望看到与该Channel
相关的Thread
,并且不看到与该Channel
无关的Thread
。
运行测试:
让我们来修复它:
forum\routes\web.php
.
.
Route::get('/home', 'HomeController@index')->name('home');
Route::get('threads/{channel}','ThreadsController@index'); -->修改此处路由
Route::get('threads/create','ThreadsController@create');
Route::get('threads/{channel}/{thread}','ThreadsController@show');
Route::post('threads','ThreadsController@store');
Route::post('/threads/{channel}/{thread}/replies','RepliesController@store');
.
.
forum\app\Http\Controllers\ThreadsController.php
.
.
public function index($channelSlug = null)
{
if($channelSlug){
$channelId = Channel::where('slug',$channelSlug)->first()->id;
$threads = Thread::where('channel_id',$channelId)->latest()->get();
}else{
$threads = Thread::latest()->get();
}
return view('threads.index',compact('threads'));
}
.
.
可以看到,我们在控制器中的代码是很粗糙的。我们这么做的原因是为了先让测试通过,稍后进行修改。现在来运行测试:
$ APP_ENV=testing phpunit --filter a_user_can_filter_threads_according_to_a_channel
测试通过:
现在我们的测试已经通过,接下来我们来完善刚刚编写的代码:
public function index(Channel $channel)
{
if($channel->exists){
$threads = $channel->threads()->latest()->get();
}else{
$threads = Thread::latest()->get();
}
return view('threads.index',compact('threads'));
}
注意到我们使用了
threads()
来获取$threads
,但模型关联尚未建立,需要先编写单元测试,这也是 TDD 的开发理念。
我们来编写单元测试:
forum\tests\Unit\ChannelTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class ChannelTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_channel_consists_of_threads()
{
$channel = create('App\Channel');
$thread = create('App\Thread',['channel_id' => $channel->id]);
$this->assertTrue($channel->threads->contains($thread));
}
}
运行一下测试:
因为此时尚未建立模型关联,因此$thread->threads
获取的对象为null
,所以就抛出了上面的异常。我们来建立模型关联:
forum\app\Channel.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Channel extends Model
{
public function threads()
{
return $this->hasMany(Thread::class);
}
}
运行测试:
测试已经 OK ,但是我们现在还有一个问题要注意:如果我们访问 http://forum.test/threads/{channel} 这样的路由,我们是无法访问的:
注意看一下我们的路由定义:
Route::get('threads/{channel}','ThreadsController@index');
我们知道,以上的路由符合 Laravel 的 隐性路由模型绑定 原则。但是{channel}
路由片段默认对应的是id
字段,而我们需要对应的是slug
字段。所以我们需要重写getRouteKeyName()
方法:
forum\app\Channel.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Channel extends Model
{
public function getRouteKeyName()
{
return 'slug';
}
.
.
}
运行一下全部的测试:
$ APP_ENV=testing phpunit
先看一下失败的guests_may_not_create_threads
测试:
forum\tests\Feature\CreateThreadsTest.php
.
.
/** @test */
public function guests_may_not_create_threads()
{
$this->withExceptionHandling();
$this->get('/threads/create')
->assertRedirect('/login');
$this->post('/threads')
->assertRedirect('/login');
}
.
.
注意看这一行$this->get('/threads/create')
。我们声明过/threads/{channel}
这样的路由,用来筛选该channel
下的所有相关thread
,在/threads/create
这样的url
中,Laravel 会认为create
是一个channel
。我们需要调整路由声明的顺序:
forum\routes\web.php
.
.
Route::get('/home', 'HomeController@index')->name('home');
Route::get('threads','ThreadsController@index'); -->在之前的课程中误删除了此路由,因此测试时报了一个错误
Route::get('threads/create','ThreadsController@create');
Route::get('threads/{channel}/{thread}','ThreadsController@show');
Route::post('threads','ThreadsController@store');
Route::get('threads/{channel}','ThreadsController@index'); -->将此路由放在后面声明
Route::post('/threads/{channel}/{thread}/replies','RepliesController@store');
.
再次运行测试,即可成功通过:
现在我们已经可以根据channel
来筛选thread
了,让我们在页面展示出来:
forum\resources\views\layouts\app.blade.php
.
.
<div class="collapse navbar-collapse" id="app-navbar-collapse">
<!-- Left Side Of Navbar -->
<ul class="nav navbar-nav">
<li><a href="/threads">All Threads</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-hidden="true"
aria-expanded="false">Channels <span class="caret"></span> </a>
<ul class="dropdown-menu">
@foreach(\App\Channel::all() as $channel)
<li><a href="/threads/{{ $channel->slug }}">{{ $channel->name }}</a> </li>
@endforeach
</ul>
</li>
</ul>
.
.
注:获取所有
channel
的代码稍后需要完善,目前的比较粗糙
看一下效果: