File uploads with Laravel is incredibly easy, thanks to the powerful file system abstraction that it comes bundled with (go ahead and give a shout-out to Frank de Jonge for the awesome FlySystem package) . Couple that up with Dropzone’s drag and drop feature, and you have a robust file upload mechanism. Not only these are easy to implement, they provide a great number of features in very few lines of code.

drop

Setting up the uploads

We’ll create a database table that contains information about each of the uploads. Of course we are not going to store our file in the database itself, rather we’d store only some metadata of the file. So let’s create a schema.

public function up()
    {
        Schema::create('uploads', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned()->index();
            $table->string('filename');
            $table->bigInteger('size');
            $table->softDeletes();
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

Here only metadata we are storing are filename and size. filename would be used to store and retrieve the file later. We add a foreign key user_id to keep track of who uploaded the file. This can also be used to check the owner of a file. We are also going to use SoftDeletes to mark a file as deleted rather than actually delete the file from storage, when a user deletes a file.


Now, let’s php artisan make:model Upload to create a Upload model for this table.


Next, we define the relationship between a user and an uploaded file – a user may upload many files and each uploaded file will belong to a user. Therefore, let’s define a one-to-many relationship in our User and Upload models. In the User model:

public function uploads()
{
    return $this->hasMany(Upload::class);
}

and the inverse relationship is defined in our Upload model:

public function user()
{
	return $this->belongsTo(User::class);
}

Also while we are at it, let’s update the fillable columns for our Upload model.

use Illuminate\Database\Eloquent\SoftDeletes;

use App\User;

class Upload extends Model
{
    use SoftDeletes;
	
    protected $fillable = [
        'filename', 'size',
    ];

    public function user()
    {
    	return $this->belongsTo(User::class);
    }
}

That’s all! Our Upload model is now ready.

Setting up dropzone

Now that our uploads backend is ready, let’s setup dropzone for drag and drop file uploads.


First we need to install dropzone, so let’s do a npm install dropzone --save . Next, we need to load the dropzone library. Since we are going to use Laravel Mix to compile our frontend assets, we can load the library within resources/assets/js/bootstrap.js :

// Dropzone
window.Dropzone = require('dropzone');

Dropzone.autoDiscover = false;

This will set Dropzone to the window object. We are disabling the autoDiscover option, which is used to automatically attach dropzone with any file input, which we do not need.


Now let’s add the styling for this (otherwise it’s going to look uglier than Voldemort’s face, trust me). We can do this in our resources/assets/sass/app.scss.

@import "~dropzone/src/dropzone.scss";

.dropzone {
	margin-bottom: 20px;
	min-height: auto;
}

All set. So we can now compile the assets using npm run dev.

Setting up routes

Before we start uploading any files, we need to define some routes. Here we’d create two routes, one for receiving the uploads and one for deleting the uploads, should a user choose to delete one.

Route::post('/upload', 'UploadController@store')->name('upload.store');

Route::delete('/upload/{upload}', 'UploadController@destroy');

Let’s php artisan make:controller UploadController to create the controller. We will use this UploadController to process the uploads in the backend.

Uploading files

Now that we have pulled in dropzone and attached it to the window object, we need to integrate dropzone within a view to start file uploads. We can simple do this using an id tag.

<div id="file" class="dropzone"></div>

class="dropzone" is used to style the dopzone upload box. We can use the #file to inject dropzone on this element. (tip: it is a best practice in laravel to create javascript snippets in separate partials and then including them in whichever blade template they are needed)

let drop = new Dropzone('#file', {
    addRemoveLinks: true,
    url: '{{ route('upload.store') }}',
    headers: {
        'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
    }
});

We are instantiating the Dropzone class on our #file and then passing in some addition configuration. addRemoveLinks will add a remove link to every uploaded file, so a user can remove a file after uploading it. The url option is used to let dropzone know where to post the uploaded file to, in case we are not using a form. If we are using a form, by default dropzone would use the action attribute of the form as the post url (I’m not quite sure about this though, so please don’t take my words as is, and do correct me if I am wrong). We are using the upload.store route which we have defined earlier.


headers are used to send additional headers to the server. Because we are posting our uploads, we need to send a X-CSRF-TOKEN header, otherwise our app will throw a TokenMismatchException (this is used by laravel to protect our app against CSRF attacks). By default, the meta information in head contains a meta with the name csrf-token . Therefore we select the meta and extract the content which gives us the token .


Dropzone provide’s a huge number of configuration options. Check out the documentation for all the available options. (Also don’t forget to thank the developer of dropzone )


Now that we are able to post files from the frontend, let’s work on our controller to receive and process the uploads on backend.


In the UploadController , let’s define a method storeUploadedFile() which receives a Illuminate\Http\UploadedFile and store all the metadata in our database.

protected function storeUploadedFile(UploadedFile $uploadedFile)
{
    $upload = new Upload;

    $upload->fill([
        'filename' => $uploadedFile->getClientOriginalName(),
        'size' => $uploadedFile->getSize(),
    ]);

    $upload->user()->associate(auth()->user());

    $upload->save();

    return $upload;
}

getClientOriginalName() gives us the original name of the file that has been uploaded. getSize() returns the size of the uploaded file in bytes. We then associate() the upload with the currently logged in user. Finally we save the file and return the model instance.


In the store() method, we accept a Request and then from that request, we extract the file.

public function store(Request $request)
{
    $uploadedFile = $request->file('file');
}

We are going to store this file in our local filesystem. Of course you can upload it to Amazon S3 or any other cloud storage services and laravel provides an excellent documentation on how to configure different storage services. However, I’m going to store the uploads on our local disk only.


Next we need to save the file metadata like name, size and the user who uploaded it to our database. For this, we can use the storeUploadedFile() method we implemented above.

$upload = $this->storeUploadedFile($uploadedFile);

This returns us an instance of the Upload model. We can now use this to store the file.

Storage::disk('local')->putFileAs(
    'uploads/' . $request->user()->id,
    $uploadedFile,
    $upload->filename
);

The Storage facade is used to interact with the disk in which we are going to store the file. Storage::disk('local') gives us an instance of the local disk. The default local storage root is storage/app .


putFileAs() method is used to automatically stream a given file to a storage location. The first argument 'uploads/' . $request->user()->id is the upload path. We are creating a new directory uploads and within that directory creating another directory using user’s id and storing the file there, so that all files uploaded by a user goes to the same location. For example, if a user’s id is 7, any file he uploads will be stored at storage/app/uploads/7/.


The second argument in putAsFile() method is the Illuminate\Http\UploadedFile instance and the last argument is the filename that will be used to store the file. Because at this point, we already have stored the file’s metadata in our database, we can simply get the name of the file by $upload->filename.


At this point, file storing is complete. Finally we send back a JSON response with the uploaded file’s id.

return response()->json([
    'id' => $upload->id
]);

That’s all. The file has been stored on our local disk.

Deleting files

To remove a file, Dropzone provides a removedfile event, which we can listen to and then send a delete request. We will use axios to send the request.


We can register to any dropzone event by calling .on(eventName, callbackFunction) on our dropzone instance. Check this documentation for a list of different dropzone events and when they are triggered.


Now we need to use the id of the uploaded file in order to send the delete request. But from the frontend how could we possibly know what’s the id of a uploaded file? Well, this is where the JSON response from the store() method is useful. When we uploaded a file successfully, we are sending back the id of the file from the backend. On a successful upload, we can therefore associate this id with the file on the frontend.

drop.on('success', function(file, response) {
	file.id = response.id;
});


When a file has been uploaded successfully, dropzone triggers a success event. We can register to this event to get the file instance and the response from the backend inside the callback function. We simple assign the response.id to file.id. Therefore, for every file that we have successfully uploaded, now we have an identifier associated with it to be used on the frontend. Great!


Now that we have an id associated with each file, deleting files is easy. We listen for removedfile event and then use axios from the global window object to fire a delete request to the backend.

drop.on('removedfile', function(file) {
	axios.delete('/upload/' + file.id).catch(function(error) {
		drop.emit('addedfile', {
			id: file.id,
			name: file.name,
			size: file.size,
		});
	});
});


Notice how we are chaining the catch() method. This to catch any error when removing a file. If an error occurs that prevented the file deletion, we want to add it back so that the user knows the deletion failed and they may try again. We do that simply by calling the emit() method and passing in the details of the file. This will call the default addedfile event handler and add the file back.


Okay. So our frontend is ready to delete files. Let’s start working on the destroy() controller method.


Because we are injecting the file id on our delete request, we can therefore accept the file we trying to delete , destroy(Upload $upload) , using laravel’s route model binding. The next thing we need to do is verify if the delete request is actually coming from the owner of the file. Let’s create a policy, UploadPolicy for that.

use App\{User, Upload};
use Illuminate\Auth\Access\HandlesAuthorization;

class UploadPolicy
{
    use HandlesAuthorization;

    public function touch(User $user, Upload $upload)
    {
        return $user->id === $upload->user_id;
    }
}


In the touch() method, we are checking if the ids of the user who uploaded it and the user who is sending the delete request is same. In our destroy() method, we can now use this touch() method to authorize the request.

$this->authorize('touch', $upload);

Finally we can delete the file.

public function destroy(Upload $upload)
{
	$this->authorize('touch', $upload);

	$upload->delete();
}

Since we are using SoftDeletes , this will mark the file as deleted in the database.


That’s all folks.


The complete UploadController looks like this.

<?php

namespace App\Http\Controllers;

use Storage;

use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;

use App\Upload;

class UploadController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function store(Request $request)
    {
    	$uploadedFile = $request->file('file');

        $upload = $this->storeUploadedFile($uploadedFile);

        Storage::disk('local')->putFileAs(
            'uploads/' . $request->user()->id,
            $uploadedFile,
            $upload->filename
        );

        return response()->json([
            'id' => $upload->id
        ]);
    }

    public function destroy(Upload $upload)
    {
        $this->authorize('touch', $upload);
        $upload->delete();
    }

    protected function storeUploadedFile(UploadedFile $uploadedFile)
    {
        $upload = new Upload;

        $upload->fill([
            'filename' => $uploadedFile->getClientOriginalName(),
            'size' => $uploadedFile->getSize(),
        ]);

        $upload->user()->associate(auth()->user());

        $upload->save();

        return $upload;
    }
}

Conclusion

There can be so many different ways of implementing file uploads. Dropzone’s documentation includes an example section that have quite a few in-depth excellent examples. Don’t forget to check that out!

ヽ(´▽`)/