Tagging With Laravel
Part 25 of 48 in API Driven Development With Laravel and VueJSWe’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!