File uploads are one of the vital pieces in most web projects, and Laravel has awesome functionality for that, but information is pretty fragmented, especially for specific cases. I decided to gather it all under one big articles, discussing the most painful tasks related to file uploads. Enjoy!

Here are the topics we will discuss in the article:

  1. Simple “local” file upload
  2. File validation process
  3. Uploading to external disks: Amazon S3 example
  4. AJAX upload and multiple files at once
  5. Image manipulation: crop, resize etc.

1. Local file upload

Let’s start from the basics – how does Laravel deal with it by default? It’s pretty easy.

So, we have a simple form:

<form action="{{ route('books.store') }}" method="POST" enctype="multipart/form-data">
    {{ csrf_field() }}
    Book title:
    <br />
    <input type="text" name="title" />
    <br /><br />
    Logo:
    <br />
    <input type="file" name="logo" />
    <br /><br />
    <input type="submit" value=" Save " />
</form>

And this is a simple code to upload that logo.

public function store(Request $request)
{
    $request->logo->store('logos');
}

After this sentence, file is actually stored in the folder storage/app/logos:

You can specify the folder where the file should be uploaded – see parameter ‘logos’.

As you can see, Laravel generated a random filename, but you can easily override it – use function storeAs():

$request->logo->storeAs('logos', '1.png');

Or, you can keep the original filename:

$request->logo->storeAs('logos', $request->logo->getClientOriginalName());

Storage folder: files are not public?

We saw that, by default, Laravel stores the file in /storage/app folder. Why?

It’s actually done with a good plan – to hide uploaded files from users by default, to avoid illegal access or scraping.

Also, you may ask how to show the file then, if /storage folder is not accessible in the browser? It’s not public, right?

If you do want those files to be public, you need to change two things:

1. Config disk change. Change config/filesystems.php parameter ‘default’ – from ‘local’ to ‘public’: then the files will be stored in storage/app/public (we will talk about filesystems settings later);

2. Symlink. Put a symlink from /public/storage to /storage/app/public folder, with one Artisan command:

php artisan storage:link

Here’s an excerpt from the official Laravel docs on this:

By default, the public disk uses the local driver and stores these files in storage/app/public. To make them accessible from the web, you should create a symbolic link from public/storage to storage/app/public.

Now, we can access the file from our example:

Another way, of course, is to check access in your Laravel code and then return the file as downloaded stream.

// This will come from database in reality
$filename = 'onvuqaJkKWx6ShRSserOR8p5HAE4RE3yJPCeAdrO.png';

if (!auth()->check()) {
    return abort(404);
}
return response()->download(storage_path('app/public/logos/' . $filename));

In this example, actual users won’t even know the actual filename until download, so you’re in control over the access to that file.

Finally, if you want to delete the file, you should use Storage facade for it:

use Illuminate\Support\Facades\Storage;

$filename = 'onvuqaJkKWx6ShRSserOR8p5HAE4RE3yJPCeAdrO.png';
Storage::delete('logos/' . $filename);

This is pretty much all the information we get from the official Laravel documentation about file upload. But in reality, it’s only the beginning. Let’s dive deeper.


2. File validation

Laravel has quite a lot of validation rules, that can be stored in Request classes, some of them are related to files.

For example, if you want to check for successfully uploaded file you may check this.

class StoreBookRequest extends FormRequest
{
    public function rules()
    {
        return [
            'logo' => 'required|file',
        ];
    }
}

This rule file means: The field under validation must be a successfully uploaded file.

Here are more rules related to files:

image:
The file under validation must be an image (jpeg, png, bmp, gif, or svg)


mimes:jpeg,bmp,png,…:
The file under validation must have a MIME type corresponding to one of the listed extensions.

Even though you only need to specify the extensions, this rule actually validates against the MIME type of the file by reading the file’s contents and guessing its MIME type.

A full listing of MIME types and their corresponding extensions may be found at the following location: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types


mimetypes:text/plain,…:
The file under validation must match one of the given MIME types.

'video' => 'mimetypes:video/avi,video/mpeg,video/quicktime'

To determine the MIME type of the uploaded file, the file’s contents will be read and the framework will attempt to guess the MIME type, which may be different from the client provided MIME type.


size:value
The field under validation must have a size matching the given value. For files, size corresponds to the file size in kilobytes.


dimensions:

The file under validation must be an image meeting the dimension constraints as specified by the rule’s parameters:

'avatar' => 'dimensions:min_width=100,min_height=200'

Available constraints are: min_width, max_width, min_height, max_height, width, height, ratio.

A ratio constraint should be represented as width divided by height. This can be specified either by a statement like 3/2 or a float like 1.5:

'avatar' => 'dimensions:ratio=3/2'

You can check other validation rules for Laravel here.

Notice. Important note about size validation – don’t forget to check your php.ini settings. By default, you can upload files only up to 2mb.

upload_max_filesize = 2M

Also, check maximum POST request size:

post_max_size = 8M

But be careful with putting bigger numbers here, it can be used in wrong ways by some people. To upload much bigger files, you can use chunks, we will discuss it separately later.


3. Uploading to external disks: Amazon S3

Disks and Drivers.

So far, in this article we touched on the basic file upload to the same code repository – into the /storage folder. But quite often it makes sense to store files separately, especially if they are bigger and could take up server disk space.

Let’s meet two new concepts of Filesystem: drivers and disks.

Driver is a type of storage.
Laravel supports these drivers out of the box:

  • local: default one we used so far
  • ‎s3: represents popular Amazon S3, requires additional package league/flysystem-aws-s3-v3
  • ‎rackspace: represents Rackspace Cloud storage, requires additional package league/flysystem-rackspace
  • ftp: you need to setup host/username/password to make it work

Ok, these are drivers.

Now, there’s another term called disk – it represents the actual folder/bucket name within your chosen driver.

And yes, it means that one driver can have multiple disks – for example, you want to use one S3 bucket for images, and another one for documents.

Let’s look at a default config/filesystems.php settings:

'default' => env('FILESYSTEM_DRIVER', 'local'),

'disks' => [

    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
    ],

    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL').'/storage',
        'visibility' => 'public',
    ],

    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
    ],

],

You can see a few things here:

  • Same driver called local has two different disks – local and public. Remember we changed between the two, in the chapter above?
  • Parameter ‘default’ points to the disk that will be used as the primary one
  • Please put your parameters in .env files, to be able to have different settings for testing and live servers. We’ve written about .env files here.

So, if you want to use S3 as primary driver, you need to install league/flysystem-aws-s3-v3 package and change the .env file:

FILESYSTEM_DRIVER=s3

And then also you can specify the disk directly in the code of file upload.

$request->logo->store('logos', 's3');

If we do that and put in our AWS details, here’s how our file will be placed in S3 Console.

Now, by default our files will get public URLs, like this:
https://s3-eu-west-1.amazonaws.com/laraveldaily-videos-test/logos/OIIB2jilZkhn7wUI6Mol14pgiCtmtxdLUyoZmVKh.png

Is that a security breach? Well, yes and no.

One of the most popular examples of S3 usage is popular project management system called Trello. If you attach a file to any card there, it is stored in S3 and is encoded to really long public URL, like this – see browser URL bar:

So you can access the file if you know the name. Here’s the official comment from Trello’s help page:

The URLs for the attachments are cryptographically unguessable, meaning that no one is going to be able to guess your attachment URL. However, if you share the URL for a Trello attachment, anyone with the URL will be able to see the attachment.

If you need extra security, we recommend using Google Drive for attachments. You can attach files from Google Drive directly to Trello and specify fine-grained access to the file via the Google Drive interface.

In other words, you can add additional layers to your file storage for security, Trello is just one example of this.

Ok, we’re done with disks/drivers and Amazon S3 example. Let’s move on to exciting topic of uploading larger files.


4. AJAX upload: Large/multiple files

We’ve touched briefly on larger file uploads, that it makes sense to store them outside the code, on something like Amazon S3. But there’s another issue – uploading process, which leaves user waiting for quite a while. We need to handle it gracefully.

If you just POST a big file via default form, browser will only show loading signs and you won’t get notified about any progress until it’s done.

We can change that using AJAX upload and certain library – as an example, I will take BlueImp JQuery library.

I’ve recently shot a demo-video for file uploads to Amazon S3:

Also, code on GitHub: https://github.com/LaravelDaily/Laravel-AmazonS3-Video

Another tutorial I’ve written is this: Laravel AJAX File Upload with BlueImp JQuery Library

So I won’t repeat those tutorials here, please read them in full detail, but I will just re-cap the main things:

  • You use JavaScript library to upload files – catch event on click of upload button
  • Every JavaScript library would still need back-end URL to perform upload – you can code it yourself, or use some Laravel package for upload like Spatie Larvel MediaLibrary
  • It is uploading files in chunks so should work for larger files, too

5. Bonus. Image manipulation: resize/crop/thumbs

Ok, one more final topic. We often upload images and there should be ways to manipulate them before/after storing. For example, we need to make several thumbnails with different sizes – one for the detailed view, one for the list view, and final one as a small icon.

There’s a great package for that, recommended by Laravel community and beyond: Intervention Image

This package is not actually for Laravel specifically, it works in general PHP, too.

How to install it in Laravel:

composer require intervention/image

The official documentation also mentions config/app.php changes, but it’s not needed anymore since Laravel 5.5.
You can use it right after the composer command.

Now, what can we do with this package? Basically, anything you need with images.

Here’s the most simple example:

use Intervention\Image\Facades\Image;

public function index()
{
    $img = Image::make(storage_path('app/logos/1.png'))->resize(300, 200);

    return $img->response('jpg');
}

It will take the file from storage (where we uploaded it), resize and show it in the browser.

You can also save it in different folder for thumbnails:

Image::make(storage_path('app/logos/1.png'))->resize(300, 200)->save('1-thumb.png');

Here are only a few of the methods that you can use for image manipulation:

So, just get creative, read their official documentation and move on to examples like this:

$image->fit(250, 250, function ($constraint) {
    $constraint->aspectRatio();
});

So, we’ve covered a lot about file uploading in Laravel. I hope you will have a deeper picture now about how it works.

In our QuickAdminPanel Laravel Generator, we do use all those three packages mentioned above, so you wouldn’t have to write the code at all, just enter the details for the field:

See you soon in other tutorials for Laravel!