DanielRønfeldt's avatar

Mission Impossible ? [Laravel 6 + InterventionImage + validation]

Hey everyone, this is my first post on Laracasts so bear with me :)

I'm trying to figure out how to safely validate a file (image) upload during the creation of a new model instance. I need to make use of the InterventionImage library, so that the image is manipulated (cropped) when the new model instance is created.

Here's my model:

namespace App;

use Illuminate\Database\Eloquent\Model;

class TestProfile extends Model
{

  protected $fillable = [
    'name',
    'main_picture',
  ];
	
}

And this is my controller:


namespace App\Http\Controllers;

use App\TestProfile;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
use Intervention\Image\Facades\Image;

class TestProfileController extends Controller
{
  
  /**
   * Display a listing of the TestProfiles.
   *
   * @return View
   */
  public function index()
  {
    $testprofiles = TestProfile::latest()->get();
    
    return view('testprofiles.index', ['testprofiles' => $testprofiles]);
  }
  
  
  /**
   * Display the specified TestProfile.
   *
   * @param TestProfile $testprofile
   *
   * @return View
   */
  public function show(TestProfile $testprofile)
  {
    return view('testprofiles.show', compact('testprofile'));
  }
  
  
  /**
   * Show the form for creating a new TestProfile.
   *
   * @return View
   */
  public function create()
  {
    return view('testprofiles.create');
  }
  
  
  /**
   * Store a newly created TestProfile in storage.
   *
   * @param Request $request
   *
   * @return Application|RedirectResponse|Redirector
   */
  public function store(Request $request)
  {
    TestProfile::create( $this->validateTestProfile() );
    
    return redirect( route('testprofiles.index') );
  }
  
  
  /**
   * Show the form for editing the specified TestProfile.
   *
   * @param TestProfile $testprofile
   *
   * @return View
   */
  public function edit(TestProfile $testprofile)
  {
    return view( 'testprofiles.edit', compact('testprofile') );
  }
  
  
  /**
   * Update the specified TestProfile in storage.
   *
   * @param Request         $request
   * @param TestProfile $testprofile
   *
   * @return Application|RedirectResponse|Redirector
   */
  public function update(Request $request, TestProfile $testprofile)
  {
    $testprofile->update( $this->validateTestProfile() );
    
    return redirect( route('testprofiles.show', $testprofile) );
  }
  
  
  /**
   * Remove the specified TestProfile from storage.
   *
   * @param TestProfile  $testprofile
   *
   * @return Response
   */
  public function destroy(TestProfile $testprofile)
  {
    //
  }
  
  
  /*   ---------------------
    UTILITIES
  ---------------------- */
  /**
   * Validates the input data. Useful for creating as well as updating a TestProfile.
   *
   * @return array
   */
  protected function validateTestProfile() {
    
    if( request()->hasFile('main_picture') ) {
      
      $extension = request()->file('main_picture')->getClientOriginalExtension(); // get file extension
      $filename_to_store = time() . '_' . Str::random(40) . '.' . $extension; // filename to store
      
      // "Make" the resized image
      $resized = Image::make( request()->file('main_picture') )
                      ->resize(300, 300, function( $constraint ) {
                        $constraint->aspectRatio(); // preserve image's aspect ratio
                      });
      
      $save_resized = $resized->save('uploads/testprofiles/main_picture/' . $filename_to_store);
      
      $filename_resized = $save_resized->basename;
      
      
      $request['main_picture'] = $filename_resized;
    }
    
    return request()->validate([
      'name' => 'required',
      'main_picture' => 'required|image|mimes:jpeg,jpg,png,gif|max:1024',
    ]);
  }
}


With my current setup, I'm getting an Unsupported Image Type exception from InterventionImage if I chose anything else other than an image (for example a .PDF file). However, if I chose an actual image file, the store() method within the controller is successfully executed, and the TestProfile instance gets created. My understanding is, that the validation doesn't work.

The second problem that I'm having with this approach, is that the main_image field of the TestProfile model instance in the database gets saved as a temporary system path, for example /private/var/tmp/phpPGy8PH.

I've been trying to figure this one out for a couple days now, and it's getting really frustrating. If anyone could help me out, I'd highly appreciate it.

0 likes
9 replies
wingly's avatar

Why are you trying to validate after you process the request ?

DanielRønfeldt's avatar

Agreed, that was totally wrong :)

But now that I moved the conditional out of the validateTestProfile() function, into the store() method of the controller, right before TestProfile::create( $this->validateTestProfile() );, nothing seems to have changed. The validation still doesn't seem to kick in.

DanielRønfeldt's avatar

Okay, getting there :)

I created an additional validator function, just for the image, so that the conditional will return false in case the image is the wrong format/filesize. Here's the updated store() method:

/**
   * Store a newly created TestProfile in storage.
   *
   * @param Request $request
   *
   * @return Application|RedirectResponse|Redirector
   */
  public function store(Request $request)
  {
  if( request()->hasFile('main_picture') ) {
      
      $this->validateMainImage(); // will return false if the image is not the correct format/filesize
      
      $extension = request()->file('main_picture')->getClientOriginalExtension(); // get file extension
      $filename_to_store = time() . '_' . Str::random(40) . '.' . $extension; // filename to store
      
      // "Make" the resized image
      $resized = Image::make( request()->file('main_picture') )
                      ->resize(300, 300, function( $constraint ) {
                        $constraint->aspectRatio(); // preserve image's aspect ratio
                      });
      
      $save_resized = $resized->save('uploads/testprofiles/main_picture/' . $filename_to_store);
      
      $filename_resized = $save_resized->basename;
      
      
      $request['main_picture'] = $filename_resized;
    }
  
    TestProfile::create( $this->validateTestProfile() );
    
    return redirect( route('testprofiles.index') );
  }

  /* ... 
      ...
     ... */

protected function validateMainImage() {
    return request()->validate([
      'main_picture' => 'required|image|mimes:jpeg,jpg,png,gif|max:1024',
    ]);
  }

Now, the only thing that's left, is saving the proper filename of the image in the database, as it's still getting saved as a temporary system path, e.g. /private/var/tmp/phpPGy8PH

wingly's avatar
wingly
Best Answer
Level 29

May i suggest to simplify it a bit ? Can you try something like that for example warning not tested

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'main_picture' => 'required|image|mimes:jpeg,jpg,png,gif',
        ]);

        $extension = request()->file('main_picture')->getClientOriginalExtension();
        $filename_to_store = time() . '_' . Str::random(40) . '.' . $extension; 

        $resized = Image::make( request()->file('main_picture') )
            ->resize(300, 300, function( $constraint ) {
                $constraint->aspectRatio();
            })->stream();        
        
        Storage::disk('local')->put($filename_to_store, (string) $resized);

        TestProfile::create([
            'name' => $request->input('name'),
            'main_picture' => $filename_to_store
        ]);

        return redirect( route('testprofiles.index') );
    }
1 like
DanielRønfeldt's avatar

@wingly Sir, you are a legend. Got it working on the first try (not a blind copy/paste of your code, I promise 😀 )

I was just wondering though, what's the purpose of using Storage::disk('local')when saving the processed image? How could I then get ahold of the newly-created image in a .blade file? I've already created a symlink with php artisan storage:link, can I just use the asset() helper method for that purpose?

DanielRønfeldt's avatar

That does help indeed. Thank you for that. But what if I'm storing the file under a much deeper folder structure, for instance

Storage::disk('local')->put('uploads/testprofiles/main-pictures/' . $filename_to_store, (string) $resized );

I'd then need to append the same uploads/testprofiles/main-pictures/path each and every time I'd need to get the URI of the stored images in my view files, right?

In other words,

<img src="{{ Storage::url('uploads/testprofiles/main-pictures/') . $testprofile->main_picture }}" alt="...">

Unless there's a better way of writing the path, either in the controller, or in the views, it seems to me a bit un-optimized way of using them.

Sorry for keeping on asking noob questions... 🤓

wingly's avatar

Then you simply store in your database the uploads/testprofiles/main-pictures/filename

DanielRønfeldt's avatar

I must be really tired. It's such a simple approach, yet it never crossed my mind. Once again, thanks so much for your help @wingly. No more noob questions from me, at least not today 😊

Have a great day (evening/night) my friend. Stay safe 🙏

1 like

Please or to participate in this conversation.