Tagging With Laravel

Part 25 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori November 27th, 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.

We’ve done a few many to many eloquent relationship tutorials, all with different use-cases. However, there’s one more that we should accomplish. That’s the ability to tag a coffee shop. Luckily, tagging with Laravel is as easy as the other solutions.

We are going to end up building a tag cloud for each coffee shop based on the number of tags the users tag each coffee shop with. I’ll moving through the basics of the Eloquent many-to-many set up and just provide the code and a brief description, but if you want more information on how to do a many-to-many relationship with Laravel and Eloquent, read https://serversideup.net/many-many-relationships-laravel/ or https://serversideup.net/favoriting-liking-laravel-vuejs/. Both go through a few many-to-many scenarios. However, we will be doing a few different functions with tagging so I’ll make sure to explain those in detail.

With the tagging implemented, we will have lots of ways to filter the cafes and build a sweet sorting system for cafes on the home screen.

Step 1: Create Your Tags Table

This table is where we will be storing the tags. We will want each tag to have an id and be unique.

First, connect to your server and run php artisan make:migration added_tags_table --create=tags. Notice the —create=tags flag. This automatically adds the create method with the name tags for the table.

Your tags migration up() method should look like:

Schema::create('tags', function (Blueprint $table) {
    $table->increments('id');
    $table->string('tag')->unique();
    $table->timestamps();
});

We added a ->unique() flag to the tag field so we don’t get replicated tag names. Just for some added data validatity.

Step 2: Create The Cafes Tags Join Table

So this table will be a little unique compared to the last join tables we have done. It will be a triple join. Since we are creating a tag cloud (or ranked tags, whatever you want to call them) we don’t want the user to submit multiple tags on a coffee shop that are the same. We will have to ensure that a user can only add a tag once.

First, we will need to create our migration, so run php artisan make:migration added_cafes_users_tags --create=cafes_users_tags

The up() method should look like:

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

Notice we have a triple primary key. This will ensure the data is valid and the user cannot add the same tag more than once. This is a good practice for anyone sort of tagging mechanism so data stays valid. If you don’t need to know who tagged the cafe, then you can leave out the user_id field.

Step 3: Add Tags Model

We will need to add a tags model so we can set up all of the relations.

First, we should add the file /app/Models/Tag.php.

In here, we just need to set up the relationship to have many cafes like so:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
  protected $fillable = [
      'name'
  ];

  public function cafes(){
    return $this->belongsToMany( 'App\Models\Cafe', 'cafes_users_tags', 'tag_id', 'user_id');
  }
}
?>

This states that there are many cafes for each tag. This should be all the relations we need. We can do it to users as well, but I don’t know what we would need it for now. If we need to add it later, we always can.

Step 4: Add The Relation For Cafes

We need to add the tags relationship to the Cafes model.

We just need to open up the /app/Models/Cafe.php file and add the following method:

public function tags(){
  return $this->belongsToMany( 'App\Models\Tag', 'cafes_users_tags', 'cafe_id', 'tag_id');
}

This associates the tags with the cafe. We could add the relations to tags on the user, but once again, I don’t see a use for it right now. Maybe in the future we will see which tags the user usually tags a coffee shop with and we will add it then.

Step 5: Add The Cafe Tagging Routes

We need to add a two routes for tagging coffee shops. One route will add tags to a cafe, the other will remove a tag. Both of these will reference the authenticated user. For example User A and User B add a tag of good-for-dates. There will be a reference for each user and the good-for-dates tag in the database. If the User A calls a DELETE method for a tag on a cafe, then we will remove only User A’s tag. Let’s begin!

First we will open the app/routes/api.php file. In here, we will add the POST and DELETE methods for the tags like so:

/*
|-------------------------------------------------------------------------------
| Adds Tags To A Cafe
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}/tags
| Controller:     API\CafesController@postAddTags
| Method:         POST
| Description:    Adds tags to a cafe for a user
*/
Route::post('/cafes/{id}/tags', 'API\CafesController@postAddTags');

/*
|-------------------------------------------------------------------------------
| Deletes A Cafe Tag
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}/tags/{tagID}
| Controller:     API\CafesController@deleteCafeTag
| Method:         DELETE
| Description:    Deletes a tag from a cafe for a user
*/
Route::delete('/cafes/{id}/tags/{tagID}', 'API\CafesController@deleteCafeTag');

These routes allow us to manage our tagging. Let’s now shell out our controllers.

Step 6: Implement Tag Controller Routes

These are the routes that will handle the tagging of each cafe and deletion of a tag with respect to the user on each cafe.

First, open the /app/Http/Controllers/API/CafesController.php file and add the stubs for the two methods:

/*
|-------------------------------------------------------------------------------
| Adds Tags To A Cafe
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}/tags
| Controller:     API\CafesController@postAddTags
| Method:         POST
| Description:    Adds tags to a cafe for a user
*/
public function postAddTags( $cafeID ){

}

/*
|-------------------------------------------------------------------------------
| Deletes A Cafe Tag
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}/tags/{tagID}
| Method:         DELETE
| Description:    Deletes a tag from a cafe for a user
*/
public function deleteCafeTag( $cafeID, $tagID ){

}

Before we get started, I’d like to point out that having a route for tags is awesome, but sometimes you will want to add tags when you add or edit a cafe as well. This is a unique API design scenario since you want a route to handle a single function, but you don’t want to make multiple calls and you don’t want to repeat code. So what we will do before we shell out our routes is to make a Utility class that handles the tagging of the cafe.

We essentially funnel this responsibility to a single method so it acts the same every time we need it and we can update it as a single point of responsibility and it will keep everything in sync.

First, let’s add a file app\Utilities\Tagger.php and add the following stub of a method:

<?php

namespace App\Utilities;

use App\Models\Tag;

class Tagger{

  public static function tagCafe( $cafe, $tags ){

  }
}

We will have be using our Tag model so we define it at the top.

In our POST request we will will accept an array of tags “strings” like this:

['tasty', 'good-for-dates', 'calm-atmosphere']

These strings will be in a POST variable named tags and will be in an array. We will loop over the array and build a new tag with each string or find the tag that already exists. If a user submits the tag of tasty but the tag exists, we don’t want to create another tag with the same name. Luckily Laravel has us covered.

In our tagCafe() method in the app\Utilities\Tagger.php , we will begin by looping over the array of tags. Then we will use Eloquent’s firstOrNew() method when retrieving the tag. This is a super slick method that simply finds an entry that matches the parameter in the query or creates a new object. This way we always have an object we can work with when we are tagging the cafe. Our code should look like this right now:

<?php

namespace App\Utilities;

use App\Models\Tag;

class Tagger{

  public static function tagCafe( $cafe, $tags ){
    /*
      Iterate over all of the tags attaching each one
      to the cafe.
    */
    foreach( $tags as $tag ){
      /*
        Get the tag by name or create a new tag.
      */
      $newCafeTag     = \App\Models\Tag::firstOrNew( array('name' => $tag ) );
    }
  }
}

Now when the user submits these tags, we need to assume that the tag has issues in formatting. We don’t want symbols or spaces, we want everything to be lower case, and we don’t want the tag to start or end with a - or whitespace.

So before we create a new tag or query by the tag name we will need to convert the name to a similar naming scheme.

First we will trim off any of the leading or trailing white space, then make all characters lower case. Next, we will replace anything that’s not a letter or number to a -. We will need a regular expression to handle that, then we will replace any occurrence of more than one -– with one - and trim out any remaining -. We will then have a valid tag!

We will add a new private static method called generateTagName( $name ) to the Tagger object and the method should look like:

private static function generateTagName( $tagName ){
  /*
    Trim whitespace from beginning and end of tag
  */
  $name = trim( $tagName );

  /*
    Convert tag name to lower.
  */
  $name = strtolower( $name );

  /*
    Convert anything not a letter or number to a dash.
  */
  $name = preg_replace( '/[^a-zA-Z0-9]/', '-', $name );

  /*
    Remove multiple instance of '-' and group to one.
  */
  $name = preg_replace( '/-{2,}/', '-', $name );
  /*
    Get rid of leading and trailing '-'
  */
  $name = trim( $name, '-' );

  /*
    Returns the cleaned tag name
  */
  return $name;
}

We will then update our tagCafe() method to utilize this new method like so:

public static function tagCafe( $cafe, $tags ){
  /*
    Iterate over all of the tags attaching each one
    to the cafe.
  */
  foreach( $tags as $tag ){
    /*
      Generates a tag name for the entered tag
    */
    $name = self::generateTagName( $tag );


    /*
      Get the tag by name or create a new tag.
    */
    $newCafeTag     = \App\Models\Tag::firstOrNew( array('name' => $name ) );
  }
}

Now we just need to finish attaching the tag to the cafe in our utility after the firstOrNew method:

/*
  Confirm the cafe tag has the name we provided. If it's set already
  because the tag exists, this won't make a difference.
*/
$newCafeTag->tag = $name;

/*
  Save the tag
*/
$newCafeTag->save();

/*
  Apply the tag to the cafe
*/
$cafe->tags()->syncWithoutDetaching( [ $newCafeTag->id => ['user_id' => Auth::user()->id ] ] );

We add the tag name, then save the tag. If we are getting the tag that already exists, the name won’t change. Finally we sync without detaching the tag to the cafe and include the user_id. This is important because on our pivot table we are keeping track of the user that added the tag. We also don’t want to remove old tags, so we build a tags array with the extra user_id information and then after we build that, we sync it all to the cafe without detaching. Our final method should look like:

public static function tagCafe( $cafe, $tags ){
  /*
    Iterate over all of the tags attaching each one
    to the cafe.
  */
  foreach( $tags as $tag ){
    /*
      Generates a tag name for the entered tag
    */
    $name = self::generateTagName( $tag );

    /*
      Get the tag by name or create a new tag.
    */
    $newCafeTag     = \App\Models\Tag::firstOrNew( array('tag' => $name ) );

    /*
      Confirm the cafe tag has the name we provided. If it's set already
      because the tag exists, this won't make a difference.
    */
    $newCafeTag->tag = $name;

    /*
      Save the tag
    */
    $newCafeTag->save();

    /*
      Apply the tag to the cafe
    */
    $cafe->tags()->syncWithoutDetaching( [ $newCafeTag->id => ['user_id' => Auth::user()->id ] ] );
  }
}

Finally, let’s go back to our /app/Http/Controllers/API/CafesController.php file and add the use App\Utilities\Tagger on top of the file. Then in our postAddTags() method, add the following lines of code to finish up the method:

/*
|-------------------------------------------------------------------------------
| Adds Tags To A Cafe
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}/tags
| Controller:     API\CafesController@postAddTags
| Method:         POST
| Description:    Adds tags to a cafe for a user
*/
public function postAddTags( $cafeID ){
  /*
    Grabs the tags array from the request
  */
  $tags = Request::get('tags');

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

  /*
    Tags the cafe
  */
  Tagger::tagCafe( $cafe, $tags );

  /*
    Grabs the cafe with the brew methods, user like and tags
  */
  $cafe = Cafe::where('id', '=', $cafeID)
              ->with('brewMethods')
              ->with('userLike')
              ->with('tags')
              ->first();

  /*
    Returns the cafe response as JSON.
  */
  return response()->json($cafe, 201);
}

We now finished our postAddTags() method, so we have to implement the deleteCafeTag() method which will simply remove a tag from a cafe. This is a much easier method since we will just be deleting a record.

We will be using the DB query builder facade for this since we have 3 parameters we are deleting by. First, at the top of the the CafesController.php file, add the following:

use DB;

Now in our deleteCafeTag() method, we add the following lines of code:

/*
|-------------------------------------------------------------------------------
| Deletes A Cafe Tag
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes/{id}/tags/{tagID}
| Method:         DELETE
| Description:    Deletes a tag from a cafe for a user
*/
public function deleteCafeTag( $cafeID, $tagID ){
  DB::statement('DELETE FROM cafes_users_tags WHERE cafe_id = `'.$cafeID.'` AND tag_id = `'.$tagID.'` AND user_id = `'.Auth::user()->id.'`');

  /*
    Return a proper response code for successful untagging
  */
  return response(null, 204);
}

This will remove the specific tag and return a null response with a 204 successful deletion code.

Step 7: Add Tag Autocompletion Route

We will want users to have the ability to autocomplete tags that have already been added as well. This will make for a much more friendly UX.

First open your routes/api.php and add the following route:
First, we will need to add a simple API route. First open the /routes/api.php file and add the following route:

/*
|-------------------------------------------------------------------------------
| Search Tags
|-------------------------------------------------------------------------------
| URL:            /api/v1/tags
| Controller:     API\TagsController@getTags
| Method:         GET
| Description:    Searches the tags if a query is set otherwise returns all tags
*/
Route::get('/tags', 'API\TagsController@getTags');

In this route, we will return all tags if the query is not set or blank, or we will query by matching tag name.

Now we need to create the \app\Http\Controllers\API\TagsController.php file. This file will handle our new search route. Right off of the bat the file should look like:

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;

use App\Models\Tag;
use Request;

class TagsController extends Controller
{
  public function getTags(){

  }
}

Now we just need to shell out the functionality to either return all of the tags if there is no query or what matches the tags query if there is one present.

Our method should now look like:

public function getTags(){
    $query = Request::get('search');

    /*
      If the query is not set or is empty, load all the tags.
      Otherwise load the tags that match the query
    */
    if( $query == null || $query == '' ){
      $tags = Tag::all();
    }else{
      $tags = Tag::where('tag', 'LIKE', $query.'%')->get();
    }

    /*
      Return all of the tags as JSON
    */
    return response()->json( $tags );
  }

We will be receiving all of our tag results as JSON.

Step 8: Update Add Cafes Route To Handle Tagging

Since we now have the ability to tag a cafe, we need to add that functionality to the to the new cafe route.

First, open up the app/Http/Controllers/API/CafesController.php file and find the postNewCafe() method. After we grab the brew methods from the first location, grab the tags for the location:

$tags = $locations[0]['tags'];

Then, right after we sync the brew methods, make sure the tag cafe method is called:

/*
  Attach the brew methods
*/
$parentCafe->brewMethods()->sync( $brewMethods );

/*
  Tags the cafe
*/
Tagger::tagCafe( $parentCafe, $tags );

Now this tags the parent cafe, we just have to do this for the child locations.

Similar to tagging the parent cafe, we need to add the tag cafe method after we sync the brew methods for the child cafe:

$tags = $locations[$i]['tags'];

/*
  Tags the cafe
*/
Tagger::tagCafe( $cafe, $tags );

This allows us to pass an array of tags to the add cafe route and tag the individual cafe.

Conclusion

We now have our tagging structure completed. In the next tutorial, we will build a tagging component with VueJS which has it’s own special features. Be 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 and feel free to ask any questions!

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Implementing the Vue JS Tag Component

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.