In Laravel 7, I am have a task management app. I can upload tasks (posts if it were a blog) and images. I have a multiple image upload working as expected. When it comes time to delete a task, the task deletes just fine but the images are left in the database and in the disk which is public into a folder called task-images. Being new to Laravel, I am struggling on how to go about this. I tried to change the settings in the filesystem.php
(which I will post with the commented out code) but that didn’t change the location as I had expected. In the end, I want to be able to delete the multiple images when I delete a post and also click delete on an individual image and delete that from both db and disk. I am using resource controller for all my task routes. I have no idea how to go about this and the tutorials that I have found don’t really address my specific issue. Any help would be greatly appreciated. Thank you in advance.
Here is my task controller at TaskController.php
<?php namespace AppHttpControllers; use AppTask; use AppImage; use IlluminateHttpRequest; use IlluminateSupportFacadesAuth; use IlluminateSupportFacadesFile; use IlluminateSupportFacadesStorage; class TasksController extends Controller { public function index() { $tasks = Task::orderBy('created_at', 'desc')->paginate(10); return view('/tasks')->with('tasks', $tasks); } public function create() { return view('tasks.create'); } public function store(Request $request) { $this->validate($request, [ 'task_name' => 'required', 'task_description' => 'required', ]); // Create Task $user = Auth::user(); $task = new Task(); $data = $request->all(); $task->user_id = $user->id; $task = $user->task()->create($data); if ($request->hasFile('images')) { $files = $request->file('images'); foreach ($files as $file) { $name = time() . '-' . $file->getClientOriginalName(); $name = str_replace(' ', '-', $name); $file->move('task-images', $name); $task->image()->create(['name' => $name]); $images = new Image; $images->name = $name; } } $task->task_name = $request->input('task_name'); $task->task_description = $request->input('task_description'); $task->task_priority = $request->input('task_priority'); $task->task_assigned_by = $request->input('task_assigned_by'); $task->task_assigned_to = $request->input('task_assigned_to'); $task->task_to_be_completed_date = $request->input('task_to_be_completed_date'); $task->task_notes = $request->input('task_notes'); $task->task_status = $request->task_status; $task->save(); return redirect('/home')->with('success', 'Task Created'); } public function edit($id) { $task = Task::find($id); return view('tasks.edit', ['task' => $task]); } public function update(Request $request, $id) { $this->validate($request, [ 'task_name' => 'required', 'task_description' => 'required', ]); $task = Task::find($id); $task->task_name = $request->input('task_name'); $task->task_description = $request->input('task_description'); $task->task_priority = $request->input('task_priority'); $task->task_assigned_by = $request->input('task_assigned_by'); $task->task_assigned_to = $request->input('task_assigned_to'); $task->task_to_be_completed_date = $request->input('task_to_be_completed_date'); $task->task_notes = $request->input('task_notes'); $task->task_status = $request->input('task_status'); if ($request->hasFile('images')) { $files = $request->file('images'); foreach ($files as $file) { $name = time() . '-' . $file->getClientOriginalName(); $name = str_replace(' ', '-', $name); $file->move('task-images', $name); $task->image()->create(['name' => $name]); } } $task->update(); return redirect('/home')->with('success', 'Task Updated'); } public function show($id) { $task = Task::find($id); return view('tasks.show')->with('task', $task); } public function destroy($id) { $task = Task::findOrFail($id); // $image = '/task-images/' . $task->image; Storage::delete($task->image); $task->delete(); return redirect('home')->with('success', 'Task Deleted'); } }
filesystem.php (just the disks section)
'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), // 'root' => public_path('task-images'), ],
…
in my individual show template, show.blade.php
complete in case there is a code conflict.
@extends('layouts.master') @section('content') <div class="container"> <a href="/home" class="btn bg-purple mb-4">Go Back</a> <div class="card p-3"> <div class="row"> <div class="col-md-4 col-sm-12"> <h3>Task</h3> <p>{{ $task->task_name }}</p> <h3>Assigned On:</h3> <p>{{ $task->created_at->format('m/d/Y') }}</p> <h3>Assigned To:</h3> <p>{{ $task->task_assigned_to }}</p> </div> <div class="col-md-4 col-sm-12"> <h3>Task Description</h3> <p>{{ $task->task_description }}</p> <h3>Priority</h3> <p>{{ $task->task_priority }}</p> <h3>Status</h3> <p>{{ $task->task_status }}</p> </div> <div class="col-md-4 col-sm-12"> <h3>Test Environment Date:</h3> <p>{{ $task->task_to_be_completed_date }}</p> <h3>Notes</h3> <p>{{ $task->task_notes }}</p> <h3>Action</h3> <div style="display: inline;"> <a href="/tasks/{{$task->id}}/edit" class="btn btn-sm btn-primary mr-2"> <i class="fa fa-edit"></i> Edit </a> </div> <form style="display: inline;" action="/tasks/{{ $task->id }}" method="POST" class=""> @csrf @method('DELETE') <button type="submit" class="btn btn-danger btn-sm ml-1 mr-1"> <i class="fa fa-trash"></i> Delete </button> </form> </div> <div class="col-md-12"> <h5>Images</h5> <hr /> <div class="row"> @if($task->image->count()>0) @for($i=0; $i < count($images = $task->image()->get()); $i++) <div class="col-lg-4 col-md-6 col-sm-12"> <a href="#" class="thumbnail" data-toggle="modal" data-target="#lightbox"><img class="w-50 mb-2" src="/task-images/{{ $images[$i]['name'] }}" alt=""></a> <form style="display: inline;" action="/tasks/{{ $task->name }}" method="POST" class=""> @csrf @method('DELETE') <button type="submit" class="btn btn-danger btn-sm ml-1 mr-1"> <i class="fa fa-trash"></i> Delete </button> </form> </div> @endfor @else <p>No images found</p> @endif </div> <br /> </div> </div> </div> </div> <!--Modal Start--> <div id="lightbox" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> <div class="modal-dialog"> <button type="button" class="close hidden" data-dismiss="modal" aria-hidden="true">×</button> <div class="modal-content"> <div class="modal-body"> <img class="w-100" src="" alt="" /> </div> </div> </div> </div> <!--Modal End--> @endsection @section('scripts') <script> $(document).ready(function() { var $lightbox = $('#lightbox'); $('[data-target="#lightbox"]').on('click', function(event) { var $img = $(this).find('img'), src = $img.attr('src'), alt = $img.attr('alt'), css = { 'maxWidth': $(window).width() - 100, 'maxHeight': $(window).height() - 100 }; $lightbox.find('.close').addClass('hidden'); $lightbox.find('img').attr('src', src); $lightbox.find('img').attr('alt', alt); $lightbox.find('img').css(css); }); $lightbox.on('shown.bs.modal', function (e) { var $img = $lightbox.find('img'); $lightbox.find('.modal-dialog').css({'width': $img.width()}); $lightbox.find('.close').removeClass('hidden'); }); }); </script> @endsection
In my Task model, Task.php
, I have:
<?php namespace App; use IlluminateDatabaseEloquentModel; use AppImage; class Task extends Model { protected $fillable = [ 'task_name', 'task_priority', 'task_assigned_to', 'task_assigned_by', 'task_description', 'task_to_be_completed_date', 'task_status', 'task_notes' ]; public function user() { return $this->belongsTo(User::class); } public function image() { // return $this->hasMany('AppImage'); return $this->hasMany(Image::class); } }
and finally my Image Model Image.php
<?php namespace App; use IlluminateDatabaseEloquentModel; use AppTask; class Image extends Model { protected $fillable = [ 'task_id', 'name', ]; protected $uploads = '/task-images/'; public function getFileAttribute($image) { return $this->uploads . $image; } public function task() { // return $this->belongsTo('AppTask', 'task_id'); return $this->belongsTo(Task::class); } }
If I am missing something, please let me know so I can edit my question. Again, thank you in advance for helping me with this issue. I have been scratching my head all week on this one. Cheers.
Edit After implementing boot functions in my model as suggested below, I received an error that an invalid argument was used for foreach. I ran a dd($task); and the following image shows the result.
Final Edit
The answer below worked for my situation. I did have to edit some things to finalize the resolution:
in Task.php
I changed the foreach to the following.
foreach($task->image ?: [] as $image)
I had declared image and not image in my model and that was causing a problem. Adding the ternary operator also helped the code not throw any errors.
In my TasksController.php I changed both the update and create functions with the same ternary operator as follows:
if ($request->hasFile('images')) { $files = $request->file('images'); foreach ($files ?: [] as $file) { $name = time() . '-' . $file->getClientOriginalName(); $name = str_replace(' ', '-', $name); $file->move('task-images', $name); $task->image()->create(['name' => $name]); } }
I hope this helps anyone else having the same issue. Thanks to @GrumpyCrouton and @lagbox for their help in resolving this as well as @user3563950 Without them, I would still by stratching my head for another couple of weeks.
Advertisement
Answer
on your AppImage
class, implement to boot function with the following;
use IlluminateSupportFacadesStorage; public static function boot() { parent::boot(); self::deleting(function($image) { Storage::delete(Storage::path($image->name)); }); }
Also implement the boot method in AppTask
class
use IlluminateSupportFacadesStorage; public static function boot() { parent::boot(); self::deleting(function($task) { foreach($task->images as $image) { $image->delete(); } }); }
Now on your TaskController
implement the destroy
method as follows;
public function destroy($id) { $task = Task::findOrFail($id); $task->delete(); return redirect('home')->with('success', 'Task Deleted'); }
As a bonus, learn Laravel model binding to ease the pain of finding an instance using findOrFail()