Click to share! ⬇️

How To Delete A Record From The Database

We should add a way for a user to be able to delete their thread from the database if they choose to do so. This tutorial will have us doing just that. First off we’ll set up a test or two to help us ensure the delete function works as intended. Next up, we’ll learn a little bit about sending response codes which is helpful when dealing with scenarios where the back end is acting as an API. Another hot topic will be learning about how to delete related models when deleting a given record. When we delete a thread, we also want to consider the relationship to replies and delete associated replies as well. Last up we’ll take a quick look at deleting with model events and setting up the link the UI to actually trigger the delete.


Thread Can Be Deleted Test

In the Feature test directory we already have a class that deals with Threads. We can add a test called test_a_thread_can_be_deleted() to the CreateThreadsTest class to get started.
test_a_thread_can_be_deleted

What will our test do? Here is the pseudocode.

  • Given a user is signed in
  • Given there is a thread
  • When the user submits a json request to delete the thread
  • Then the thread should no longer exist in the database

With those steps in mind, here is our first stab at creating the code for this test.

public function test_a_thread_can_be_deleted()
{
    $this->withoutExceptionHandling()->signIn();

    $thread = create('AppThread');

    $this->json('DELETE', $thread->path());

    $this->assertDatabaseMissing('threads', $thread->toArray());
}

Running the test_a_thread_can_be_deleted() test gives us a MethodNotAllowedHttpException which we know means we are missing a route.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_thread_can_be_deleted
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 993 ms, Memory: 8.00MB

There was 1 error:

1) TestsFeatureCreateThreadsTest::test_a_thread_can_be_deleted
SymfonyComponentHttpKernelExceptionMethodNotAllowedHttpException:

As such, let’s open up our routes file and get to fixing that right away. Below is the new route highlighted for us.


<?php

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

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::delete('/threads/{channel}/{thread}', 'ThreadsController@destroy');
Route::post('/threads', 'ThreadsController@store');
Route::get('/threads/{channel}', 'ThreadsController@index');
Route::post('/threads/{channel}/{thread}/replies', 'RepliesController@store');
Route::post('/replies/{reply}/favorites', 'FavoritesController@store');
Route::get('/profiles/{user}', 'ProfilesController@show')->name('profile');

We can run the test again, but now we find a different error.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_thread_can_be_deleted
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 1.04 seconds, Memory: 10.00MB

There was 1 error:

1) TestsFeatureCreateThreadsTest::test_a_thread_can_be_deleted
SymfonyComponentDebugExceptionFatalThrowableError: Type error: Argument 1 passed to AppHttpControllersThreadsController::destroy() must be an instance of AppThread, string given

We are getting a fatal throwable type error. Well, let’s look at the destroy method in the ThreadsController and see what we have.

public function destroy(Thread $thread)
{
    //
}

Of course this test would fail. We only have a method stub for destroy(). We need to update that method so it actually deletes a thread.

public function destroy($channel, Thread $thread)
{
    $thread->delete();
}

Let’s run the test again to see the result.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_thread_can_be_deleted
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 878 ms, Memory: 10.00MB

There was 1 error:

1) TestsFeatureCreateThreadsTest::test_a_thread_can_be_deleted
IlluminateDatabaseQueryException: SQLSTATE[HY000]: General error: 25 bind or column index out of range (SQL: select count(*) as aggregate from "threads" where ("user_id" = 2 and "channel_id" = 1 and "title" = Ea maxime et omnis. and "body" = Id velit sequi nulla est officiis sed. Laborum sit quas velit aliquid. Fugit culpa rerum enim. Quos dicta ut est maiores. and "updated_at" = 2018-01-24 23:02:50 and "created_at" = 2018-01-24 23:02:50 and "id" = 1 and "channel" = 1))

This actually might be an issue in the test code. Let’s change the test to this:


public function test_a_thread_can_be_deleted()
{
    $this->withoutExceptionHandling()->signIn();

    $thread = create('AppThread');

    $this->json('DELETE', $thread->path());

    $this->assertDatabaseMissing('threads', ['id' => $thread->id]);
}

Running the test one more time gives us a green for passing, so it looks like that change fixed us up.
beautiful green test passing result


Setting Response Code

When data is changed on the server as a result of a request sent to it, it makes sense to return a status code to indicate the result of that request. We can update the test and the destroy() method to facilitate this idea.


public function test_a_thread_can_be_deleted()
{
    $this->withoutExceptionHandling()->signIn();

    $thread = create('AppThread');

    $response = $this->json('DELETE', $thread->path());

    $response->assertStatus(204);

    $this->assertDatabaseMissing('threads', ['id' => $thread->id]);
}

public function destroy($channel, Thread $thread)
{
    $thread->delete();

    return response([], 204);
}

How To Delete Related Models

Threads may have replies. If a user deletes a thread, but that thread has replies associated with it, well then that is a problem at this point. Right now, those replies will be left in the database like phantom records. You don’t want that, so we need to look at how to delete related replies to any given thread. The first option is to simply reference the relationship in the destroy() method, and make a call to delete:


public function destroy($channel, Thread $thread)
{
    $thread->replies()->delete();
    $thread->delete();

    return response([], 204);
}

Since we are now deleting associated replies, let’s also update our test to reflect that.

public function test_a_thread_can_be_deleted()
{
    $this->withoutExceptionHandling()->signIn();

    $thread = create('AppThread');
    $reply = create('AppReply', ['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]);
}

That ought to do it, let’s run the test.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_thread_can_be_deleted
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 1.11 seconds, Memory: 8.00MB

There was 1 error:

1) TestsFeatureCreateThreadsTest::test_a_thread_can_be_deleted
InvalidArgumentException: A four digit year could not be found
Data missing

What the heck? That is a strange error. “InvalidArgumentException: A four digit year could not be found Data missing”. Ok, it looks like the wrong argument was passed to the create() method for the model factory. create(‘AppReply’, [‘thread_id’, $thread->id]) should be changed to create(‘AppReply’, [‘thread_id’ => $thread->id]). Here is the correct test with the lines highlighted which deal with related model relationships, and when we run the test it does pass. Yes!


public function test_a_thread_can_be_deleted()
{
    $this->withoutExceptionHandling()->signIn();

    $thread = create('AppThread');
    $reply = create('AppReply', ['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]);
}

Deleting With Model Events

As Eloquent is interacting with the database, it is triggering various events during the lifecycle of the interaction. We can hook into those events and take various actions if we like. In fact we can use Model Events to actually listen for a thread delete, and then delete associated replies as well. Go ahead and open up the Thread.php model, and we can look at how to set this up.


<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class Thread extends Model
{
    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()->delete();
        });
    }

    public function path()
    {
        return '/threads/' . $this->channel->slug . '/' . $this->id;
    }

    public function replies()
    {
        return $this->hasMany(Reply::class);
        //->withCount('favorites');
        //->with('owner');
    }

    public function creator()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function channel()
    {
        return $this->belongsTo(Channel::class);
    }

    public function addReply($reply)
    {
        $this->replies()->create($reply);
    }

    public function scopeFilter($query, $filters)
    {
        return $filters->apply($query);
    }
}

In the code above, we see the highlighted snippet inside of the boot() method on the Model. This is basically tapping into those model events for us. It is saying, when the thread is in the process of being deleted, go ahead and also delete all of the replies. Pretty cool, right? Now you can remove the call to $thread->replies()->delete() from the destroy() method on the ThreadsController and everything should still work. Running our test shows us that it is still working. Nice.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_thread_can_be_deleted
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 828 ms, Memory: 10.00MB

OK (1 test, 3 assertions)

Guests can not delete threads

Only logged in users should be able to delete their thread. We don’t want random visitors of the application to be able to just browse a thread, click a link, and delete the thread. We can set up a test to support this feature. Once again we try to follow the Given, When, Then format of setting up a test.

  • Given there is a guest
  • Given there is a thread
  • When a guest submits a json request to delete the thread
  • Then the response should be a redirect

The test may look like so:

public function test_guests_can_not_delete_threads()
{
    $thread = create('AppThread');

    $response = $this->json('DELETE', $thread->path());

    $response->assertRedirect('/');
}

Upon running the test, we get an error of “Response status code [401] is not a redirect status code.” Ok, this is because the request in the test is a json request and not a standard request.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_guests_can_not_delete_threads
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 993 ms, Memory: 10.00MB

There was 1 failure:

1) TestsFeatureCreateThreadsTest::test_guests_can_not_delete_threads
Response status code [401] is not a redirect status code.
Failed asserting that false is true.

/home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:93
/home/vagrant/Code/forumio/tests/Feature/CreateThreadsTest.php:61

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

To fix this, let’s update the test like so:

public function test_guests_can_not_delete_threads()
{
    $thread = create('AppThread');

    $response = $this->delete($thread->path());

    $response->assertRedirect('/login');
}

Run the test, see it passing, and be assured that guests can not delete a thread they shouldn’t be able to. Nice.
phpunit filter test example


Configure A Link To Delete A Model

The plumbing is in place for threads to be deleted, however the user interface needs an actual link or button the user can click to send the delete request to the server. We can easily add this in threads/show.blade.php. We’ll re arrange a few things to handle the layout properly, but the key markup is highlighted.


<div class="panel-heading">
    <div class="level">
        <span class="flex">
            <a href="{{ route('profile', $thread->creator) }}">{{ $thread->creator->name }}</a> posted:
            {{ $thread->title }}
        </span>
        <form action="{{$thread->path()}}" method="POST">
            {{ csrf_field() }}
            {{ method_field('DELETE') }}
            <button class="btn btn-link">Delete Thread</button>
        </form>
    </div>
</div>

There are a couple of things to note here. This is actually a form, which submits a post request to the endpoint of the thread in question. The styling of the button simply makes it look like a link in the browser. Since we are dealing with a form submission, we must include the call to csrf_field() to generate a Cross-site request forgery Token to protect the session. You may also notice the call to method_field(‘DELETE’). This is to inform Laravel that the intention is to actually delete something from the server.


Redirecting Upon Deletion

One last thing we want to set up is to redirect the user somewhere after deleting a thread. In this case we will redirect to the main threads page. Let’s update the destroy() method on the ThreadsController like so:


public function destroy($channel, Thread $thread)
{
    $thread->delete();

    if (request()->wantsJson()) {
        return response([], 204);
    }

    return redirect('/threads');
}

Getting Ready To Delete A Thread

delete model link example


Successful redirect after deleting the model

redirect after deleting the model


How To Delete A Record From The Database Summary

We hit all the topics we needed to cover in this tutorial with regard to setting up a user to delete a thread from the database. From creating the supporting tests right through to creating the link to send the delete request from the UI, we got it covered. Nice.

Click to share! ⬇️