# File upload and image processing with Laravel

> A walkthrough of the end-to-end flow for file uploading, validation, and image resizing in Laravel.

- Published: 2016-08-07
- Category: Web Development
- Tags: Laravel
- Reading time: 4 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/file-upload-and-image-processing-with-laravel/
- Language: en-US
- Author: Muhammet Şafak

---
File uploading looks simple on the surface, but once you get into the details it forces you to answer several questions at once: Did the file actually upload? What should the size and type limits be? What happens if a file with the same name already exists? If the uploaded image is a profile photo, it needs to be resized to a standard dimension.

In this post I walk through how I handle all of that in Laravel, using a simple profile photo upload flow as the example.

## The form side

To upload a file, the HTML form needs to be defined with `enctype="multipart/form-data"`:

```html
<form action="/profil/fotograf" method="POST" enctype="multipart/form-data">
    {{ csrf_field() }}
    <input type="file" name="fotograf" accept="image/*">
    <button type="submit">Yükle</button>
</form>
```

Forgetting the `enctype` attribute is the most common mistake with this feature. The rest of the form appears to work fine, but `$request->file('fotograf')` always returns `null`. When debugging, checking this attribute should be the first step.

## Validation

The first thing to do in the controller is always validation. Laravel's `image` rule checks whether the file is genuinely an image; the `max` rule sets a size limit in kilobytes:

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfilController extends Controller
{
    public function fotografGuncelle(Request $request)
    {
        $this->validate($request, [
            'fotograf' => 'required|image|mimes:jpeg,png,gif|max:2048',
        ]);

        // From here we know the file is valid
        $dosya = $request->file('fotograf');

        // ...
    }
}
```

The `mimes:jpeg,png,gif` rule checks the MIME type — it does not just look at the extension, it also inspects the file's contents. `max:2048` enforces a 2 MB limit.

Here is why MIME type checking matters: a user could upload a file with a `.jpg` extension whose content is `<?php echo shell_exec($_GET['cmd']); ?>`. Checking only the extension would let it through. Laravel's `mimes` rule reads the first few bytes of the file to verify it is actually a JPEG. This check is essential for secure file handling.

## Saving the file

Saving with Laravel's filesystem API is clean and straightforward:

```php
// Generate a unique filename and save to the public disk
$dosyaAdi = uniqid() . '.' . $dosya->getClientOriginalExtension();
$yol = 'profil-fotograflari/' . $dosyaAdi;
Storage::disk('public')->put($yol, file_get_contents($dosya->getRealPath()));

// $yol: something like "profil-fotograflari/abc123.jpg"
```

The `Storage` facade's `put` method saves the file to the specified folder under the given name. The `public` disk is defined in `config/filesystems.php` and typically writes to `storage/app/public`.

Thanks to the symbolic link you create by hand from `public/storage` to `storage/app/public`, these files are accessible from the web.

For a stronger unique name than `uniqid()`, you can use `Str::random(40)`. `uniqid()` operates at microsecond precision, so collisions on concurrent uploads are unlikely but theoretically possible. For large-scale applications, a UUID or a random string is the safer choice.

## Resizing the image

For profile photos I usually want a standard size. I use the **Intervention Image** library for this:

```bash
composer require intervention/image
```

I resize the image before saving it:

```php
use Intervention\Image\ImageManagerStatic as Image;

public function fotografGuncelle(Request $request)
{
    $this->validate($request, [
        'fotograf' => 'required|image|mimes:jpeg,png,gif|max:2048',
    ]);

    $dosya    = $request->file('fotograf');
    $dosyaAdi = uniqid() . '.jpg';
    $hedefYol = storage_path('app/public/profil-fotograflari/' . $dosyaAdi);

    // Read the image, resize it, save as JPEG
    Image::make($dosya->getRealPath())
        ->fit(200, 200)      // 200x200 square, cropped from center
        ->save($hedefYol, 80); // JPEG at quality 80

    // Update the user record
    auth()->user()->update([
        'fotograf' => 'profil-fotograflari/' . $dosyaAdi,
    ]);

    return back()->with('basarili', 'Profil fotoğrafınız güncellendi.');
}
```

`fit(200, 200)` scales the image to 200x200 without distortion — ideal for square profile photos. `save($yol, 80)` saves it as a JPEG at quality 80, which is a reasonable trade-off between file size and visual quality.

There is one more reason to convert all uploaded images to JPEG: if a PNG with a transparent background is uploaded as a profile photo, it may be saved with a black background (JPEG does not support transparency). This edge case, if not caught early, tends to come back as user complaints about color mismatches. For images with transparency, you need to either save as PNG or fill the background with white before converting.

## Deleting the old photo

When updating, deleting the old file is good practice:

```php
use Illuminate\Support\Facades\Storage;

$eskiFotograf = auth()->user()->fotograf;

if ($eskiFotograf && Storage::disk('public')->exists($eskiFotograf)) {
    Storage::disk('public')->delete($eskiFotograf);
}

// Save the new photo...
```

Not deleting old files leads to storage bloat. If a user updates their photo every month, you end up with dozens of unnecessary files by end of year. It seems trivial at first, but as the user base grows, disk usage balloons at an unpredictable rate.

## Thinking about error cases

If [`validate()`](/en/blog/laravel-form-validation-in-practice/) fails, Laravel automatically redirects back to the form and populates the `$errors` variable with the error messages. If something goes wrong during image processing, Intervention Image throws an exception, which Laravel's global error handler catches.

In a production application it is better to catch these exceptions more carefully and display a meaningful error message to the user. But for a project still in development, this level of handling is sufficient for now.

One more gotcha: PHP's `upload_max_filesize` and `post_max_size` values in `php.ini` kick in before Laravel's `max` validation rule. You can tell Laravel `max:2048`, but if `php.ini` has `upload_max_filesize=1M`, a 1.5 MB file gets rejected before it ever reaches Laravel — and instead of a Laravel error message, you may see PHP's raw error page. Checking these values in your development environment makes it much easier to track down unexpected behaviour.

File uploading might seem like it does not need to be this involved, but skipping each of these steps creates problems down the line. Skipping validation opens the door to malicious uploads; skipping resizing leads to storage problems; not deleting old files leads to accumulation. Setting these steps up from the start is easier than fixing them later.
