85.锁定话题(二)
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频教程第 85 小节:An Administrator May Lock Any Thread: Part 2
本节内容
本节我们继续锁定话题功能的开发。首先我们添加一个功能测试:
forum\tests\Feature\LockThreadsTest.php
.
.
/** @test */
public function non_administrator_may_not_lock_threads()
{
$this->signIn();
$thread = create('App\Thread',[
'user_id' => auth()->id()
]);
$this->patch($thread->path(),[
'locked' => true
])->assertStatus(403);
$this->assertFalse(!! $thread->fresh()->locked);
}
/** @test */
public function once_locked_thread_may_not_receive_new_replies()
{
.
.
}
}
这个测试是确保非管理员角色的用户无法锁定话题。在之前的章节中,我们设定了用户 NoNo1 是管理员。我们本节继续这么设定,保持简单:
forum\app\User.php
.
.
public function confirm()
{
$this->confirmed = true;
$this->confirmation_token = null;
$this->save();
}
public function isAdmin()
{
return in_array($this->name,['NoNo1']);
}
.
.
为了让测试通过,我们来添加patch
路由:
forum\routes\web.php
.
.
Route::get('threads/{channel}/{thread}','ThreadsController@show');
Route::patch('threads/{channel}/{thread}','ThreadsController@update')->name('threads.update');
.
.
添加update()
方法:
forum\app\Http\Controllers\ThreadsController.php
.
.
public function update()
{
if (request()->has('locked')) {
if(! auth()->user()->isAdmin()) {
return response('',403);
}
}
}
public function destroy($channel,Thread $thread)
{
.
.
}
.
.
运行测试:
我们继续添加功能测试:
forum\tests\Feature\LockThreadsTest.php
.
.
public function non_administrator_may_not_lock_threads()
{
.
.
}
/** @test */
public function administrators_can_lock_threads()
{
$this->signIn(factory('App\User')->states('administrator')->create());
$thread = create('App\Thread',['user_id' => auth()->id()]);
$this->patch($thread->path(),[
'locked' => true
]);
$this->assertTrue(!! $thread->fresh()->locked);
}
.
.
在上面测试中,我们让管理员登录,为了更具可读性,我们使用了states('administrator')
。我们修改模型工厂文件,添加states('administrator')
声明:
forum\database\factories\ModelFactory.php
.
.
$factory->state(App\User::class,'unconfirmed',function () {
return [
'confirmed' => false
];
});
$factory->state(App\User::class,'administrator',function () {
return [
'name' => 'NoNo1'
];
});
.
.
我们还需要修改控制器,当登录用户为管理员时进行更新locked
字段:
forum\app\Http\Controllers\ThreadsController.php
.
.
public function update($channelId,Thread $thread)
{
if (request()->has('locked')) {
if(! auth()->user()->isAdmin()) {
return response('',403);
}
$thread->lock();
}
}
.
.
运行测试:
接下来我们来做点重构。我们知道,很多的动作只有管理员才能触发,所以我们把验证用户是否是管理员的逻辑封装成中间件,在需要验证的控制器中开启此中间件即可。首先我们新建中间件:
$ php artisan make:middleware Administrator
forum\app\Http\Middleware\Administrator.php
<?php
namespace App\Http\Middleware;
use Closure;
class Administrator
{
public function handle($request, Closure $next)
{
if(auth()->check() && auth()->user()->isAdmin()) {
return $next($request);
}
abort(403,'You do not have permission to do so.');
}
}
然后我们注册中间件:
forum\app\Http\Kernel.php
<?php
namespace App\Http;
use App\Http\Middleware\Administrator;
use App\Http\Middleware\RedirectIfEmailNotConfirmed;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
.
.
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'must-be-confirmed' => \App\Http\Middleware\RedirectIfEmailNotConfirmed::class,
'admin' => \App\Http\Middleware\Administrator::class,
];
}
接着我们来把锁定话题的逻辑放到单独的控制器中。因为在锁定话题时,我们实际上只需更新locked
字段即可。如果放到话题控制器的update
方法中,那会与更新话题的逻辑混杂,所以我们抽取成单独的控制器。首先需要修改路由:
forum\routes\web.php
.
.
Route::get('threads','ThreadsController@index')->name('threads');
Route::get('threads/create','ThreadsController@create');
Route::get('threads/{channel}/{thread}','ThreadsController@show');
Route::delete('threads/{channel}/{thread}','ThreadsController@destroy');
Route::post('threads','ThreadsController@store')->middleware('must-be-confirmed');
Route::get('threads/{channel}','ThreadsController@index');
Route::post('locked-threads/{thread}','LockedThreadsController@store')->name('locked-threads.store')->middleware('admin');
.
.
我们移除了本节之前定义的路由,并且为新定义的路由应用了中间件:admin
。然后我们来新建控制器:
$ php artisan make:controller LockedThreadsController
将锁定话题的逻辑移到store()
方法中:
forum\app\Http\Controllers\LockedThreadsController.php
<?php
namespace App\Http\Controllers;
use App\Thread;
class LockedThreadsController extends Controller
{
public function store(Thread $thread)
{
$thread->lock();
}
}
最后,我们需要更新我们的测试:
forum\tests\Feature\LockThreadsTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class LockThreadsTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function non_administrator_may_not_lock_threads()
{
// 开启
$this->withExceptionHandling();
$this->signIn();
$thread = create('App\Thread',[
'user_id' => auth()->id()
]);
// 更改
$this->post(route('locked-threads.store',$thread))->assertStatus(403);
$this->assertFalse(!! $thread->fresh()->locked);
}
/** @test */
public function administrators_can_lock_threads()
{
$this->signIn(factory('App\User')->states('administrator')->create());
$thread = create('App\Thread',['user_id' => auth()->id()]);
// 更改
$this->post(route('locked-threads.store',$thread));
$this->assertTrue(!! $thread->fresh()->locked);
}
/** @test */
public function once_locked_thread_may_not_receive_new_replies()
{
$this->signIn();
$thread = create('App\Thread');
$thread->lock();
$this->post($thread->path() . '/replies',[
'body' => 'Foobar',
'user_id' => auth()->id()
])->assertStatus(422);
}
}
我们运行测试: