How to Restore Deleted Data using Soft-Delete in Laravel

Laravel Soft Delete is a very useful option from Laravel Eloquent if you want to use it as a recycle bin before permanently deleting data. In case some data gets deleted accidentally, Laravel has the option to restore the deleted data. So, you can use this feature as trash data. You can also permanently delete the data using the Laravel force delete option.

To use soft delete feature in your Laravel application:

  1. While creating the migration use $table->softDeletes(); for the table you want to use Soft-Delete. This will add "deleted_at" column in the table.
  2. In the model add use Illuminate\Database\Eloquent\SoftDeletes; and add use SoftDeletes; trait.
  3. In the controller use delete() method on the selected row to move data to trash. It actually updates the "deleted_at" column with the timestamp.
  4. To Restore - Use restore() method in the controller to restore deleted data.
  5. To Permanently Delete - use forceDelete() method in the controller for the selected rows from Trashed data.

Please read the entire topic to implement all these. We will develop a Laravel application to show how Soft Delete works. We will have a list of training courses with the option to delete each course. After deleting you can view the trashed data with the option to restore it or delete it permanently.

Step 1 - Create a Laravel project using composer

We will create a Laravel project using the below command. You need to have the composer and PHP installed in your computer. We are using the project name as lara_soft_delete.


composer create-project --prefer-dist laravel/laravel lara_soft_delete

This command will create lara_soft_delete folder and install all necessary files in it. So, our project root folder is "lara_soft_delete".

Step 2 - Create Laravel migration

We will create a table named 'training_courses' using Laravel migration. This table will store the details of all courses. Go to your project root folder and run the below command from VS code terminal. It will create the model, controller and migration.


php artisan make:model TrainingCourse -mc

The option "mc" will create the model (TrainingCourse.php), controller (TrainingCourseController.php) and the migration file (create_training_courses_table.php with a datetime prefix).

Create a database named "lara_demo" for this project using phpMyAdmin and update the .env file for database details as below:


DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lara_demo
DB_USERNAME=root
DB_PASSWORD=

Migration for the custom table

We will create the migration file for a new table named 'training_courses' and will use $table->softDeletes(); in it.


public function up()
{
  Schema::create('training_courses', function (Blueprint $table) {
      $table->id();
      $table->string('course_title');
      $table->string('course_descr');
      $table->string('level');
      $table->string('duration');
      $table->softDeletes(); // for soft delete
      $table->timestamps();
  });
}  

So, our table will have course_title, course_descr, level (basic, Intermediate etc.) and duration columns along with the column "deleted_at" for soft delete.

We will now run the migration to create the default Laravel tables and our custom table. Run the below artisan command from the project root to create the tables.


php artisan migrate

This will create all the default Laravel tables as well as our custom 'training_courses' table. After migration see the structure of the table.

Laravel soft delete table structure

Now let us update the model for soft delete. Use SoftDelete Trait here. Below is the model:

app/Models/TrainingCourse.php


  namespace App\Models;

  use Illuminate\Database\Eloquent\Factories\HasFactory;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Database\Eloquent\SoftDeletes;
  
  class TrainingCourse extends Model
  {
      use HasFactory;
      use SoftDeletes;
  }

Create Laravel Database Seeder

We will create some dummy data for the courses. Run the below command to create a seeder file:


php artisan make:seeder TrainingCourseSeeder

Update seeder file as below to insert 3 rows in training_courses table:


namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class TrainingCourseSeeder extends Seeder
{
    /**
      * Run the database seeds.
      *
      * @return  void
      */
    public function run()
    {
        DB::table('training_courses')->insert([
            'course_title'  => 'Core PHP',
            'course_descr'  => 'CodeIgniter',
            'level'         => 'Intermediate',
            'duration'      => '2 weeks'
            ]);
        DB::table('training_courses')->insert([
            'course_title'  => 'JavaScript',
            'course_descr'  => 'Basic JavaScript',
            'level'         => 'Basic',
            'duration'      => '1 week'
            ]);

        DB::table('training_courses')->insert([
            'course_title'  => 'Core PHP',
            'course_descr'  => 'Training for Core PHP',
            'level'         => 'Basic',
            'duration'      => '1 week'
            ]);
    }
}

Update run() method of the database seeder file (database/seeders/DatabaseSeeder.php) as below to call our seeder file:


public function run()
  {
      $this->call(TrainingCourseSeeder::class);
  }

Now run the below command to create the rows:


php artisan db:seed

After running this command, you will see the three rows inserted in the database table.

Step 3 - Controller code

Controller will have few methods for each of the actions performed on the course.

  1. index() method to display all the courses.
  2. moveToTrash() to move data to trash, i.e., soft delete.
  3. showTrash() method to display all trashed data. These data can be restored or deleted permanently.
  4. restore() method to restore the deleted row.
  5. forceDelete() method to delete data from trash permanently.

Let us see the code for the controller

app/Http/Controllers/TrainingCourseController.php


  namespace App\Http\Controllers;

  use App\Models\TrainingCourse;
  use Illuminate\Http\Request;
  use Illuminate\Support\Facades\Session;
  
  class TrainingCourseController extends Controller
  {
      /**
       * Display a listing of the resource.
       *
       * @return  \Illuminate\Http\Response
       */
      public function index()
      {
          $courses = TrainingCourse::all();
          return view('view_courses', compact('courses'));
      }
  
      public function moveToTrash(Request $request)
      {
          $id = $request->delete;
          TrainingCourse::findOrFail($id)->delete();
  
          Session::flash('message', 'Course Moved to Trash');
          return redirect()->route('course.index');
      }
  
      public function showTrash()
      {
          $trash_course = TrainingCourse::onlyTrashed()->get();
  
          return view('trashed_courses', compact('trash_course'));
      }
  
      public function restore(Request $request)
      {
          $id = $request->restore;
          $data = TrainingCourse::withTrashed()->find($id);
          if (!is_null($data)) {
              $data->restore();
              Session::flash('message', 'Course restored');
          }
  
          return redirect()->route('show.trashed');
      }
  
      public function forceDelete(request $request)
      {
          $id = $request->delete;
  
          $data = TrainingCourse::withTrashed()->find($id);
          if (!is_null($data)){
              $data->forceDelete();
              Session::flash('message', 'Course deleted');
          }
          return redirect()->route('show.trashed');
      }
  }

Step 4 - Laravel blade views

We will create two views, one for listing all courses and the other for viewing all deleted (trashed) data. Along with them, we will have layouts and a couple of modals for user confirmations before deleting.

We have the below files in the resources/views folder

Laravel Soft delete views

header, footer and master are for the layouts. view_courses.blade.php is for the listing of all courses, trashed_courses.blade.php is the list of all trashed(soft deleted) courses.

resources/views/layouts/header.blade.php


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Laravel Soft Delete</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
    <meta name="_token" content="kWdRyK9nJrdFeLtxMaBddU5wVssSkVf4BHoO3wV8">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" media="screen" href="https://codehow2.com/assets/css/style.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>

resources/views/layouts/footer.blade.php


<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/js/bootstrap.min.js"></script>

resources/views/layouts/master.blade.php


@include('layouts.header')
<body>
@yield('main-content')
@include('layouts.footer')
@stack('js')
</body>
</html>

resources/views/view_courses.blade.php


@extends('layouts.master')
@section('main-content')
    <h1>Laravel Soft Delete</h1>
    <div class="container">
        <div class="text-end">
            <a href="{{ route('show.trashed') }}" class="btn btn-primary">View Trashed Data</a>
        </div>

        <h4>List of Courses</h4>
        <div class="table-responsive">
            <table class="table table-striped table-bordered mt-5">
                @if (session('message'))
                    <div class="alert alert-success">
                        {{ session('message') }}
                    </div>
                @endif
                <thead>
                    <tr>
                        <th>#</th>
                        <th>Course Name</th>
                        <th>Description</th>
                        <th>Level</th>
                        <th>Duration</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody>
                    @forelse($courses as $index => $row)
                        <tr>
                            <td>{{ $index + 1 }}</td>
                            <td>{{ $row->course_title }}</td>
                            <td>{{ $row->course_descr }}</td>
                            <td>{{ $row->level }}</td>
                            <td>{{ $row->duration }}</td>
                            <td>
                                <button class="fa fa-trash" onClick="trashConfirm('{{ $row->id }}')" data-toggle="modal"
                                    data-target="#deleteConfirm" title="Move to Trash"></button>
                            </td>
                        </tr>
                    @empty
                        <tr>
                            <td colspan="6">No Courses Found</td>
                        </tr>
                    @endforelse
                </tbody>
            </table>
        </div>
@include ('modals.modal_trash')
@endsection
@push('js')
    <script>
        function trashConfirm(id) {
            document.getElementById('delete_id').value = id;
            $("#modalTrash").modal('show');
        }
    </script>
@endpush

In the above code, we have a simple list of all courses with a delete button (an icon for trash) against each course. A modal for confirmation will open when it is clicked. Also, a JavaScript function is called just to write the id of course in a form in the modal for confirmation. We will see it in the modal.

resources/views/modals/modal_trash.blade.php


  <!-- Modal -->
  <div class="modal fade" id="modalTrash" tabindex="-1" role="dialog" aria-labelledby="modalTrashTitle" aria-hidden="true">
      <div class="modal-dialog modal-dialog-centered" role="document">
          <div class="modal-content">
              <form action="{{ route('soft.delete') }}" method="post">
                  {{ csrf_field() }}
                  <input type="hidden" name="delete" id="delete_id">
                  <div>
                      <center>
                          <h1>!</h1>
                      </center>
                  </div>
  
                  <div class="modal-body">
                      <center>
                          <h1>Are You Sure?</h1>
                          <h6>You want to Move Course to Trash!</h6>
                      </center>
                  </div>
                  <div class="row" style="margin-bottom: 50px; text-align: center;">
                      <div class="col-sm-3"></div>
                      <div class="col-sm-3">
                          <button type="button" class="btn btn-danger btn-cancel" data-bs-dismiss="modal">Cancel</button>
                      </div>
                      <div class="col-sm-3">
                          <button type="submit" class="btn btn-success btn-submit">Move to Trash</button>
                      </div>
                      <div class="col-sm-3"></div>
                  </div>
              </form>
          </div>
      </div>
  </div>
    

Below is the view for trashed data.

resources/views/trashed_courses.blade.php


  @extends('layouts.master')
  @section('main-content')
  <h1>Laravel Soft Delete</h1>
  <div class="container">
      <div class="text-end">
          <a href="{{ route('course.index') }}" class="btn btn-primary">Back to Courses</a>
      </div>
  
      @if (session('message'))
          <div class="alert alert-success">
              {{ session('message') }}
          </div>
      @endif
      <h4>Courses Trashed Data</h4>
      <div class="table-responsive">
          <table class="table table-striped table-bordered mt-5">
              <thead>
                  <tr>
                      <th>#</th>
                      <th>Course Name</th>
                      <th>Description</th>
                      <th>Level</th>
                      <th>Duration</th>
                      <th>Action</th>
                  </tr>
              </thead>
              <tbody>
                  @forelse($trash_course as $index => $row)
                      <tr>
                          <td>{{ $index + 1 }}</td>
                          <td>{{ $row->course_title }}</td>
                          <td>{{ $row->course_descr }}</td>
                          <td>{{ $row->level }}</td>
                          <td>{{ $row->duration }}</td>
  
                          <td>
                              <button class="btn-restore" onClick="restoreFunction('{{ $row->id }}')"
                                  data-toggle="modal" data-target="#restoreModal">Restore</button>
                              <button class="btn-delete" onClick="delConfirm('{{ $row->id }}')"
                                  data-toggle="modal" data-target="#deleteModal">Permanent Delete</i></button>
                          </td>
                      </tr>
                  @empty
                      <tr>
                          <td colspan="6">No data Found in Trash</td>
                      </tr>
                  @endforelse
              </tbody>
          </table>
      </div>
  </div>
  
  @include('modals.modal_restore_delete')
  @endsection
  @push('js')
      <script>
          function delConfirm(id) {
              document.getElementById('delete_id').value = id;
              $("#deleteModal").modal('show');
          }
  
          function restoreFunction(id) {
              document.getElementById('restore_id').value = id;
              $("#restoreModal").modal('show');
          }
      </script>
  @endpush

This will list all deleted or trashed data. Against each row, there are two buttons; one for restoring and the other for permanent deletion. There will be a confirmation modal for each of the actions.

resources/views/modals/modal_restore_delete.blade.php


  <!-- Modal for Permanent Delete Confirmation-->
  <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalTitle" aria-hidden="true">
      <div class="modal-dialog modal-dialog-centered" role="document">
          <div class="modal-content">
              <form action="{{ route('permanent.delete') }}" method="post">
                  {{ csrf_field() }}
                  <input type="hidden" name="delete" id="delete_id">
                  <div>
                      <center>
                          <h1>!</h1>
                      </center>
                  </div>
  
                  <div class="modal-body">
                      <center>
                          <h1>Are You Sure?</h1>
                          <h6>You want to Permanently delete the Course? It can not be Restored!!</h6>
                      </center>
                  </div>
                  <div class="row" style="margin-bottom: 50px; text-align: center;">
                      <div class="col-sm-3"></div>
                      <div class="col-sm-3">
                          <button type="button" class="btn btn-danger btn-cancel" data-bs-dismiss="modal">Cancel</button>
                      </div>
                      <div class="col-sm-3">
                          <button type="submit" class="btn btn-success btn-submit">Delete</button>
                      </div>
                      <div class="col-sm-3"></div>
                  </div>
              </form>
          </div>
      </div>
  </div>
  
  <!-- Restore Modal  -->
  <div class="modal fade" id="restoreModal" tabindex="-1" role="dialog" aria-labelledby="restoreModalTitle"
      aria-hidden="true">
      <div class="modal-dialog modal-dialog-centered" role="document">
          <div class="modal-content">
              <form action="{{ route('course.restore') }}" method="post">
                  {{ csrf_field() }}
                  <input type="hidden" name="restore" id="restore_id">
                  <div class="swal-icon swal-icon--warning">
                      <center>
                          <h1>!</h1>
                      </center>
                  </div>
  
                  <div class="modal-body">
                      <center>
                          <h1>Are You Sure</h1>
                          <h6>Are You Sure You Want to Restore Selected Course?</h6>
                      </center>
                  </div>
                  <div class="row" style="margin-bottom: 50px; text-align: center;">
                      <div class="col-sm-3"></div>
                      <div class="col-sm-3">
                          <button type="button" class="btn btn-danger btn-cancel" data-bs-dismiss="modal">Cancel</button>
                      </div>
                      <div class="col-sm-3">
                          <button type="submit" class="btn btn-success btn-submit">Yes Restore It</button>
                      </div>
                      <div class="col-sm-3"></div>
                  </div>
              </form>
          </div>
      </div>
  </div>
  

Routes


Route::get('/', [TrainingCourseController::class, 'index'])->name('course.index');
Route::group(['prefix' => 'course'], function () {
    Route::post('move-trash', [TrainingCourseController::class, 'moveToTrash'])->name('soft.delete');
    Route::get('trashed', [TrainingCourseController::class, 'showTrash'])->name('show.trashed');
    Route::post('restore', [TrainingCourseController::class, 'restore'])->name('course.restore');
    Route::post('delete-trash', [TrainingCourseController::class, 'forceDelete'])->name('permanent.delete');
}); 

Below are the styles:

public/css/style.css


* {box-sizing: border-box;
}
body {
  margin: 0;
  font-family: Helvetica, sans-serif;
  font-size:14px;
}
h1,h4{
  text-align: center;
  margin-bottom: 20px;
  margin-top: 10px;
}

.container{
    min-height: 600px;;
}

.modal-content{
color:#fff;
background: #32243c;
}
table{
  width: 70%!important;
  margin: auto;
}
table>thead{
  background-color:#2b5171;
  color:#fff;
}

button{
  border: none;
}
.btn-cancel{
padding:10px; 
margin-left: -10px; 
width:120px;
}
.btn-submit{
  padding:10px; 
  margin-left: -10px;
  width:140px;
}
.btn-restore{
  background: #1586e9;
    color: #fff;
    border-radius: 6px;
    padding: 6px;
    font-size: 14px;
}
.btn-delete {
  background: #de0808;
    color: #fff;
    border-radius: 6px;
    padding: 6px;
    font-size: 14px;
}
.fa-trash{
  color:#ef3b3b;
}
.alert-success{
  text-align: center;
}

Step 5 - Test the application

From the project root, start the php development server:


php artisan serve 

From browser run localhost:8000. Check the Laravel soft delete, restore and permanent delete functions against the courses.

Download Source Code from github.

Watch YouTube Video