One of less-used functions in web-projects is ability for user to delete their account. In this article I will show three cases of how it can be done: block, hide and actually delete data.


Case 1. Block user from logging in.

Sometimes there’s a need to just restrict user’s access, but all their data should remain in the system, for history purposes.

So it’s not really a delete action, more like a block or a “ban”.

To achieve that, just add a field in users table, called blocked_at.

php artisan make:migration add_blocked_at_to_users_table

And then migration:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->timestamp('blocked_at')->nullable();
    });
}

Same logic as other Timestamps fields – it will either be NULL if user is active, or contain a time value of when it was blocked.

Since this block/ban action will probably be rare, you may not even build a button in admin panel to block a user, just set field value directly with SQL query, like this:

update users set blocked_at = NOW() where id = X;

Protip: before launching update or delete statements directly from SQL client, run select statement with the same WHERE clause, to make sure that you’re updating/deleting the correct entries.

I mean, run this first: select * from users where id = X;

Then look at the results and make sure it’s the same user. And only then run update statement.


Now, how to check if user is blocked? We should probably use a Middleware that would be attached to all routes along with “auth”, and if user is blocked, they would get redirected to login form with error message.

Step 1. Generate Middleware class.

php artisan make:middleware ActiveUser

Then, in generated app/Http/Middleware/ActiveUser.php:

class ActiveUser
{
    public function handle($request, Closure $next)
    {
        if (auth()->user()->blocked_at) {
            $user = auth()->user();
            auth()->logout();
            return redirect()->route('login')
                ->withError('Your account was blocked at ' . $user->blocked_at);
        }

        return $next($request);
    }
}

So, we force user’s logout and redirect to login form with error message.

Step 2. Register Middleware in Kernel

Then we need to register this Middleware in app/Http/Kernel.php – we will give it an alias name of active_user:

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    // ... other middlewares 

    'active_user' => \App\Http\Middleware\ActiveUser::class,
];

Step 3. Assign middleware to Routes

Here’s our routes/web.php – we will assign this route to any authenticated routes.

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

Auth::routes();

Route::group(['middleware' => ['auth', 'active_user']], function() {
    Route::get('/home', 'HomeController@index')->name('home');
    // ... Any other routes that are accessed only by non-blocked user
});

Step 4. Show error on login page

Finally, we’re redirecting with error message, remember ->withError(‘Your account was blocked at ‘ . $user->blocked_at); above. So we need to show it visually.

In resources/views/auth/login.blade.php we add this:

<div class="card-header">{{ __('Login') }}</div>

<div class="card-body">

    @if (session('error'))
        <div class="alert alert-danger">
            {{ session('error') }}
        </div>
    @endif

    <form method="POST" action="{{ route('login') }}">

Visual result:

Laravel login user block

In that case, user’s data stays in the system, other users/admins will see history of their actions.

Let’s move on with the second case, where history should become invisible.


Case 2. Hide user with soft-delete.

Another case where user wants to delete their account and hide all their history in system.

Main word is hide. Not fully delete. Cause maybe some data is important for reports like financial numbers, also they may change their mind and want to be recovered.

In that case, you should use Eloquent Soft Deletes and fill in users.deleted_at with timestamp value.

php artisan make:migration add_deleted_at_to_users_table

Then Migration code:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->softDeletes();
    });
}

Then, in app/User.php model:

use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
    use Notifiable, SoftDeletes;

Then, soft-delete a user by SQL query, same as previous example:

update users set deleted_at = NOW() where id = X;

Or, if you prefer to not connect to database, and use Artisan Tinker, you can do this:

User::find(1)->delete();

As in previous case, user won’t be able to login, but will just see default error message: “These credentials do not match our records.”.

Laravel User soft-delete

Now, question here is what do you do with other database data that has user_id or similar fields, related to users table?

You have a choice:

  1. Restrict user delete if they have related data
  2. Cascade and soft-delete related data
  3. Do nothing with and just make sure that whenever you want to show that related data, it doesn’t throw errors. Like, instead of {{ $project->user->name }} do {{ $project->user->name or ” }}

Read more about soft deleting relationships in this article: One-To-Many with Soft-Deletes. Deleting Parent: Restrict or Cascade?


Case 3. Actually delete user with all the data.

Now, sometimes you need to delete user’s data from legal point of view. They just want all their data gone forever, they have rights to demand that.

Before actually deleting, make sure that it wouldn’t affect all data already aggregated, like monthly reports or some important financial numbers. Double-check that you won’t have incorrect numbers after deleting user.

And then, to delete user with all related data, there are two methods:

Method 1. Delete cascade in migrations.

If you think about that case from very beginning of your project, you just set up foreign keys in migration file, with cascading delete.

Schema::create('posts', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->unsignedBigInteger('user_id');
    $table->foreign('user_id')->references('id')->on('users')
        ->onDelete('cascade');

If you don’t specify that onDelete() value, then default value is RESTRICT, which means that MySQL will prevent parent records to be deleted if there is a foreign key.

But if you do specify onDelete(‘cascade’), then it will delete all posts whenever User gets deleted, like User::find(1)->delete();.

Remember – it happens on database level, not in Laravel or Eloquent. So cascade delete will happen even if you run direct SQL query delete from users where id = 1;.

Method 2. Manually delete records via eloquent

If you don’t have cascade delete on database level, you need to delete all manually. So if you have for, example, UserController and destroy() method, list all related delete sentences one by one, from the deepest.

Let’s say, user has posts, and posts have comments. So you would do something like this:

public function destroy(User $user)
{
    $posts = Post::where('user_id', $user->id)->pluck('id');
    Comment::whereIn('post_id', $posts)->delete();
    Post::where('user_id', $user->id)->delete();
    $user->delete();
}

Now, be extremely careful with such chained delete sentences. Any one of them may fail, for other foreign key reasons.

Here, what I would suggest, is use database transactions. Cause what if some delete query would fail, and previous one won’t be recoverable anymore?

So here’s the actual code:

public function destroy(User $user)
{
    $posts = Post::where('user_id', $user->id)->pluck('id');
    \DB::transaction(function () use ($posts, $user) {
        \DB::table('comments')->whereIn('post_id', $posts)->delete();
        \DB::table('posts')->where('user_id', $user->id)->delete();
        \DB::table('users')->where('id', $user->id)->delete();
    });
}

So, there you go. Three ways to delete the user with their data. Which one is relevant in your project?