Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A solution for adding tags in Laravel

In a form, I have a Tags field, which is a just a standard text field. A user can type a Tag name and it is added to the article.

I already have three tables: tags, taggables and articles and they are linked via Eloquent relationship methods, given the setup in a previous question I asked.

This is my update method in my ArticleController

/**
 * Update the specified resource in storage.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  int  $id
 * @return \Illuminate\Http\Response
 */
public function update(Request $request, $id)
{
    $validatedData = $request->validate([
        'title' => 'required',
        'excerpt' => 'required',
    ]);

    $article = Article::find($id);

    $article->title = $request->get('title');
    $article->author = $request->get('author');
    $article->category = $request->get('category');
    $article->excerpt = $request->get('excerpt');
    $article->content = $request->get('content');
    $article->featuredImage = $request->get('featuredImage');
    $article->featuredVideo = $request->get('featuredVideo');
    $article->readingTime = $request->get('readingTime');
    $article->published = $request->get('published');

    $article->save();

    /**
     * Once the article has been saved, we deal with the tag logic.
     * Grab the tag or tags from the field, sync them with the article
     */
    $tags = $request->get('tags');
    $comma = ',';

    if (!empty($tags)) {
        if (strpos($tags, $comma) !== false) {
            $tagList = explode(",", $tags);

            // Loop through the tag array that we just created
            foreach ($tagList as $tags) {

                // Get any existing tags
                $tag = Tag::where('name', '=', $tags)->first();

                // If the tag exists, sync it, otherwise create it
                if ($tag != null) {
                    $article->tags()->sync($tag->id);
                } else {
                    $tag = new Tag();

                    $tag->name = $tags;
                    $tag->slug = str_slug($tags);

                    $tag->save();

                    $article->tags()->sync($tag->id);
                }
            }
        } else {
            // Only one tag
            $tag = Tag::where('name', '=', $tags)->first();

            if ($tag != null) {
                $article->tags()->sync($tag->id);
            } else {
                $tag = new Tag();

                $tag->name = $tags;
                $tag->slug = str_slug($tags);

                $tag->save();

                $article->tags()->sync($tag->id);
            }
        }
    }

    return back();
    return redirect()->back();
}

In the section of this method that looks for tags, I do the following things:

  1. Check if the field is not empty
  2. Check if the string sent contains a comma
  3. If there is a comma, I use explode() to convert the string into an array
  4. Loop through the array to see if the given tag within the string already exists
  5. If it doesn't exist, I create it and then sync it with the Article, otherwise I just sync it

This approach feels very messy, however, is there any way I could make this cleaner?

An update, given the answers provided

I went with the following approach:

/**
 * Store a newly created resource in storage.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function store(Request $request)
{
    $validatedData = $request->validate([
        'title' => 'required',
        'excerpt' => 'required',
    ]);

    $article = new Article();

    $article->title = $request->get('title');
    $article->author = $request->get('author');
    $article->category = $request->get('category');
    $article->excerpt = $request->get('excerpt');
    $article->content = $request->get('content');
    $article->featuredImage = $request->get('featuredImage');
    $article->featuredVideo = $request->get('featuredVideo');
    $article->readingTime = $request->get('readingTime');
    $article->published = $request->get('published');

    //If no featured image set, automatically create featured image placeholder
    if ($request->get('featuredImage') == null) {
        $article->featuredImage = "http://via.placeholder.com/350x150";
    }

    $article->save();

    // Handle Tags
    $tags = $request->get('tags');

    if (!empty($tags)) {
        $tagList = array_filter(explode(",", $tags));

        // Loop through the tag array that we just created
        foreach ($tagList as $tags) {
            $tag = Tag::firstOrCreate(['name' => $tags, 'slug' => str_slug($tags)]);
        }

        $tags = Tag::whereIn('name', $tagList)->get()->pluck('id');

        $article->tags()->sync($tags);
    }

    return redirect('editable/news-and-updates')->with('success', 'Article has been added');
}

And then, to display the tags when updating, I did the following:

/**
 * Show the form to edit this resource
 */
public function edit($id)
{
    $user = auth::user();
    $article = Article::find($id);

    // Get the tags associated with this article and convert to a comma seperated string
    if ($article->has('tags')) {
        $tags = $article->tags->pluck('name')->toArray();

        $tags = implode(', ', $tags);
    } else {
        $tags = "";
    }

    return view('editable.news.edit', compact('article', 'user', 'tags'));
}

Essentially, I just grab the tags associated with the article, convert them to an array, then use implode().

This gives me the tags as a comma separated list in the tags field like:

blue, red, orange

However, when updating, If I try to save with the same tags in the field I get:

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'sauce' for key 'tags_slug_unique' (SQL: insert intotags(name,slug,updated_at,created_at) values ( sauce, sauce, 2018-05-26 11:42:17, 2018-05-26 11:42:17))

Here is the tag migration for reference:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTagsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('slug')->unique();
            $table->timestamps();
        });

        Schema::create('taggables', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('tag_id')->unsigned();
            $table->integer('taggable_id')->unsigned();
            $table->string('taggable_type');

            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('taggables');
        Schema::dropIfExists('tags');
    }
}
like image 773
Jesse Orange Avatar asked May 25 '18 12:05

Jesse Orange


3 Answers

Maybe something like this, typed just from head, not tested, but i hope it kinda give you an idea

public function update(Request $request, Article $article)
{
    $validatedData = $request->validate([
        'title' => 'required',
        'excerpt' => 'required',
        // Make validation for all inputs !
    ]);

    // Fill model with inputs and save (make sure that inputs are in fillable model property)
    $article->fill($request->all())->save();

    // Handle Tags 
    $this->handleTags($request, $article);

    // Return Redirect to previous URL
    return redirect()->back();
}

/**
 * Handle Tags for Article
 * @param  \Illuminate\Http\Request  $request
 * @param \App\Article $article
 * @return void
 */
public function handleTags(Request $request, Article $article){
    /**
     * Once the article has been saved, we deal with the tag logic.
     * Grab the tag or tags from the field, sync them with the article
     */
    $tagsNames = explode(',', $request->get('tags'));

    // Create all tags (unassociet)
    foreach($tagsNames as $tagName){
        Tag::firstOrCreate(['name' => $tagName, 'slug' => str_slug($tagName)])->save();
    }

    // Once all tags are created we can query them
    $tags = Tag::whereIn('name', $tagsNames)->get()->pluck('id')->get();
    $article->tags()->sync($tags);
}
like image 136
Lukáš Irsák Avatar answered Oct 23 '22 01:10

Lukáš Irsák


There is really no need to check if there are commas and have two different paths. If there are no commas, explode will return one element to iterate over. You can literally just remove the if and the else.

$tagList = explode(",", $tags);

// Loop through the tag array that we just created
foreach ($tagList as $tags) {

    // Get any existing tags
    $tag = Tag::where('name', '=', $tags)->first();

    // If the tag exists, sync it, otherwise create it
    if ($tag != null) {
        $article->tags()->sync($tag->id);
    } else {
        $tag = new Tag();

        $tag->name = $tags;
        $tag->slug = str_slug($tags);

        $tag->save();

        $article->tags()->sync($tag->id);
    }
}

Additionally, there is the ability to do firstOrCreate which you can see the documentation for here.

The firstOrCreate method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the attributes from the first parameter, along with those in the optional second parameter.

This can be use to refactor code to the following:

$tagList = explode(",", $tags);

// Loop through the tag array that we just created
foreach ($tagList as $tags) {
    $tag = Tag::firstOrCreate(['slug' => $tags];
}

$tags = Tag::whereIn('name', $tagList)->get()->pluck('id')->get();
$article->tags()->sync($tags);
like image 3
Alex Harris Avatar answered Oct 23 '22 00:10

Alex Harris


I think the Easiest way to do tagging is by using many-to-many polymorphic relationship... morphedByMany() and morphToMany(). See this example code...

In migration their are 3 tables articles, tags, taggables

# --- for Article Table ---
public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        // ---
    });
}
# --- for Tags Table ---
public function up()
{
    Schema::create('tags', function (Blueprint $table) {
        $table->increments('id');
        $table->string('tagname');
    });
}

# --- for Taggables Table ---
public function up()
{
    Schema::create('taggables', function (Blueprint $table) {
        $table->integer('tag_id');
        $table->integer('taggable_id'); // for storing Article ID's
        $table->string('taggable_type'); // Aside from Article, if you decide to use tags on other model eg. Videos, ... 
    });
}

In the Model

# Tag.php Model
class Tag extends Model
{
     protected $fillable = [

            'tagname',
    ];

    public function article()
    {
        return $this->morphedByMany('Yourapp\Article', 'taggable');
    }

}


# Article.php Model
class Article extends Model
{
     protected $fillable = [
        'title',
        # and more...
    ];
    public function tags()
    {
        return $this->morphToMany('Yourapp\Tag', 'taggable');
    }

}

In the AppServiceProvide.php ~ Yourapp/app/Providers/AppServiceProvider.php

public function boot()
{
    //... by creating this map you don't need to store the "Yourapp\Post" to the "taggable_type" on taggable table
    Relation::morphMap([
        'article' => 'Yourapp\Article',
        'videos' => 'Yourapp\Videos', // <--- other models may have tags
    ]);

}

Now using Elequent you can easily access data

   $article->tags; # retrieve related tags for the article
   $tags->articles; # or $tags->articles->get()  retrieve all article that has specific tag

For storing and updating article

# SAVING article with tags
public function store(Request $request) 
{
    $validatedData = $request->validate([
        'title' => 'required',
        //----
        // Validate tags and also it should be an Array but its up to you
        'tag' => 'required|array|exists:tags,id' # < (exist:tags,id) This will check if tag exist on the Tag table
    ]);

    $article = Article::create([
        'title' => $request->input('title'),
        //----
    ]);

    //Adding tags to article, Sync() the easy way
    $article->tags()->sync($request->input('tag'));

    return "Return anywhare";
}

# UPDATE tags article
public function update(Request $request, $id) 
{   
    // Validate first and ...
    $article = Article::find($id)->first();
    $article->title = $request->input('title');
    $article->save();
    //Updating tags of the article, Sync() the easy way
    $article->tags()->sync($request->input('tag'));

    return "Return anywhare";
}

For more details about many-to-many polymorphic relationship

like image 3
KirtJ Avatar answered Oct 23 '22 00:10

KirtJ