File Management with VueJS and Laravel

Part 38 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori January 15th, 2018
⚡️ Updated content is available We learned a lot since we originally wrote this article. We now have this updated for Laravel 10, Vue 3/NuxtJS 3, and Capacitor 3.

This whole tutorial series: API Driven Development With Laravel and VueJS has been focused on building a single page application with VueJS that’s powered by an API with Laravel. We’ve gone through a variety of requests, but what we haven’t touched on is file uploading. File uploading can be a little tricky by nature, but through an API and an AJAX request it’s a little bit trickier. This tutorial will guide you through the process of uploading files with VueJS and Axios: GitHub – axios/axios: Promise based HTTP client for the browser and node.js. Axios is what we’ve been using to perform our API requests so it should already be installed in your app up to this point! We will be using our Add Cafe form as an example. This will allow users to upload pictures of the cafe.

If you want to get more in depth on the VueJS side of things, I wrote a tutorial series on just uploading files and all the bells and whistles with VueJS and Axios: Your Guide To Uploading Files with VueJS and Axios.

Step 1: Add Migrations To Store Files

The first step I do when preparing to allow a resource to have files is to add a table of records in the database. This allows us to make a relationship between the file and the resource. In this case, we are going to attach files to the cafes so make a quick migration:

php artisan make:migration added_cafes_files --create=cafes_photos

Our migration should look like:

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('cafes_photos', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('cafe_id')->unsigned();
        $table->foreign('cafe_id')->references('id')->on('cafes');
        $table->integer('uploaded_by')->unsigned();
        $table->foreign('uploaded_by')->references('id')->on('users');
        $table->text('file_url');
        $table->timestamps();
    });
}

/**
 * Reverse the migrations.
 *
 * @return void
 */
public function down()
{
    Schema::dropIfExists('cafes_photos');
}

In our migration we allowed the photo uploaded to be bound to a cafe and bound to the user uploading the file. We can now use these relationships in eloquent on the API side.

Step 2: Add Eloquent Relationships

Let’s quickly add these relationships before we dive into the front end which will be the guts of the article.

First, add the following file: app/Models/CafePhoto.php. In the file add the code:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class CafePhoto extends Model
{
    protected $table = 'cafes_photos';

  public function cafe(){
    return $this->belongsTo('App\Models\Cafe', 'cafe_id', 'id');
  }

  public function user(){
    return $this->belongsTo('App\Models\User', 'uploaded_by', 'id');
  }
}

This allows us to relate the photo to the cafe and to the user who uploaded it.

Second, open up /app/Http/Models/Cafe.php and add the following relationship:

public function photos(){
  return $this->hasMany( 'App\Models\CafePhoto', 'id', 'cafe_id' );
}

This states that the cafe has many photos and we can call the record to get the information about the photo.

Third, open up /app/Http/Models/User.php and add the following relationship:

public function cafePhotos(){
  return $this->hasMany( 'App\Models\CafePhoto', 'id', 'cafe_id' );
}

This relationship states that the user has many cafe photos that they’ve uploaded.

Step 3: Make File Folder

We will want to make a quick folder to house all of our files. This should be
located in our app directory and outside of the public directory so we can have security over who can access the file.

For now, just create the following directory: app/Photos. We will be grouping the files in folders by the id of the cafe. This will be handled on upload.

Now that we have our relationships set up and our file container set up we can begin uploading the files with VueJS and Axios.

Step 4: Adjust API Calls for Adding and Editing Cafe to Handle Files

There are two URLs that need to be adjusted right now to handle file uploads. Those are the adding and editing cafe routes : POST /api/v1/cafes & PUT /api/v1/cafes/{cafeID}.

To do this, we will need to open up the /resources/assets/js/api/cafe.js and make a few changes to how these requests are sent. Like with any form request that involves files, we need to make sure we have the proper headers set and for this case, we will be using the FormData object which will allow us to send files with the proper header. For more information on FormData check out: FormData – Web APIs | MDN.

First, let’s edit the postAddNewCafe method to use FormData. We need to initialize an empty object:

/*
  Initialize the form data
*/
let formData = new FormData();

We will be adding all of our data to be passed to the API through here. Now, in our request, the second parameter we pass to the axios.post() method was an object that contained the data we are submitting to add a cafe:

{
  name: name,
  locations: locations,
  website: website,
  description: description,
  roaster: roaster
}

What we will do now is build out our formData object to contain these fields:

formData.append('name', name);
formData.append('locations', locations);
formData.append('website', website);
formData.append('description', description);
formData.append('roaster', roaster);

and then pass the form data object as the second parameter of the axios.post() request like so:

return axios.post( ROAST_CONFIG.API_URL + '/cafes',
  formData
);

Now, what we need to do is add an additional parameter to our postAddNewCafe() method that will allow for a picture of the cafe to be passed along with the rest of the data.

Our method signature should look like:

/*
  POST  /api/v1/cafes
*/
postAddNewCafe: function( name, locations, website, description, roaster, picture )

We then need to add the picture to the form data now like so:

formData.append('picture', picture);

Now the picture will be added to the form request. We only need to do one thing, and that’s make sure that we have the right header being passed. Luckily axios.post() accepts a third parameter which is an object that can transform the request and add additional headers. So for the third parameter, we should add the following code:

{
  headers: {
      'Content-Type': 'multipart/form-data'
  }
}

Our new postAddNewCafe() method should look like:

/*
  POST  /api/v1/cafes
*/
postAddNewCafe: function( name, locations, website, description, roaster, picture ){
  /*
    Initialize the form data
  */
  let formData = new FormData();

  /*
    Add the form data we need to submit
  */
  formData.append('name', name);
  formData.append('locations', JSON.stringify( locations ) );
  formData.append('website', website);
  formData.append('description', description);
  formData.append('roaster', roaster);
  formData.append('picture', picture);

  return axios.post( ROAST_CONFIG.API_URL + '/cafes',
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      }
    }
  );
},

We now use the formData() to pass the data to the API request and we set the headers to accept a picture for the cafe. Notice we kept the same key, value names as before so our API will accept them properly. The only difference is we did a JSON.stringify() on the locations. This is so it’s passed as JSON and not a javascript object or our API will get confused

We will update our putEditCafe() to operate similarly:

/*
  PUT   /api/v1/cafes/{id}
*/
putEditCafe: function( id, name, locations, website, description, roaster, picture ){
  /*
    Initialize the form data
  */
  let formData = new FormData();

  /*
    Add the form data we need to submit
  */
  formData.append('name', name);
  formData.append('locations', JSON.stringify( locations ) );
  formData.append('website', website);
  formData.append('description', description);
  formData.append('roaster', roaster);
  formData.append('picture', picture);

  return axios.put( ROAST_CONFIG.API_URL + '/cafes/'+id,
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      }
    }
  );
},

Now that we’ve updated our API request, let’s make sure our Vuex action is sending the right parameters.

Step 5: Update Vuex Actions to Pass Picture

All we need to do to update the actions is to add the picture parameter to the call in the action. If we open our /resources/assets/js/modules/cafes.js file and find the addCafe() and editCafe() actions, we will see our api call within each method.

At the end of the method call, add the following parameter:
data.picture

our addCafe() action should look like:

/*
  Adds a cafe
*/
addCafe( { commit, state, dispatch }, data ){
  commit( 'setCafeAddedStatus', 1 );

  CafeAPI.postAddNewCafe( data.name, data.locations, data.website, data.description, data.roaster, data.picture )
      .then( function( response ){
        commit( 'setCafeAddedStatus', 2 );
        dispatch( 'loadCafes' );
      })
      .catch( function(){
        commit( 'setCafeAddedStatus', 3 );
      });
},

the editCafe() action should look like:

/*
  Edits a cafe
*/
editCafe( { commit, state, dispatch }, data ){
  commit( 'setCafeEditStatus', 1 );

  CafeAPI.putEditCafe( data.id, data.name, data.locations, data.website, data.description, data.roaster, data.picture )
      .then( function( response ){
        commit( 'setCafeEditStatus', 2 );
        dispatch( 'loadCafes' );
      })
      .catch( function(){
        commit( 'setCafeEditStatus', 3 );
      });
},

now our actions will pass the picture we choose. Our last edits should be in the forms for both editing and adding cafes.

Step 6: Update Forms To Allow For Picture Upload

Now that we have our API set up and our requests ready to handle the uploading of a picture, it’s time to allow for the user to upload pictures to the cafe. We will do this through the Edit and Add forms for the cafes.

First, let’s do this on the /resources/assets/js/pages/NewCafe.vue page.

In this page, we will add the following template code:

<div class="large-12 medium-12 small-12 cell">
  <label>Photo
    <input type="file" id="cafe-photo" ref="photo" v-on:change="handleFileUpload()"/>
  </label>
</div>

This allows us to upload a file and listen to when the input has changed. Now, we just need to add the handleFileUpload() method:

handleFileUpload(){
  this.picture = this.$refs.photo.files[0];
}

So, when our photo gets selected, we set the local data to the file in the first array of the photo element. We added the ref=“photo" to our file so we can access it from the $refs global in our VueJS element. We are now ready to send this along when we add a cafe:

submitNewCafe(){
  if( this.validateNewCafe() ){
    this.$store.dispatch( 'addCafe', {
      name: this.name,
      locations: this.locations,
      website: this.website,
      description: this.description,
      roaster: this.roaster,
      picture: this.picture
    });
  }
},

When we clear the form too, we should reset the picture to be an empty string and clear the input:

/*
  Clears the form.
*/
clearForm(){
  this.name = '';
  this.locations = [];
  this.website = '';
  this.description = '';
  this.roaster = false;
  this.picture = '';
  this.$refs.photo.value = '';

  this.validations = {
    name: {
      is_valid: true,
      text: ''
    },
    locations: [],
    oneLocation: {
      is_valid: true,
      text: ''
    },
    website: {
      is_valid: true,
      text: ''
    }
  };

  EventBus.$emit('clear-tags');

  this.addLocation();
},

We now are sending files to our API through VueJS and axis! Let’s quick make these updates on the edit side and jump back to the API to handle the upload on these files.

Step 7: Handle File Uploads on API

So we will head back to the Laravel API side again. We need to handle each file upload on the API. To do this, we will make a few changes to accept the new formatting from the FormData() and to accept a file. Before we do anything though, we will be working with the File facade, so add:

use File;

to the top of the controller.

First, open up the app/Http/Controllers/API/CafesController.php file and navigate to the postNewCafe() method.

On top of the method, load the locations into an array. Since we are using form data, this is now getting passed as JSON which we have to decode. So first, change the loading to look like:

$locations = json_decode( $request->get('locations') );

Now we are decoding the JSON to load the locations. When JSON is decoded, it’s values are stored in an object. We will need to adjust the reference from an array to an object when we load our first location, so change the location references to:

$address            = $locations[0]->address;
$city               = $locations[0]->city;
$state              = $locations[0]->state;
$zip                    = $locations[0]->zip;
$locationName           = $locations[0]->name;
$brewMethods            = $locations[0]->methodsAvailable;
$tags               = $locations[0]->tags;

Now we are ready to handle the file uploads. So AFTER the $parentCafe->save() method call, we will handle the upload. We need to have a parent cafe present before we can attach a picture to it.

First, let’s grab our picture:

$photo = Request::file('picture');

This grabs the file with the name of picture from our incoming request. Now, let’s check to see if we have something and if it’s a valid file:

if( count( $photo ) > 0 ){
  if( $photo != null && $photo->isValid() ){

    }
}

Next, we check to see if there is a directory for the cafe. We need the cafe ID so this is why we do it after the cafe has been saved:

/*
  Creates the cafe directory if needed
*/
if( !File::exists( app_path().'/Photos/'.$parentCafe->id.'/' ) ){
  File::makeDirectory( app_path() .'/Photos/'.$parentCafe->id.'/' );
}

This creates a directory for the cafe so in the future when we add logos and location pictures, we can have a nice structure.

Now, we set the destination path, get the file name and add the file to the directory, then save the record in the database bound to the cafe:

/*
  Sets the destination path and moves the file there.
*/
$destinationPath = app_path().'/Photos/'.$parentCafe->id;

/*
  Grabs the filename and file type
*/
$filename = time().'-'.$photo->getClientOriginalName();

/*
  Moves to the directory
*/
$photo->move( $destinationPath, $filename );

/*
  Creates a new record in the database.
*/
$cafePhoto = new CafePhoto();

$cafePhoto->cafe_id = $parentCafe->id;
$cafePhoto->uploaded_by = Auth::user()->id;
$cafePhoto->file_url = app_path() .'/Photos/'.$parentCafe->id.'/';

$cafePhoto->save();

That’s all we need to do to get a simple file uploader done with VueJS and Axios and handle the file upload with Laravel. Don’t forget to adjust your API to handle the multiple locations as an object and to also update your edit route! I’ll have all of that code pushed to GitHub here: GitHub – serversideup/roastandbrew so make sure you check it out!

Conclusion

That was a simple overview on how to upload pictures for the cafe. There will be a few more smaller details I will discuss as the project goes on and we will work more with Axios and VueJS file management. We will work on deleting files, uploading multiple pictures at once, and of course, displaying the photos on the page. Those tutorials coming soon! For now, make sure you check out the GitHub: GitHub – serversideup/roastandbrew and ask any questions in the comments below!

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Roast Designs Have Been Updated

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.