Menu

11.根据频道来筛选话题

本节说明

  • 对应视频第 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]);
    $threadNotInChanne = create('App\Thread');

    $this->get('/threads/' . $channel->slug)
        ->assertSee($threadInChannel->title)
        ->assertDontSee($threadNotInChanne->title);
}
.
.

在测试用例中,我们新建了一个Channel两个Thread,其中一个Threadchannel_id是我们新建Channelid。我们的测试是,当我们通过该Channle来筛选Thread,我们希望看到与该Channel相关的Thread,并且不看到与该Channel无关的Thread
运行测试:
file
让我们来修复它:
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

测试通过:
file
现在我们的测试已经通过,接下来我们来完善刚刚编写的代码:

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

运行一下测试:
file
因为此时尚未建立模型关联,因此$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);
    }
}

运行测试:
file
测试已经 OK ,但是我们现在还有一个问题要注意:如果我们访问 http://forum.test/threads/{channel} 这样的路由,我们是无法访问的:
file
注意看一下我们的路由定义:

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

file
先看一下失败的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');
.

再次运行测试,即可成功通过:
file
现在我们已经可以根据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的代码稍后需要完善,目前的比较粗糙

看一下效果:

file

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 2
发起讨论


Chasers9527
为什么不合并一下路由呢?
0 个点赞 | 0 个回复 | 问答
zh117