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:
- Display tasks in the table - Treat this like a To-Do list of Google Keep.
- Add archive functionality - Move tasks to the Archive.
- Add archive preview - Show archived tasks in a separate view.
- Add bin functionality via soft deletes - Move tasks to the Trash Bin.
- Add bin preview - Show tasks in the Trash Bin.
- 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.