Search code examples
phplaraveleloquentlaravel-6laravel-events

Laravel each() closure not executing


I am following a Laracast. (https://laracasts.com/series/lets-build-a-forum-with-laravel/episodes/28) I am trying to get one of my tests to pass. I have done a bit of debugging. In RecordActivity.php I currently have the following:


        static::deleting(function ($thread) {

            $thread->replies->each(function($reply) {
                dd($reply);
                $reply->delete();
            });
        });

However when I run the test created in the lession, it appears to me the closure in each() never fires because the test returns the error instead of spitting out the reply. I rewatched the video several times and compared my code to Jeffery's code in the github repo.

What am I doing wrong?

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

    }
}

PHPUnit error:

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

Why is my test not dumping the $reply Model


Solution

  • The problem is in your controller here:

    $thread->replies()->delete();
    $thread->delete();
    

    You first remove replies and then delete thread, so when:

    static::deleting(function ($thread) {
                $thread->replies->each(function($reply) {
                    dd($reply);
                    $reply->delete();
                });
            });
    

    is executed $thread->replies returns empty collection because you have just deleted them in your controller.

    You should remove $thread->replies()->delete(); line in your controller