tahauygun's avatar

Avoid duplicate records in tagging system

I am pretty newbie for Laravel 5 and I am building an file gallery with tagging system. I am using Laravel 5.5 and each file has tags. I have created file table and tags table as well as file_tag pivot table. I am using Select2 for tag selection and creation on front end input. When I add tags and if the tags don't exist for that file, it works perfectly. But when I try to tag existing tags on file creation, it only passes existing tags' tag_id as name.

My goal is if the tags don't exist create the tags and create file tag relationship, if the tags exist already, just the create file tag relationship.

This is how I pass tags in form in my upload.blade.php form:

<select name="tags[]" class="form-control select2-multi-tags" multiple="multiple">
      @foreach($tags as $tag)
           <option value="{{$tag->id}}">{{$tag->name}}</option>
      @endforeach
</select>

These are my tables for tags and file_tag pivot:

Tags:

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

file_tag pivot table:

public function up()
{
    Schema::create('file_tag', function (Blueprint $table) {
        $table->increments('id');

        $table->integer('file_id')->unsigned()->index();
        $table->foreign( 'file_id' )->references( 'id' )->on( 'files' )->onDelete( 'cascade' );

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

Tag Model:

class Tag extends Model
{
  protected $table = 'tags';

  protected $fillable = [ 'name'];

  public function files()
  {
    return $this->belongsToMany( '\App\File');
  }
} 

File Model

class File extends Model
{
  protected $table = 'files';

  public function tags()
  {
    return $this->belongsToMany( '\App\Tag');
  }
}

My store controller

public function storeFile( request $request )
{
    if ( $request->hasFile( 'file' ) )
    {
        $image = $request->file( 'file' );
        $thumbnail_name = pathinfo($image->getClientOriginalName(), PATHINFO_FILENAME).'_thumb_'.time().'.'.$image->getClientOriginalExtension();
        $path = 'thumbnails/'.$thumbnail_name;

        Image::make( $image->getRealPath() )->resize( 640, 400 )->save( $path );


        //Get filename with extension
        $filenameWithExt = $image->getClientOriginalName();

        //Get just the file name
        $filename = pathinfo( $filenameWithExt, PATHINFO_FILENAME );

        //Get the extension
        $extension = $image->getClientOriginalExtension();

        //Create new filename
        $filenameToStore = $filename . '_' . time() . '.' . $extension;

        //Upload image
        $fileStore = $image->storeAs( 'public/images', $filenameToStore );
        $fileSize = $image->getClientSize();
        $fileTitle = $request->title;

        $fileModel = new File;
        $fileModel->title = $fileTitle;
        $fileModel->name = $filenameToStore;
        $fileModel->thumbnail_name = $thumbnail_name;
        $fileModel->size = $fileSize;
        $fileModel->save();

        $tags = $request->tags;
        $tagIds = [];

        foreach ($tags as $tagName)
        {
            $tag = Tag::firstOrCreate(['name'=>$tagName]);
            if($tag)
            {
                $tagIds[] = $tag->id;
            }
        }

        $fileModel->tags()->syncWithoutDetaching($tagIds);


        return redirect('/upload')->with('success', 'Image/Images created.');
    }
}

I am not sure what I am doing wrong or what I need to do in order to accomplish the goal. Any help would be appreciated.

Thanks for your time and attention.

0 likes
5 replies
rumm.an's avatar

I think you are all set. Its just select2.

For exisiting tags, in the option tag the value of the attribute value that you have given that is $tag->id will be posted with form submission. For new tags, in the option tag the value of the attribute value is set to the text value that you type.

Here is what you can do, Instead of giving $tag->id to the attribute value use $tag->name like so.

<select name="tags[]" class="form-control select2-multi-tags" multiple="multiple">
      @foreach($tags as $tag)
           <option value="{{$tag->name}}">{{$tag->name}}</option>
      @endforeach
</select>       

This will always post $tag->name with the form. And in your controller you are all good.

tahauygun's avatar

Is there any way I can avoid creating tag_id in tag table and create only the relationship between file and tag if tag already exist??

robrogers3's avatar

so if you want to ignore tags that are not 'used'.

then just find on by name,

and if it is null, then only then add it to the array.

$tag = Tag::whereName($tagName)->get()->first();

if (!$tag) continue;

???

rumm.an's avatar

Is there any way I can avoid creating tag_id in tag table and create only the relationship between file and tag if tag already exist??

For removing tag_id

All you need to do is, in your tags table:

public function up()
 {
    Schema::create('tags', function (Blueprint $table) {
        $table->string( 'name' , 190)->primary();
        $table->timestamps();
    });
 }

In your pivot Table:

public function up()
{
    Schema::create('file_tag', function (Blueprint $table) {
        $table->increments('id');

        $table->integer('file_id')->unsigned()->index();
        $table->foreign( 'file_id' )->references( 'id' )->on( 'files' )->onDelete( 'cascade' );

        $table->string( 'tag_name' , 190)->index();
        $table->foreign( 'tag_name' )->references( 'name' )->on( 'tags' )->onDelete( 'cascade' );
    });
}

in Your File Model:

class File extends Model
{
  protected $table = 'files';

  public function tags()
  {
    return $this->belongsToMany('App\Tag', 'file_tag', 'file_id', 'tag_name');
  }
}

in Your Tag Model:

class Tag extends Model
{
  protected $table = 'tags';

  public function files()
  {
    return $this->belongsToMany('App\File', 'file_tag', 'tag_name', 'file_id');
  }
}

and you controller:

        $tags = $request->tags;
        $tagNames = [];

        foreach ($tags as $tagName)
        {
            $tag = Tag::firstOrCreate(['name'=>$tagName]);
            if($tag)
            {
                $tagNames[] = $tag->name;
            }
        }

        $fileModel->tags()->syncWithoutDetaching($tagNames);

This should work for you...

Please or to participate in this conversation.