Multiple File Upload with Dropzone.js and Laravel MediaLibrary Package

File upload is one of the most popular features in modern web. And we have quite a few libraries that can help us to build upload form. Let’s take two of my favorites – Dropzone on the front-end, and Spatie MediaLibrary on the back-end, and build a great uploading experience, in this tutorial.

First, what we’re building here. A simple for to add Projects, where you can also upload multiple files for every project.

As you can see, file upload has a big block instead of just an input file field. That’s how Dropzone works. But let’s take it one step at a time.

Step 1. MediaLibrary Installation

Let’s prepare the back-end, where we will actually store the files. We install the package like this:

composer require spatie/laravel-medialibrary:^7.0.0

Next, we publish their migration files, and run migrations:

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"
php artisan migrate

By this time, we should have media table in our database.

This table uses Polymorphic Relations, so in our case will store records with model_type field equals app\Project, which means that media file will be assigned to a project (not to a user, or anything else).

Step 2. Adding Dropzone.js code

In our Blade file, with the form, we need to add JavaScript code for Dropzone. There are multiple ways to do it, depending how you structure your whole Blade architecture, but here’s my version of resources/views/admin/projects/create.blade.php:

<form action="{{ route("") }}" method="POST" enctype="multipart/form-data">

    {{-- Name/Description fields, irrelevant for this article --}}

    <div class="form-group">
        <label for="document">Documents</label>
        <div class="needsclick dropzone" id="document-dropzone">

        <input class="btn btn-danger" type="submit">

Ok, so what you can see here?

  • Full form is gonna be submitted to route – we will get to that later
  • Dropzone block is just a DIV with ID and some classes, how will it actually work?

Ok ok, let’s add the JavaScript to make it actually work. At the end of the Blade file, I have this section:

  var uploadedDocumentMap = {}
  Dropzone.options.documentDropzone = {
    url: '{{ route('projects.storeMedia') }}',
    maxFilesize: 2, // MB
    addRemoveLinks: true,
    headers: {
      'X-CSRF-TOKEN': "{{ csrf_token() }}"
    success: function (file, response) {
      $('form').append('<input type="hidden" name="document[]" value="' + + '">')
      uploadedDocumentMap[] =
    removedfile: function (file) {
      var name = ''
      if (typeof file.file_name !== 'undefined') {
        name = file.file_name
      } else {
        name = uploadedDocumentMap[]
      $('form').find('input[name="document[]"][value="' + name + '"]').remove()
    init: function () {
      @if(isset($project) && $project->document)
        var files =
          {!! json_encode($project->document) !!}
        for (var i in files) {
          var file = files[i]
, file)
          $('form').append('<input type="hidden" name="document[]" value="' + file.file_name + '">')

Looks complicated, doesn’t it? No worries, I will point to the actual places you need to look at:

  • route(‘admin.projects.storeMedia’) – that would be the URL to process the file that has been dropped into the area, before the actual form is submitted;
  • $(‘form’).append() – after the URL above will do the job of uploading the file, we will take its filename and add a hidden input array field with that filename. And later on submitting the form we will process only that filename and assign it where appropriate;
  • There’s also function to remove file, then that hidden field is also being deleted;
  • A few more details like CSRF-token or 2 MB upload restriction, but I think you will figure it out.

Notice: this JavaScript code will also work without any changes for edit form, not only create.

Now, keep in mind that this section cannot come just like that. In the main “parent” Blade layout file you should load some more scripts and @yield(‘scripts’) command. Here are excerpts from my resources/views/layouts/admin.blade.php:

{{-- CSS assets in head section --}}
<link href="" rel="stylesheet" />

{{-- ... a lot of main HTML code ... --}}

{{-- JS assets at the bottom --}}
<script src=""></script>
<script src=""></script>
{{-- ...Some more scripts... --}}
<script src=""></script>


So, as you can see, we’re loading jQuery, Bootstrap theme and Dropzone CSS+JS from CDN.

Ok, at this point, we should be able to drop the files into our Dropzone block, but they are not being uploaded yet. We need to implement that route(‘projects.storeMedia’) part.

Step 3. Uploading the Files

First, our routes/web.php will have this line:

Route::post('projects/media', 'ProjectsController@storeMedia')

Now, let’s go to app/Http/Controllers/ProjectsController.php:

public function storeMedia(Request $request)
    $path = storage_path('tmp/uploads');

    if (!file_exists($path)) {
        mkdir($path, 0777, true);

    $file = $request->file('file');

    $name = uniqid() . '_' . trim($file->getClientOriginalName());

    $file->move($path, $name);

    return response()->json([
        'name'          => $name,
        'original_name' => $file->getClientOriginalName(),

Nothing magical here, just using standard Laravel/PHP functions to upload file, forming its unique filename, and returning it along with original name, as JSON result, so that Dropzone script could continue its work.

Notice: I store files temporarily in storage/tmp/uploads, you may choose other location.

Ok, we’re getting close. Now we have files in our server, but no entry in the database, because Project form isn’t submitted yet. It looks something like this:

Now, let’s hit Submit and see how to tie it all together.

Step 4. Submitting the Form

After we click Submit, we land on the method ProjectsController@store(), which is typical for Laravel resource controller. Here’s the code:

public function store(StoreProjectRequest $request)
    $project = Project::create($request->all());

    foreach ($request->input('document', []) as $file) {
        $project->addMedia(storage_path('tmp/uploads/' . $file))->toMediaCollection('document');

    return redirect()->route('projects.index');

Looks simple, doesn’t it? Typical creating of Project record, and then going through each hidden document field (remember, we create them after each file upload), and adding them into Media Library.

At this point, you should have records in media database table, related to the Project’s ID that you have just saved.

Step 5. Edit/Update Form

If you want to have the same functionality in Edit form, the front-end part (Blade/JavaScript) remains almost unchanged, the important part is how to save the update files with record. So we’re looking at ProjectController again:

public function update(UpdateProjectRequest $request, Project $project)

    if (count($project->document) > 0) {
        foreach ($project->document as $media) {
            if (!in_array($media->file_name, $request->input('document', []))) {

    $media = $project->document->pluck('file_name')->toArray();

    foreach ($request->input('document', []) as $file) {
        if (count($media) === 0 || !in_array($file, $media)) {
            $project->addMedia(storage_path('tmp/uploads/' . $file))->toMediaCollection('document');

    return redirect()->route('admin.projects.index');

In other words, first we delete unused files, and then assign only those that are not in the media list yet.

Step Homework: Clean-up

I didn’t put it in this article, but you may want to handle the situation when people upload the files on the form but then don’t hit final Submit. It means that the files are still stored on the server.

It’s up to you how to deal with them – save in user’s “memory” somewhere for future use, or maybe create a separate cron-based Artisan command to cleanup all those files that have not been used.

That’s it! For more information, visit official documentations of both packages and Laravel Filesystem.

Like our articles?
Check out our Laravel online courses!


  1. Hi,

    Have implemented your approach! Thank you. This was really hard to find a solution online, despite close to a million downloads of Media Library.

    The MediaLibrary creates folders with the uploads, e.g. media_id = 1 has a folder with it’s files, conversions, etc.. However, this approach puts all images in a single folder. Do you have a solution to maintain that functionality?

    Thank you,

    • Hi Trevor
      Great that my article helped.

      What exactly do you mean about all images in a single folder? So how exactly you want to save them, in what folder structure?

        • I don’t have any issues with images storing in their own subfolders. At first it seemed unnecessary, but then it doesn’t cause any problems, and sometimes help with search.

          • Hi Povilas,

            So your demo created folders?

            I’ll take a look again, for some reason the installation I have just dumps all files into storage/tmp/uploads per the tutorial.

            Can you think of anything I missed?

            I’ll remove the count block (your below comment). A few posts on stack talked about php7.2 not being able to handle the count.

            Thank you`

          • Well spotted, in that particular tutorial indeed I see I missed the part of moving files to their folder appropriately. Will check both of those – this miss, and the previous one with count() and will update the article. But only on Monday, sorry.

          • Hi again Trevor, I double-checked the code again and seems that it’s all working, maybe you missed something.

            1. Storing from tmp folder to “normal” storage is done by ->addMedia() function.
            2. That count($project->document) comes from JS init method, and it does set project document variable.

            So, works for me, but maybe I missed some edge case while testing.

  2. Hi Povilas,

    “`count(): Parameter must be an array or an object that implements Countable“`

    Have you received that error on the update method?
    here’s my: PHP 7.2.14 (cli) (built: Jan 12 2019 05:23:00) ( NTS )

    • Probably error comes from count($project->document) and I see that in the article I forgot to include that relationship from the model. Well, just remove all that block of code (it is for deleting old documents) and it will start working. I don’t have the code of that demo project anymore, so I can’t reproduce that relationship, sorry.

  3. Povilas!

    Ok, finally tracked down why the folders were not being created.

    In the Step 4 above, the line:
    input(‘document’, []) as $file) {
    $project->addMedia(storage_path(‘tmp/uploads/’ . $file))->toMediaCollection(‘document’);
    should add the ‘disks’ so it is stored in ‘tmp/uploads/’ as a file (no folder), and also to the disk media_id folder. (in my case I used ‘media’ disk, and defined it to go to a public folder)

    As a beginner, I watched videos and saw folders being created to my amazed eyes – but my demo here did not create them. It was just maddening because without the disks second parameter, obviously now, they were not created.

    For anyone else, the disks is in /config/filesystems.php

    >input(‘document’, []) as $file) {
    $project->addMedia(storage_path(‘tmp/uploads/’ . $file))->toMediaCollection(‘document’, ‘media’);

    thank you`

  4. Hello Povilas,

    is Step 3 really necessary? I mean the part with uploading the file. Can’t it be handled by medialibrary package only?


    • Hi Miro,
      In the way I do it, it is necessary. Cause I’m uploading the files on the event when they are actually uploaded with Dropzone, so that’s done in Step 3. Later Step 4 is only ASSIGNING files by their IDs (files already exist on the server by then) with Media Library.

      But maybe you can come up with a way to make it all in one step, then let me know I will update the article.

  5. Hi Povilas,

    How to retrieve the images and pass to edit form? Is this correct?
    The images didn’t render in the dropzone.

    $project = Project::find($id);
    $project->document = $project->getMedia(‘document’);

    return view(‘projects.edit’,compact(‘project’));

    • Sorry, there’s no short answer, I would have to write whole separate section on edit, I think your way is correct but then it depends how you use those variables in projects/edit.blade.php view. Please experiment.

  6. Good morning, how are you?

    Greetings from Brazil.

    Does your project save the “one per folder” files?
    My saves like this:

    A folder for each file, which for me is not viable.

    Do you have any tips on how to save all the files in your specific template? For example:

    Property 1 = Create the folder name ‘1’ and upload the files in that particular folder … and so on.

  7. Hello!
    I am getting this Error!

    ErrorException (E_NOTICE)
    Array to string conversion

    Somehow, your $document variable is $document[]
    so its not possible to use the Laravel create method.

    when I want to save the data from project.

    any ideas?

  8. Hi,

    Thank you for the article. I was battling with this. Would you be able to help with large file uploads? I am in need to do chunking with dropzone. My clients need to able to upload video files that is more or less 5gb. I have looked at different tutorials, but have no idea how to implement it with your tutorial.

    • alson my inti function not work properly , I can not show my image thumbnail and I have set maxFile=1, attribute but I also select one file too

  9. Hey, I used the same steps you have done but the main issue I m facing is the after uploading the file to the server file is not getting deleted from temp folder which has been created can you help me to solve this issue.

  10. Hello Povilas,

    i think upload file at the same time with another fields is better. To avoid user can upload but not submit form and go back. in that case, you will have a lot of files trash. Dropzone have an option to do it, not need to use cron

  11. Hello Povilas, nice tutorial here, I am trying to do the clean up thing, do you have any reference on how to do the cleanup?

  12. Hello Povilas Korop,

    Thanks for the Article.

    I seems to be getting an error:
    File “ does not exist
    The images get uploaded in step 3 but get removed on clicking on save button and through the above error.

  13. I notice that there is a ‘custom_properties’ field. Is that available for our own use? For example, to add tags to an uploaded file ? Any examples of such a case?

  14. This is fabulous, thank you!! I spent hours trying to get this functionality working. I love how you separated the “upload” and “database insertion” parts into different controllers.

  15. Hello from Africa, thanks for your excellent tutorial. It works perfectly.

    I have one question how can I retrieve the multiple images on the blade files?

  16. Hi

    for those like me who had problems inserting the thumb when editing.

    follows the javascriptp line that solved the problem for me., file), file, file.original_url);

    Hope this helps

  17. Hi Povilas,

    I’ve come across this article trying to work through the Vue output of the QuickAdminPanel with regards to file uploads. You use the same approach of uploading files immediately, even before the model has been persisted. I understand the benefits of this from some of the above discussion too.

    My question is: should we have some sort of process to identify media with model_id = 0 and remove these from storage and the database, perhaps on a weekly basis? From everything I can see, this isn’t implemented but would be interested in your confirmation and some quick thoughts on how to attack the problem if you can please?

  18. Hello sir,

    Is it possible to have the dropzone input field and the uploaded files/images to be set smaller than the usual in some page?


Please enter your comment!
Please enter your name here