Laravel Projects Examples

Laravel Soft-Deletes "On Steroids": Archive and Bin

This Laravel demo project is based on functionality of Google Keep:

  • It allows to Archive records
  • Or put them to Bin, which is auto-emptied after 7 days (or you can empty the bin manually)
  • From both Archive and Bin, you may restore the record

The idea is based on this YouTube video (Google Keep example starts at 6:16 min).

We tried to re-create the same scenario for a Tasks CRUD with Laravel.

For this example to work, we have to handle a few things:

  1. Display tasks in the table - Treat this like a To-Do list of Google Keep.
  2. Add archive functionality - Move tasks to the Archive.
  3. Add archive preview - Show archived tasks in a separate view.
  4. Add bin functionality via soft deletes - Move tasks to the Trash Bin.
  5. Add bin preview - Show tasks in the Trash Bin.
  6. Add Model Pruning - Delete tasks from the Trash Bin after a certain period.

Let's start by looking at our Task model:

Here, we need to add the SoftDeletes and Prable traits to the model and define scopes for prunable tasks and active/archived tasks.

app/Models/Task.php

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Prunable;
 
class Task extends Model
{
/** @use HasFactory<\Database\Factories\TaskFactory> */
use HasFactory;
use HasFactory, SoftDeletes, Prunable;
 
protected $fillable = ['name', 'description', 'user_id', 'archived_at'];
 
protected $dates = ['archived_at'];
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
 
public function prunable()
{
return static::onlyTrashed()
->where('deleted_at', '<=', now()->subDays(7));
}
 
public function scopeActive($query)
{
return $query->whereNull('archived_at');
}
 
public function scopeArchived($query)
{
return $query->whereNotNull('archived_at')
->whereNull('deleted_at');
}
}

In our Controller, we scope the tasks based on their status by adding ->active() to our Query:

app/Http/Controllers/TaskController.php

public function index(): View
{
$tasks = Task::with('user')->active()->paginate(10);
 
return view('tasks.index', compact('tasks'));
}

Next, we implemented two other controllers for the Archive and Bin views:

app/Http/Controllers/TaskArchiveController.php

class TaskArchiveController extends Controller
{
public function index()
{
$tasks = Task::archived()
->paginate(10);
 
return view('tasks.archive', compact('tasks'));
}
 
public function restore(int $id): RedirectResponse
{
$task = Task::archived()
->findOrFail($id);
 
$task->update(['archived_at' => null]);
 
return redirect()->route('archive.index')
->with('message', 'Task restored from archive successfully.');
}
 
public function moveToBin(int $id): RedirectResponse
{
$task = Task::archived()
->findOrFail($id);
 
$task->delete();
 
return redirect()->route('archive.index')
->with('message', 'Task moved to bin successfully.');
}
}

app/Http/Controllers/TaskBinController.php

class TaskBinController extends Controller
{
public function index()
{
$tasks = Task::onlyTrashed()->paginate(10);
 
return view('tasks.bin', compact('tasks'));
}
 
public function restore(int $id): RedirectResponse
{
Task::withTrashed()->find($id)->restore();
 
return redirect()->route('bin.index')
->with('message', 'Task restored successfully.');
}
 
public function forceDelete(int $id): RedirectResponse
{
Task::withTrashed()->find($id)->forceDelete();
 
return redirect()->route('bin.index')
->with('message', 'Task permanently deleted.');
}
 
public function emptyBin(): RedirectResponse
{
Task::onlyTrashed()->forceDelete();
 
return redirect()->route('bin.index')
->with('message', 'Bin emptied successfully.');
}
}

Of course, we can't forget about the routes:

Note: We are using Route prefixes to group the routes for the Archive and Bin views.

routes/web.php

// ...
 
Route::prefix('tasks')->group(function () {
Route::get('/archive', [TaskArchiveController::class, 'index'])->name('archive.index');
Route::post('/archive/{task}/restore', [TaskArchiveController::class, 'restore'])->name('archive.restore');
Route::delete('/archive/{task}', [TaskArchiveController::class, 'moveToBin'])->name('archive.bin');
Route::post('/{task}/archive', [TaskController::class, 'archive'])->name('tasks.archive');
 
Route::get('/bin', [TaskBinController::class, 'index'])->name('bin.index');
Route::post('/bin/{task}/restore', [TaskBinController::class, 'restore'])->name('bin.restore');
Route::delete('/bin/{task}', [TaskBinController::class, 'forceDelete'])->name('bin.force-delete');
Route::post('/bin/empty', [TaskBinController::class, 'emptyBin'])->name('bin.empty');
 
});
 
Route::resource('tasks', TaskController::class);

Once this is done, we recommend you to look at the views:

  • resources/views/tasks/index.blade.php
  • resources/views/tasks/archive.blade.php
  • resources/views/tasks/bin.blade.php

And see how the tasks are displayed and how the buttons are implemented.

Last, we have created tests for the controllers:

  • tests/Feature/TaskArchivingTest.php
  • tests/Feature/TaskDeletingTest.php
  • tests/Feature/TasksCRUDTest.php

That's it! This was a simple example of implementing an Archive and Bin feature in Laravel.