Skip to content
Advertisement

Laravel 7 Delete array or single image from db and disk – Deletes post but not associated images from db nor disk

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. dd of $task on delete

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()

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement