0

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

Marcin Nabiałek
  • 109,655
  • 42
  • 258
  • 291
Tom Morison
  • 564
  • 1
  • 8
  • 26

2 Answers2

1

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

Marcin Nabiałek
  • 109,655
  • 42
  • 258
  • 291
0

First thing is you are directly deleting all the Replies before you are deleting the Thread. So there are no replies to loop through for that thread when the deleting listener is called.

When you directly call delete on a Builder, it does not use the Model's delete method. It directly does a DELETE query so there are no Model events fired. So there is no deleting event fired for your Replies in this case: $thread->replies()->delete(). That is a direct DELETE query on the database. You would have to spin through the replies and call delete to have the model events fired for each.

In short don't do that and let your other listener handle deleting the records.

lagbox
  • 48,571
  • 8
  • 72
  • 83