Favoriting or Liking With Laravel and VueJS

Part 24 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori November 20th, 2017
⚡️ 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.

A very common feature of newer applications, especially social media applications, is the ability to favorite, or like a specific entity such as a post, tweet, etc. In this tutorial I will show how to do the whole process from backend to front end of how to like a resource with Laravel on the backend and VueJS on the frontend. In this case, we will be allowing a user to like a coffee shop.

We will be using the code we have for Roast which is found here: GitHub – serversideup/roastandbrew: Helping the coffee enthusiast find their next cup of coffee. Also, helping aspiring web and mobile app developers build a single page app and converting it to a mobile hybrid. All tutorials can be found at https://serversideup.net and extending the functionality so the logged in user can like a certain coffee shop.

In the last few tutorials we built a parent child relationship between cafes (https://serversideup.net/eloquent-parent-child-relationship-laravel/) and we built a many to many relationship between cafes and brew methods (https://serversideup.net/many-many-relationships-laravel/). We will be building another relationship in this tutorial this time between a user and a cafe, which will be another many to many relationship since a coffee shop can be liked by many users and a user can visit many coffee shops. The difference is this relationship can be much more dynamic than an initial adding of a cafe with multiple brew methods and users can undo the relationship as well

Step 1: Add Likes Table

This table will be our join table between the users and the cafes that the user visits. It will contain the user_id and the cafe_id and the timestamps of the interaction.

First, run the command:

php artisan make:migration added_users_cafes_likes --create=users_cafes_likes

This will create a stubbed out migration for adding a table named users_cafes_likes. There might be some other relationships between cafes and users (like checkins 😉 ) that we will be adding later, so we will suffix the table with likes.

We will want to edit the up() method since we don’t need an incremental ID and we need to add our foreign keys. It should look like this:

public function up()
{
    Schema::create('users_cafes_likes', function (Blueprint $table) {
      $table->integer('user_id')->unsigned();
      $table->foreign('user_id')->references('id')->on('users');
      $table->integer('cafe_id')->unsigned();
      $table->foreign('cafe_id')->references('id')->on('cafes');
      $table->primary(['user_id', 'cafe_id']);
      $table->timestamps();
    });
}

The first four rows create a foreign key reference to the two entities involved in the many to many relationship. The fifth row does something we haven’t discussed in our tutorials and that is add add a composite primary key. What this does is makes the primary key on the table a combination of the user_id and the cafe_id. A user can like a cafe but can’t like the same cafe twice. This is a nice database mechanism to keep clean data, which is awesome!

Once we have our migrations created, we just have to run:

php artisan migrate

and we have our likes table!

Step 2: Build Relationships on Models

Both the /app/Models/Cafe.php and the /app/Models/User.php need to be set up for liking. Let’s start with the Cafe.php file.

We will need to add a relationship for likes() like so:

public function likes(){
  return $this->belongsToMany( 'App\Models\User', 'users_cafes_likes', 'cafe_id', 'user_id');
}

What this does is state that the cafe belongs to many users through the users_cafes_likes with the likes() relationship.

Now, we will add the likes relationship to the User.php file:

public function likes(){
  return $this->belongsToMany( 'App\Models\Cafe', 'users_cafes_likes', 'user_id', 'cafe_id');
}

This states that each user has many likes to many cafes.

Step 3: Add Routes to Like and Unlike Cafe

We need to add a few routes to like and unlike a cafe. The approach that I will be taking will be to make the route under the cafes because the user has the action to like or unlike a cafe, but not the reverse. A cafe can not like a user.

First, we will open the /routes/api.php file and add the following route:

Route::post('/cafes/{id}/like', 'API\CafesController@postLikeCafe');

This route will handle the liking of a cafe and is handled through an HTTP POST request which RESTFul for adding a resource. In the next step, we will be implementing these routes, but for now, we will add a route to remove a like from a cafe like this:

Route::delete('/cafes/{id}/like', 'API\CafesController@deleteLikeCafe');

This will delete a like from the cafe. Notice how we don’t have the user ID in the route? That’s fine because these routes are protected so we have to have a user authenticated before they can be called so we can grab the user ID in the implementation. For the “unliking” of a cafe we use the HTTP DELETE request which is RESTFul standards for deleting a resource which in this case is a like. Now it’s time to implement these routes!

Step 4: Implementing Liking and Unliking Routes

Laravel makes this implementation extremely easy with the built in Eloquent methods of attach and detach (more documentation here: Eloquent: Relationships – Laravel – The PHP Framework For Web Artisans ).

First we will open our /app/Http/Controllers/API/CafesController.php file where we will add our method handlers.

Let’s do the like handler first. To do this, we will add the following method:

public function postLikeCafe( $cafeID ){

}

This method accepts a cafe ID as the parameter. When we implemented, the many to many with the brew methods in this tutorial (https://serversideup.net/many-many-relationships-laravel/) we used a method called sync() which accepts an array of ids of the entity we want to sync to the relationship. If we don’t pass the entity in the array, it is removed from the table.

In this case, we want to attach a like to the cafe by the requesting user. We will be using the attach() method.

First we need to check if the relationship exists between the user and the cafe with a like. To do that, check to see if the user ID appears in the likes bound to the cafe:

$cafe = Cafe::where('id', '=', $cafeID)->first();

if( !$cafe->likes->contains( Auth::user()->id ) ){

}

If it does not, then we add the attach() code which attaches a like from the user to the cafe. Our final method should look like:

public function postLikeCafe( $cafeID ){
  $cafe = Cafe::where('id', '=', $cafeID)->first();

    /*
        If the user doesn't already like the cafe, attaches the cafe to the user's likes
    */
    $cafe->likes()->attach( Auth::user()->id, [
        'created_at'    => date('Y-m-d H:i:s'),
        'updated_at'    => date('Y-m-d H:i:s')
  ] );

  return response()->json( ['cafe_liked' => true], 201 );
}

This method returns the proper 201 when the resource has been liked.
We also attached more data for the created at and updated timestamps. This is a pretty cool feature of Laravel where you can attach extra data to a pivot table. You could attach anything based on a key, value array to store extra data in the pivot table.

Now we need to implement the functionality when a user unlikes a cafe. We first need to add this method:

public function deleteLikeCafe( $cafeID ){

}

This method will delete a cafe from a user’s likes. What we will do is utilize the detach() method to remove the cafe from the user’s likes and return an empty response with a status code of 204 which is RESTFul for no content, but successful method execution. Our final method should look like:

public function deleteLikeCafe( $cafeID ){
  $cafe = Cafe::where('id', '=', $cafeID)->first();

  $cafe->likes()->detach( Auth::user()->id );

  return response(null, 204);
}

Laravel’s Eloquent ORM makes this process extremely useful and the little information we are gathering can be aggregated into interesting results.

Now it’s time to implement this on the front end!

Step 5: Add Routes to cafe.js

We first need to open our /resources/assets/js/api/cafe.js file and add some routes for liking and un-liking the cafe.

First, we will add the route for liking the cafe so add the following to the bottom of your file:

/*
  POST  /api/v1/cafes/{cafeID}/like
*/
postLikeCafe: function( cafeID ){
  return axios.post( ROAST_CONFIG.API_URL + '/cafes/' + cafeID + '/like' );
},

This simply calls the URL to like a cafe through javascript. There are no parameters besides the cafe id in the URL. The rest is taken care of with the authentication token for the User ID we join to.

The next method we have to add calls our route to un-like a cafe:

/*
  DELETE /api/v1/cafes/{cafeID}/like
*/
deleteLikeCafe: function( cafeID ){
  return axios.delete( ROAST_CONFIG.API_URL + '/cafes/' + cafeID + '/like' );
}

Like the first method, the parameters are passed in the URL for the Cafe ID and anything else happens on the server side with grabbing the appropriate User ID.

Next, next we have to adjust a few things in our Vuex module and we should be ready to implement the functionality in a component!

Step 6: Update Vuex Cafe Module

We will need to add a few things to our Vuex Cafe module to keep track of the state of whether the user liked the cafe or not.

First, open /resources/assets/js/modules/cafes.js. In the state section, add two variables to watch the status of liking or un-liking a cafe:

cafeLikeActionStatus: 0,
cafeUnlikeActionStatus: 0

This will keep track of the status on how far along the request is to liking or un-liking a cafe is. We named it cafeLikeActionStatus and cafeUnlikeActionStatus because we should now add the cafeLiked piece of state to the module. This keeps track of if the cafe we are visiting is liked or not.

cafeLiked: false,

Now, let’s add the two actions that go along with our two routes:

/*
  Likes a cafe
*/
likeCafe( { commit, state }, data ){
  commit( 'setCafeLikeActionStatus', 1 );

  CafeAPI.postLikeCafe( data.id )
    .then( function( response ){
      commit( 'setCafeLikedStatus', true );
      commit( 'setCafeLikeActionStatus', 2 );
    })
    .catch( function(){
      commit( 'setCafeLikeActionStatus', 3 );
    });
},

/*
  Unlikes a cafe
*/
unlikeCafe( { commit, state }, data ){
  commit( 'setCafeUnlikeActionStatus', 1 );

  CafeAPI.deleteLikeCafe( data.id )
    .then( function( response ){
      commit( 'setCafeLikedStatus', false );
      commit( 'setCafeUnlikeActionStatus', 2 );
    })
    .catch( function(){
      commit( 'setCafeUnlikeActionStatus', 3 );
    });
}

Each action corresponds to the route we are calling. There are 2 different mutations in each route. The first one sets status of the cafe being liked or unliked in the call. The second, sets the like status. So when we are viewing a cafe page (which we will be implementing in the next step) we can display whether the cafe has been liked or not without re-loading the entire cafe.

Now let’s add the mutations to go along with the state and actions. There will be three mutations, the cafe like action status, the cafe unlike action status, and the cafe liked status. The mutations should look like:

/*
  Set the cafe liked status
*/
setCafeLikedStatus( state, status ){
  state.cafeLiked = status;
},

/*
  Set the cafe like action status
*/
setCafeLikeActionStatus( state, status ){
  state.cafeLikeActionStatus = status;
},

/*
  Set the cafe unlike action status
*/
setCafeUnlikeActionStatus( state, status ){
  state.cafeUnlikeActionStatus = status;
}

These should be added to the end of the mutations object.

Finally, let’s make a getter for each of the state we are adding like this:

/*
  Gets the cafe liked status
*/
getCafeLikedStatus( state ){
  return state.cafeLiked;
},

/*
  Gets the cafe liked action status
*/
getCafeLikeActionStatus( state ){
  return state.cafeLikeActionStatus;
},

/*
  Gets the cafe un-like action status
*/
getCafeUnlikeActionStatus( state ){
  return state.cafeUnlikeActionStatus;
}

We now have our Vuex module updated to support liking a cafe! Now it’s time to implement it!

Step 7: Update the Cafe.vue page

Now we haven’t really designed this page at all. However, I’m not going to dive into any complex designs, I’m just going to display the data for each cafe.

A few things to note about the Cafe.vue page. When we are creating the page, we load the cafe. We use the Vue Router to load the cafe based on the ID parameter in the route. To do that, you can access the route parameters like this:

this.$route.params.id

When we are on the cafe page, we have a parameter named ID, which allows us to load the specific cafe.

Our created method, will dispatch a loadCafe method to our Vuex store for the specific cafe:

created(){
  this.$store.dispatch( 'loadCafe', {
    id: this.$route.params.id
  });
},

The other thing we need to do, is display the map for where the cafe is located. For this, I created a component called IndividualCafeMap.vue which is way less complex than our big cafe map. It simply, takes the loaded cafe and displays it’s location on a map. The entire component is here:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div#individual-cafe-map{
    width: 700px;
    height: 500px;
    margin: auto;
    margin-bottom: 200px;
  }
</style>

<template>
  <div id="individual-cafe-map">

  </div>
</template>

<script>
  export default {
    /*
      Defines the computed properties on the component.
    */
    computed: {
      /*
        Gets the cafe load status from the Vuex state.
      */
      cafeLoadStatus(){
        return this.$store.getters.getCafeLoadStatus;
      },

      /*
        Gets the cafe from the Vuex state.
      */
      cafe(){
        return this.$store.getters.getCafe;
      }
    },

    /*
      Defines the variables we need to watch on the component.
    */
    watch: {
      /*
        The cafe load status. When the cafe load status equals 2
        we display the individual cafe map. We have to wait until the
        cafe is loaded so we get the lat and long for the cafe.
      */
      cafeLoadStatus(){
        if( this.cafeLoadStatus == 2 ){
          this.displayIndividualCafeMap();
        }
      }
    },

    /*
      Defines the methods used by the component.
    */
    methods: {
      /*
        Displays the individual cafe map.
      */
      displayIndividualCafeMap(){
        /*
          Builds the individual cafe map.
        */
        this.map = new google.maps.Map(document.getElementById('individual-cafe-map'), {
          center: {lat: parseFloat( this.cafe.latitude ), lng: parseFloat( this.cafe.longitude )},
          zoom: 13
        });

        /*
          Defines the image used for the marker.
        */
        var image = '/img/coffee-marker.png';

        /*
          Builds the marker for the cafe on the map.
        */
        var marker = new google.maps.Marker({
          position: { lat: parseFloat( this.cafe.latitude ), lng: parseFloat( this.cafe.longitude )},
          map: this.map,
          icon: image
        });
      }
    }
  }
</script>

I import the component into the Cafe.vue component and use it to display the map. Here’s what the Cafe.vue page should look like when we are finished for now:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div.cafe-page{
    h2{
      text-align: center;
      color: $primary-color;
      font-family: 'Josefin Sans', sans-serif;
    }

    h3{
      text-align: center;
      color: $secondary-color;
      font-family: 'Josefin Sans', sans-serif;
    }

    span.address{
      text-align: center;
      display: block;
      font-family: 'Lato', sans-serif;
      color: #A0A0A0;
      font-size: 20px;
      line-height: 30px;
      margin-top: 50px;
    }

    a.website{
      text-align: center;
      color: $dull-color;
      font-size: 30px;
      font-weight: bold;
      margin-top: 50px;
      display: block;
      font-family: 'Josefin Sans', sans-serif;
    }

    div.brew-methods-container{
      max-width: 700px;
      margin: auto;

      div.cell{
        text-align: center;
      }
    }
  }
</style>

<template>
  <div id="cafe" class="page">

    <div class="grid-container">
      <div class="grid-x grid-padding-x">

        <div class="large-12 medium-12 small-12 cell">
          <loader v-show="cafeLoadStatus == 1"
                  :width="100"
                  :height="100"></loader>

          <div class="cafe-page" v-show="cafeLoadStatus == 2">
            <h2>{{ cafe.name }}</h2>
            <h3 v-if="cafe.location_name != ''">{{ cafe.location_name }}</h3>

            <span class="address">
              {{ cafe.address }}<br>
              {{ cafe.city }}, {{ cafe.state }}<br>
              {{ cafe.zip }}
            </span>

            <a class="website" v-bind:href="cafe.website" target="_blank">{{ cafe.website }}</a>

            <div class="brew-methods-container">
              <div class="grid-x grid-padding-x">
                <div class="large-3 medium-4 small-12 cell" v-for="brewMethod in cafe.brew_methods">
                  {{ brewMethod.method }}
                </div>
              </div>
            </div>

            <br>

            <individual-cafe-map></individual-cafe-map>
          </div>
        </div>

      </div>
    </div>

  </div>
</template>

<script>
  /*
    Import the loader and cafe map for use in the component.
  */
  import Loader from '../components/global/Loader.vue';
  import IndividualCafeMap from '../components/cafes/IndividualCafeMap.vue';

  export default {
    /*
      Defines the components used by the page.
    */
    components: {
      Loader,
      IndividualCafeMap
    },

    /*
      When created, load the cafe based on the ID in the
      route parameter.
    */
    created(){
      this.$store.dispatch( 'loadCafe', {
        id: this.$route.params.id
      });
    },

    /*
      Defines the computed variables on the cafe.
    */
    computed: {
      /*
        Grabs the cafe load status from the Vuex state.
      */
      cafeLoadStatus(){
        return this.$store.getters.getCafeLoadStatus;
      },

      /*
        Grabs the cafe from the Vuex state.
      */
      cafe(){
        return this.$store.getters.getCafe;
      }
    }
  }
</script>

We are now ready to add our like button and implement the front end functionality!

Step 8: Add Cafe Toggle Like Component

This component will be a simple element we can insert into the individual cafe page to like or unlike the cafe. I wanted both of these functionalities in one simple to use component, so I toggle the display based off of the status of liking or unliking the cafe. I also threw in a loader so until the server responds with status of the like or unlike we can show the progress. For this, I made a minor change to the loader component to accept a display property that allows it to be displayed inline with a default of block. The property object for the /resources/assets/js/components/global/Loader.vue component should look like:

props: {
  'width': Number,
  'height': Number,
  'display': {
    default: 'block'
  }
}

Now we can pass in whether we want it to be an inline component or a block level component in css. I made the changes in the template like so:

<div class="loader loader--style3" v-bind:style="'width: '+width+'px; height: '+height+'px; display: '+display+''" title="2">
  <svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     v-bind:width="width+'px'" v-bind:height="height+'px'" viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
  <path fill="#000" d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z">
    <animateTransform attributeType="xml"
      attributeName="transform"
      type="rotate"
      from="0 25 25"
      to="360 25 25"
      dur="0.6s"
      repeatCount="indefinite"/>
    </path>
  </svg>
</div>

Now on to our toggle like component. When we click like, we send the command to like the cafe. When the like is processing by the server, we display the loader. When the processing is complete, we display the unlike button.

First, create the file: /resources/assets/js/components/cafes/ToggleLike.vue. In the file put the stubbed code for a component like so:

<style>

</style>
<template>
  <span class="toggle-like">

  </span>
</template>
<script>
  export default {

  }
</script>

Now let’s implement the like portion of the component. We will need to add a like button, so add this to the template piece of the component:

<template>
  <span class="toggle-like">
    <span class="like" v-on:click="likeCafe( cafe.id )" v-if="!liked && cafeLoadStatus == 2 && cafeLikeActionStatus != 1 && cafeUnlikeActionStatus != 1">
      Like
    </span>
  </span>
</template>

As you can see there’s a few other things we need to do. Right now it’s a span tag that displays when certain conditions are met and fires the likeCafe method when clicked. Let’s make sure we have everything we need in our component. We will need to start by grabbing a few pieces of data from the Vuex Store:

computed: {
  /*
    Gets the cafe load status from the Vuex state.
  */
  cafeLoadStatus(){
    return this.$store.getters.getCafeLoadStatus;
  },

  /*
    Gets the cafe from the Vuex state.
  */
  cafe(){
    return this.$store.getters.getCafe;
  },

  /*
    Determines if the cafe is liked or not.
  */
  liked(){
    return this.$store.getters.getCafeLikedStatus;
  },

  /*
    Determines if the cafe is still processing the like action.
  */
  cafeLikeActionStatus(){
    return this.$store.getters.getCafeLikeActionStatus;
  },

  /*
    Determines if the cafe is still processing the un-like action.
  */
  cafeUnlikeActionStatus(){
    return this.$store.getters.getCafeUnlikeActionStatus;
  }
},

In this, we grab the cafe load status which helps determine if the cafe has been loaded yet, the cafe data, which determines which cafe to like. The liked data which is a boolean flag on whether the cafe has been liked or not. Then the 2 action statuses for the cafe like action status and the un like action status. These action status help determine the processing part of the component.

When you look back at the like button:

<span class="like" v-on:click="likeCafe( cafe.id )" v-if="!liked && cafeLoadStatus == 2 && cafeLikeActionStatus != 1 && cafeUnlikeActionStatus != 1">
    Like
</span>

you will see that it only displays if the cafe has not been liked (since this button will like the cafe), if the cafe has been loaded (since we need to like a cafe, not null 😉 ), and if the cafe is not in the process of being unliked or liked since that would get the visual out of sync with the data.

Now, we need to add our method to like the cafe to our methods object:

likeCafe( cafeID ){
  this.$store.dispatch( 'likeCafe', {
    id: this.cafe.id
  });
},

This dispatches the the likeCafe method with the cafe to our Vuex component. The reactivity and the way we set up our component in Step 6 will handle the updating of the state of the method.

Next, add the unlike button:

<span class="un-like" v-on:click="unlikeCafe( cafe.id )" v-if="liked && cafeLoadStatus == 2 && cafeLikeActionStatus != 1 && cafeUnlikeActionStatus != 1">
  Un-like
</span>

Very similar to the like button, the unlike button unbinds the like the user submitted. The difference is it displays if the cafe is liked. If the cafe is already liked, the only action for the user is to unlike the cafe.

We now need to add the unlike method to our methods object:

unlikeCafe( cafeID ){
  this.$store.dispatch( 'unlikeCafe', {
    id: this.cafe.id
  });
}

When the unlike button is clicked, we dispatch the command to unlike the cafe.

Finally, we just need to add our loader to the mix, so make sure you include the global Loader.vue component from /resources/assets/js/components/global/Loader.vue and add the following to the template:

<loader v-show="cafeLikeActionStatus == 1 || cafeUnlikeActionStatus == 1"
        :width="30"
        :height="30"
        :display="'inline-block'">

This will display the loader as an inline-block element and will only show when an action is taking place whether it is liking or unliking a cafe. This is also our first user interactions on any elements, definitely moving towards the use of user profiles within the application 🙂 Our ToggleLike.vue component should look like:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  span.toggle-like{
    display: block;
    text-align: center;
    margin-top: 30px;

    span.like-toggle{
      display: inline-block;
      font-weight: bold;
      text-decoration: underline;
      font-size: 20px;
      cursor: pointer;

      &.like{
        color: $dark-success;
      }

      &.un-like{
        color: $dark-failure;
      }
    }
  }
</style>
<template>
  <span class="toggle-like">
    <span class="like like-toggle" v-on:click="likeCafe( cafe.id )" v-if="!liked && cafeLoadStatus == 2 && cafeLikeActionStatus != 1 && cafeUnlikeActionStatus != 1">
      Like
    </span>
    <span class="un-like like-toggle" v-on:click="unlikeCafe( cafe.id )" v-if="liked && cafeLoadStatus == 2 && cafeLikeActionStatus != 1 && cafeUnlikeActionStatus != 1">
      Un-like
    </span>
    <loader v-show="cafeLikeActionStatus == 1 || cafeUnlikeActionStatus == 1"
            :width="30"
            :height="30"
            :display="'inline-block'">
    </loader>
  </span>
</template>
<script>
  import Loader from '../global/Loader.vue';

  export default {
    components: {
      Loader
    },

    computed: {
      /*
        Gets the cafe load status from the Vuex state.
      */
      cafeLoadStatus(){
        return this.$store.getters.getCafeLoadStatus;
      },

      /*
        Gets the cafe from the Vuex state.
      */
      cafe(){
        return this.$store.getters.getCafe;
      },

      /*
        Determines if the cafe is liked or not.
      */
      liked(){
        return this.$store.getters.getCafeLikedStatus;
      },

      /*
        Determines if the cafe is still processing the like action.
      */
      cafeLikeActionStatus(){
        return this.$store.getters.getCafeLikeActionStatus;
      },

      /*
        Determines if the cafe is still processing the un-like action.
      */
      cafeUnlikeActionStatus(){
        return this.$store.getters.getCafeUnlikeActionStatus;
      }
    },

    /*
      Defines the methods used by the component.
    */
    methods: {
      /*
        Likes the cafe specified by the id
      */
      likeCafe( cafeID ){
        this.$store.dispatch( 'likeCafe', {
          id: this.cafe.id
        });
      },

      /*
        Un-likes the cafe specified by the id
      */
      unlikeCafe( cafeID ){
        this.$store.dispatch( 'unlikeCafe', {
          id: this.cafe.id
        });
      }
    }
  }
</script>

Step 9: Ensure We Load State When Loading Cafe

This is the final step in liking a cafe. We have to ensure that when we load the cafe, we load whether the user has already liked it or not. First, we need to open our /app/Models/Cafe.php file and add another relationship for the user liking the Cafe:

public function userLike(){
  return $this->belongsToMany( 'App\Models\User', 'users_cafes_likes', 'cafe_id', 'user_id')->where('user_id', auth()->id());
}

What this does is state that the relationship is many to many but only return the relationship for the user logged in. This allows us to return whether the logged in user likes or hasn’t liked the cafe, so we can initialize our state correctly.

Next, open up the /app/Http/Controllers/API/CafesController.php and adjust the getCafe($id) method to load the user like relationship:

/*
|-------------------------------------------------------------------------------
| Get An Individual Cafe
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}
| Method:         GET
| Description:    Gets an individual cafe
| Parameters:
|   $id   -> ID of the cafe we are retrieving
*/
public function getCafe( $id ){
  $cafe = Cafe::where('id', '=', $id)
              ->with('brewMethods')
              ->with('userLike')
              ->first();

  return response()->json( $cafe );
}

We now return back whether the user likes a cafe or not when we initially load the cafe.

Finally, we open the /resources/assets/js/modules/cafes.js module and find the loadCafe() method. In this method, is where we load the cafe from our API and set the data accordingly. We also, in the module, have a method to set whether the cafe is liked or not, which is read from our ToggleLike.vue component.

Adjust this method to function like this:

/*
  Loads an individual cafe from the API
*/
loadCafe( { commit }, data ){
    commit( 'setCafeLikedStatus', false );
  commit( 'setCafeLoadStatus', 1 );

  CafeAPI.getCafe( data.id )
    .then( function( response ){
      commit( 'setCafe', response.data );
      if( response.data.user_like.length > 0 ){
        commit('setCafeLikedStatus', true);
      }
      commit( 'setCafeLoadStatus', 2 );
    })
    .catch( function(){
      commit( 'setCafe', {} );
      commit( 'setCafeLoadStatus', 3 );
    });
},

What this does is sets the cafe liked status to true if there is more than one user_like for the cafe. Since the only time there is more than one user like for the cafe is if the authenticated user liked the cafe, this should be accurate. We also, reset the cafe liked status to false on load, so it doesn’t stay there for a cafe that isn’t liked.

Conclusion

We now have implemented a fully functioning like system into our application. With the like system implemented, we are now generating user data which means that in future tutorials we can start diving into user profiles which is exciting! This is a little longer of a tutorial, but hopefully it helps! Feel free to reach out at any time if there are any questions and make sure to check out the source code here: GitHub – serversideup/roastandbrew: Helping the coffee enthusiast find their next cup of coffee. Also, helping aspiring web and mobile app developers build a single page app and converting it to a mobile hybrid. All tutorials can be found at https://serversideup.net

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Tagging With Laravel

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.