Laravel each() 闭包未执行
Laravel each() closure not executing
我正在关注 Laracast。 (https://laracasts.com/series/lets-build-a-forum-with-laravel/episodes/28) 我正在努力让我的一项测试通过。我做了一些调试。在 RecordActivity.php 我目前有以下内容:
static::deleting(function ($thread) {
$thread->replies->each(function($reply) {
dd($reply);
$reply->delete();
});
});
然而,当我 运行 在课程中创建测试时,在我看来 each()
中的闭包永远不会触发,因为测试 returns 错误而不是吐出回复.我重新看了几次视频,并将我的代码与 github 存储库中 Jeffery 的代码进行了比较。
我做错了什么?
RecordActivity.php
<?php
namespace App;
trait RecordActivity
{
protected static function bootRecordActivity()
{
if (auth()->guest()) return;
foreach (static::getRecordEvents() as $event) {
static::$event(function ($model) use ($event) {
$model->recordActivity($event);
});
}
static::deleting(function ($model) {
$model->activity()->delete();
});
}
protected function recordActivity($event)
{
$this->activity()->create([
'user_id' => auth()->id(),
'type' => $this->getActivityType($event)
]);
}
protected static function getRecordEvents()
{
return ['created'];
}
public function activity() {
return $this->morphMany('App\Activity', 'subject');
}
protected function getActivityType($event)
{
$type = strtolower((new \ReflectionClass($this))->getShortName());
return "{$event}_{$type}";
}
}
Thread.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Thread extends Model
{
use RecordActivity;
protected $guarded = [];
protected $with = ['creator', 'channel'];
protected static function boot()
{
parent::boot();
static::addGlobalScope('replyCount', function ($builder) {
$builder->withCount('replies');
});
static::deleting(function ($thread) {
$thread->replies->each(function($reply) {
dd($reply);
$reply->delete();
});
});
}
public function path() {
return page_url('forum',"threads/" . $this->channel->slug . '/'. $this->id);
}
public function replies() {
return $this->hasMany(Reply::class);
}
public function channel() {
return $this->belongsTo(Channel::class);
}
public function creator()
{
return $this->belongsTo(User::class, 'user_id');
}
public function addReply($reply) {
$this->replies()->create($reply);
}
public function scopeFilter($query, $filters) {
return $filters->apply($query);
}
}
CreateThreadsTest.php
<?php
namespace Tests\Feature;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
use App\Thread;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class CreateThreadsTest extends TestCase
{
use DatabaseMigrations;
function test_guests_may_not_create_threads() {
$thread = make('App\Thread');
$this->withExceptionHandling()->post(page_url('forum','threads'), $thread->toArray())->assertRedirect('/login');
}
function test_an_authenticated_user_can_create_new_forum_threads() {
$this->signIn();
$thread = make('App\Thread');
$response = $this->post(page_url('forum','/threads'), $thread->toArray());
$this->get($response->headers->get('Location'))
->assertSee($thread->title)
->assertSee($thread->body);
}
function test_a_thread_requires_a_title()
{
$this->publishThread(['title' => null])->assertSessionHasErrors(['title']);
}
function test_a_thread_requires_a_body()
{
$this->publishThread(['body' => null])->assertSessionHasErrors(['body']);
}
function test_a_thread_requires_a_channel_id()
{
factory('App\Channel', 2)->create();
$this->publishThread(['channel_id' => 999])->assertSessionHasErrors(['channel_id']);
}
function test_guests_cannot_delete_threads() {
$thread = create('App\Thread');
$this->delete($thread->path())->assertRedirect('/login');
$this->signIn();
$this->delete($thread->path())->assertStatus(403);
}
//
// function test_threads_may_only_be_deleted_by_those_who_have_permission() {
//
// }
function test_authorized_users_can_delete_threads() {
$this->signIn();
$thread = create('App\Thread', ['user_id' => auth()->id()]);
$reply = create('App\Reply', ['thread_id' => $thread->id]);
$response = $this->json('DELETE', $thread->path());
$response->assertStatus(204);
$this->assertDatabaseMissing('threads', ['id' => $thread->id]);
$this->assertDatabaseMissing('replies', ['id' => $reply->id]);
$this->assertDatabaseMissing('activities', ['subject_id' => $thread->id, 'subject_type' => get_class($thread)]);
$this->assertDatabaseMissing('activities', ['subject_id' => $reply->id, 'subject_type' => get_class($reply)]);
}
public function publishThread($overrides) {
$this->withExceptionHandling()->signIn();
$thread = make('App\Thread', $overrides);
return $this->post(page_url('forum','/threads'), $thread->toArray());
}
}
ThreadsController.php
<?php
namespace App\Http\Controllers;
use App\Filters\ThreadFilters;
use Illuminate\Http\Request;
use App\Thread;
use App\Channel;
use Auth;
class ThreadsController extends Controller
{
public function __construct() {
$this->middleware('auth')->except(['index', 'show']);
}
/**
* Display a listing of the resource.
*
* @param Channel $channel
* @param \App\Http\Controllers\ThreadFilters $filter
* @return \Illuminate\Http\Response
*/
public function index(Channel $channel, ThreadFilters $filter)
{
$threads = $this->getThread($channel, $filter);
if(request()->wantsJson()) {
return $threads;
}
return view('threads.index', compact('threads'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('threads.create');
}
/**
// * Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate(request(), [
'title' =>'required',
'body' => 'required',
'channel_id' => 'required|exists:channels,id'
]);
$thread = Thread::create([
'user_id' => Auth::user()->id,
'channel_id' => request('channel_id'),
'title' => request('title'),
'body' => request('body')
]);
return redirect($thread->path());
}
/**
* Display the specified resource.
*
* @param int $id
*/
public function show($channelSlug, Thread $thread)
{
return view('threads.show', [
'thread' => $thread,
'replies' => $thread->replies()->paginate(25)
]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $channel
* @return \Illuminate\Http\Response
*/
public function destroy($channel, Thread $thread)
{
$this->authorize("update", $thread);
$thread->replies()->delete();
$thread->delete();
if(request()->wantsJson()) {
return response([], 204);
}
return redirect(page_url('forum','/threads'));
}
protected function getThread(Channel $channel, ThreadFilters $filter) {
$threads = Thread::latest()->filter($filter);
if($channel->exists) {
$threads->where('channel_id', $channel->id);
}
return $threads->get();
}
}
<?php
namespace App\Http\Controllers;
use App\Filters\ThreadFilters;
use Illuminate\Http\Request;
use App\Thread;
use App\Channel;
use Auth;
class ThreadsController extends Controller
{
public function __construct() {
$this->middleware('auth')->except(['index', 'show']);
}
/**
* Display a listing of the resource.
*
* @param Channel $channel
* @param \App\Http\Controllers\ThreadFilters $filter
* @return \Illuminate\Http\Response
*/
public function index(Channel $channel, ThreadFilters $filter)
{
$threads = $this->getThread($channel, $filter);
if(request()->wantsJson()) {
return $threads;
}
return view('threads.index', compact('threads'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('threads.create');
}
/**
// * Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate(request(), [
'title' =>'required',
'body' => 'required',
'channel_id' => 'required|exists:channels,id'
]);
$thread = Thread::create([
'user_id' => Auth::user()->id,
'channel_id' => request('channel_id'),
'title' => request('title'),
'body' => request('body')
]);
return redirect($thread->path());
}
/**
* Display the specified resource.
*
* @param int $id
*/
public function show($channelSlug, Thread $thread)
{
return view('threads.show', [
'thread' => $thread,
'replies' => $thread->replies()->paginate(25)
]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $channel
* @return \Illuminate\Http\Response
*/
public function destroy($channel, Thread $thread)
{
$this->authorize("update", $thread);
$thread->replies()->delete();
$thread->delete();
if(request()->wantsJson()) {
return response([], 204);
}
return redirect(page_url('forum','/threads'));
}
protected function getThread(Channel $channel, ThreadFilters $filter) {
$threads = Thread::latest()->filter($filter);
if($channel->exists) {
$threads->where('channel_id', $channel->id);
}
return $threads->get();
}
}
PHP 单元错误:
PHPUnit 7.5.18 by Sebastian Bergmann and contributors.
.
................F............ 30 / 30 (100%)
Time: 15.02 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\CreateThreadsTest::test_authorized_users_can_delete_threads
Failed asserting that a row in the table [activities] does not match the attributes {
"subject_id": 1,
"subject_type": "App\Reply"
}.
Found: [
{
"id": "2",
"user_id": "1",
"subject_type": "App\Reply",
"subject_id": "1",
"type": "created_reply",
"created_at": "2019-12-23 20:26:03",
"updated_at": "2019-12-23 20:26:03"
}
].
/home/vagrant/Code/intransportal/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php:44
/home/vagrant/Code/intransportal/tests/Feature/CreateThreadsTest.php:75
为什么我的测试没有转储 $reply
模型
问题出在您的控制器上:
$thread->replies()->delete();
$thread->delete();
您先删除回复,然后删除话题,所以当:
static::deleting(function ($thread) {
$thread->replies->each(function($reply) {
dd($reply);
$reply->delete();
});
});
执行 $thread->replies
returns 空集合,因为您刚刚在控制器中删除了它们。
您应该删除控制器中的 $thread->replies()->delete();
行
首先,您在删除主题之前直接 删除了所有回复。因此,当 deleting
侦听器被调用时,该线程没有要循环的回复。
当您直接 调用构建器上的delete
时,它不会使用模型的delete
方法。它直接执行 DELETE 查询,因此不会触发任何模型事件。因此,在这种情况下,不会为您的回复触发 deleting
事件:$thread->replies()->delete()
。这是对数据库的直接 DELETE 查询。您必须浏览回复并调用 delete
才能为每个回复触发模型事件。
简而言之,不要这样做,让您的其他侦听器处理删除记录。
我正在关注 Laracast。 (https://laracasts.com/series/lets-build-a-forum-with-laravel/episodes/28) 我正在努力让我的一项测试通过。我做了一些调试。在 RecordActivity.php 我目前有以下内容:
static::deleting(function ($thread) {
$thread->replies->each(function($reply) {
dd($reply);
$reply->delete();
});
});
然而,当我 运行 在课程中创建测试时,在我看来 each()
中的闭包永远不会触发,因为测试 returns 错误而不是吐出回复.我重新看了几次视频,并将我的代码与 github 存储库中 Jeffery 的代码进行了比较。
我做错了什么?
RecordActivity.php
<?php
namespace App;
trait RecordActivity
{
protected static function bootRecordActivity()
{
if (auth()->guest()) return;
foreach (static::getRecordEvents() as $event) {
static::$event(function ($model) use ($event) {
$model->recordActivity($event);
});
}
static::deleting(function ($model) {
$model->activity()->delete();
});
}
protected function recordActivity($event)
{
$this->activity()->create([
'user_id' => auth()->id(),
'type' => $this->getActivityType($event)
]);
}
protected static function getRecordEvents()
{
return ['created'];
}
public function activity() {
return $this->morphMany('App\Activity', 'subject');
}
protected function getActivityType($event)
{
$type = strtolower((new \ReflectionClass($this))->getShortName());
return "{$event}_{$type}";
}
}
Thread.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Thread extends Model
{
use RecordActivity;
protected $guarded = [];
protected $with = ['creator', 'channel'];
protected static function boot()
{
parent::boot();
static::addGlobalScope('replyCount', function ($builder) {
$builder->withCount('replies');
});
static::deleting(function ($thread) {
$thread->replies->each(function($reply) {
dd($reply);
$reply->delete();
});
});
}
public function path() {
return page_url('forum',"threads/" . $this->channel->slug . '/'. $this->id);
}
public function replies() {
return $this->hasMany(Reply::class);
}
public function channel() {
return $this->belongsTo(Channel::class);
}
public function creator()
{
return $this->belongsTo(User::class, 'user_id');
}
public function addReply($reply) {
$this->replies()->create($reply);
}
public function scopeFilter($query, $filters) {
return $filters->apply($query);
}
}
CreateThreadsTest.php
<?php
namespace Tests\Feature;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
use App\Thread;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class CreateThreadsTest extends TestCase
{
use DatabaseMigrations;
function test_guests_may_not_create_threads() {
$thread = make('App\Thread');
$this->withExceptionHandling()->post(page_url('forum','threads'), $thread->toArray())->assertRedirect('/login');
}
function test_an_authenticated_user_can_create_new_forum_threads() {
$this->signIn();
$thread = make('App\Thread');
$response = $this->post(page_url('forum','/threads'), $thread->toArray());
$this->get($response->headers->get('Location'))
->assertSee($thread->title)
->assertSee($thread->body);
}
function test_a_thread_requires_a_title()
{
$this->publishThread(['title' => null])->assertSessionHasErrors(['title']);
}
function test_a_thread_requires_a_body()
{
$this->publishThread(['body' => null])->assertSessionHasErrors(['body']);
}
function test_a_thread_requires_a_channel_id()
{
factory('App\Channel', 2)->create();
$this->publishThread(['channel_id' => 999])->assertSessionHasErrors(['channel_id']);
}
function test_guests_cannot_delete_threads() {
$thread = create('App\Thread');
$this->delete($thread->path())->assertRedirect('/login');
$this->signIn();
$this->delete($thread->path())->assertStatus(403);
}
//
// function test_threads_may_only_be_deleted_by_those_who_have_permission() {
//
// }
function test_authorized_users_can_delete_threads() {
$this->signIn();
$thread = create('App\Thread', ['user_id' => auth()->id()]);
$reply = create('App\Reply', ['thread_id' => $thread->id]);
$response = $this->json('DELETE', $thread->path());
$response->assertStatus(204);
$this->assertDatabaseMissing('threads', ['id' => $thread->id]);
$this->assertDatabaseMissing('replies', ['id' => $reply->id]);
$this->assertDatabaseMissing('activities', ['subject_id' => $thread->id, 'subject_type' => get_class($thread)]);
$this->assertDatabaseMissing('activities', ['subject_id' => $reply->id, 'subject_type' => get_class($reply)]);
}
public function publishThread($overrides) {
$this->withExceptionHandling()->signIn();
$thread = make('App\Thread', $overrides);
return $this->post(page_url('forum','/threads'), $thread->toArray());
}
}
ThreadsController.php
<?php
namespace App\Http\Controllers;
use App\Filters\ThreadFilters;
use Illuminate\Http\Request;
use App\Thread;
use App\Channel;
use Auth;
class ThreadsController extends Controller
{
public function __construct() {
$this->middleware('auth')->except(['index', 'show']);
}
/**
* Display a listing of the resource.
*
* @param Channel $channel
* @param \App\Http\Controllers\ThreadFilters $filter
* @return \Illuminate\Http\Response
*/
public function index(Channel $channel, ThreadFilters $filter)
{
$threads = $this->getThread($channel, $filter);
if(request()->wantsJson()) {
return $threads;
}
return view('threads.index', compact('threads'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('threads.create');
}
/**
// * Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate(request(), [
'title' =>'required',
'body' => 'required',
'channel_id' => 'required|exists:channels,id'
]);
$thread = Thread::create([
'user_id' => Auth::user()->id,
'channel_id' => request('channel_id'),
'title' => request('title'),
'body' => request('body')
]);
return redirect($thread->path());
}
/**
* Display the specified resource.
*
* @param int $id
*/
public function show($channelSlug, Thread $thread)
{
return view('threads.show', [
'thread' => $thread,
'replies' => $thread->replies()->paginate(25)
]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $channel
* @return \Illuminate\Http\Response
*/
public function destroy($channel, Thread $thread)
{
$this->authorize("update", $thread);
$thread->replies()->delete();
$thread->delete();
if(request()->wantsJson()) {
return response([], 204);
}
return redirect(page_url('forum','/threads'));
}
protected function getThread(Channel $channel, ThreadFilters $filter) {
$threads = Thread::latest()->filter($filter);
if($channel->exists) {
$threads->where('channel_id', $channel->id);
}
return $threads->get();
}
}
<?php
namespace App\Http\Controllers;
use App\Filters\ThreadFilters;
use Illuminate\Http\Request;
use App\Thread;
use App\Channel;
use Auth;
class ThreadsController extends Controller
{
public function __construct() {
$this->middleware('auth')->except(['index', 'show']);
}
/**
* Display a listing of the resource.
*
* @param Channel $channel
* @param \App\Http\Controllers\ThreadFilters $filter
* @return \Illuminate\Http\Response
*/
public function index(Channel $channel, ThreadFilters $filter)
{
$threads = $this->getThread($channel, $filter);
if(request()->wantsJson()) {
return $threads;
}
return view('threads.index', compact('threads'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('threads.create');
}
/**
// * Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate(request(), [
'title' =>'required',
'body' => 'required',
'channel_id' => 'required|exists:channels,id'
]);
$thread = Thread::create([
'user_id' => Auth::user()->id,
'channel_id' => request('channel_id'),
'title' => request('title'),
'body' => request('body')
]);
return redirect($thread->path());
}
/**
* Display the specified resource.
*
* @param int $id
*/
public function show($channelSlug, Thread $thread)
{
return view('threads.show', [
'thread' => $thread,
'replies' => $thread->replies()->paginate(25)
]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $channel
* @return \Illuminate\Http\Response
*/
public function destroy($channel, Thread $thread)
{
$this->authorize("update", $thread);
$thread->replies()->delete();
$thread->delete();
if(request()->wantsJson()) {
return response([], 204);
}
return redirect(page_url('forum','/threads'));
}
protected function getThread(Channel $channel, ThreadFilters $filter) {
$threads = Thread::latest()->filter($filter);
if($channel->exists) {
$threads->where('channel_id', $channel->id);
}
return $threads->get();
}
}
PHP 单元错误:
PHPUnit 7.5.18 by Sebastian Bergmann and contributors.
.
................F............ 30 / 30 (100%)
Time: 15.02 seconds, Memory: 30.00 MB
There was 1 failure:
1) Tests\Feature\CreateThreadsTest::test_authorized_users_can_delete_threads
Failed asserting that a row in the table [activities] does not match the attributes {
"subject_id": 1,
"subject_type": "App\Reply"
}.
Found: [
{
"id": "2",
"user_id": "1",
"subject_type": "App\Reply",
"subject_id": "1",
"type": "created_reply",
"created_at": "2019-12-23 20:26:03",
"updated_at": "2019-12-23 20:26:03"
}
].
/home/vagrant/Code/intransportal/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php:44
/home/vagrant/Code/intransportal/tests/Feature/CreateThreadsTest.php:75
为什么我的测试没有转储 $reply
模型
问题出在您的控制器上:
$thread->replies()->delete();
$thread->delete();
您先删除回复,然后删除话题,所以当:
static::deleting(function ($thread) {
$thread->replies->each(function($reply) {
dd($reply);
$reply->delete();
});
});
执行 $thread->replies
returns 空集合,因为您刚刚在控制器中删除了它们。
您应该删除控制器中的 $thread->replies()->delete();
行
首先,您在删除主题之前直接 删除了所有回复。因此,当 deleting
侦听器被调用时,该线程没有要循环的回复。
当您直接 调用构建器上的delete
时,它不会使用模型的delete
方法。它直接执行 DELETE 查询,因此不会触发任何模型事件。因此,在这种情况下,不会为您的回复触发 deleting
事件:$thread->replies()->delete()
。这是对数据库的直接 DELETE 查询。您必须浏览回复并调用 delete
才能为每个回复触发模型事件。
简而言之,不要这样做,让您的其他侦听器处理删除记录。