phplaravellaravel-8

Failure to update article tags in Laravel 8 blog application?


I am working on a blogging application in Laravel 8.

There is the option to add tags to articles. There is a many-to-many relationship between articles and tags. I have an article_tag pivot table.

In the Tag model I have:

class Tag extends Model
{
   use HasFactory;

   protected $fillable = ['name'];

   public function articles()
   {
       return $this->belongsToMany(Article::class);
   } 
}

To the Article model I have added the tags() method

public function tags()
{
    return $this->belongsToMany(Tag::class)->as('tags');
}

I the ArticleController controller I have the two methods for editing and updating an article:

public function edit($id)
  {
    $article = Article::find($id);
    return view(
      'dashboard/edit-article',
      [
        'categories' => $this->categories(),
        'tags' => $this->tags(),
        'selected_tags' => $this->tags()->pluck('id')->toArray(),
        'article' => $article
      ]
    );
  }

  public function update(Request $request, $id)
  {
    $validator = Validator::make($request->all(), $this->rules, $this->messages);

    if ($validator->fails()) {
      return redirect()->back()->withErrors($validator->errors())->withInput();
    }

    $fields = $validator->validated();
    $article = Article::find($id);

    // If a new image is uploaded, set it as the article image
    // Otherwise, set the old image...
    if (isset($request->image)) {
      $imageName = md5(time()) . Auth::user()->id . '.' . $request->image->extension();
      $request->image->move(public_path('images/articles'), $imageName);
    } else {
      $imageName = $article->image;
    }

    $article->title = $request->get('title');
    $article->short_description = $request->get('short_description');
    $article->category_id = $request->get('category_id');
    $article->tags[] = $request->get('tags[]');
    $article->featured = $request->has('featured');
    $article->image = $request->get('image') == 'default.jpg' ? 'default.jpg' : $imageName;
    $article->content = $request->get('content');
    // Save changes to the article
    $article->save();
    //Attach tags to article
    if ($request->has('tags')) {
      $article->tags()->attach($request->tags);
    }

    return redirect()->route('dashboard.articles')->with('success', 'The article titled "' . $article->title . '" was updated');
  }

The form for editing an article:

<form method="POST" action="{{ route('dashboard.articles.update', [$article->id]) }}" enctype="multipart/form-data"
    novalidate>
    @csrf
    <div class="row mb-2">
        <label for="title" class="col-md-12">{{ __('Title') }}</label>

        <div class="col-md-12 @error('title') has-error @enderror">
            <input id="title" type="text" placeholder="Title"
                class="form-control @error('title') is-invalid @enderror" name="title"
                value="{{ old('title', $article->title) }}" autocomplete="title" autofocus>

            @error('title')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-2">
        <label for="short_description" class="col-md-12">{{ __('Short description') }}</label>

        <div class="col-md-12 @error('short_description') has-error @enderror">
            <input id="short_description" type="text" placeholder="Short description"
                class="form-control @error('short_description') is-invalid @enderror" name="short_description"
                value="{{ old('short_description', $article->short_description) }}" autocomplete="short_description"
                autofocus>

            @error('short_description')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-2">
        <label for="category" class="col-md-12">{{ __('Category') }}</label>

        <div class="col-md-12 @error('category_id') has-error @enderror">

            <select name="category_id" id="category" class="form-control @error('category_id') is-invalid @enderror">
                <option value="0">Pick a category</option>
                @foreach ($categories as $category)
                    <option value="{{ $category->id }}"
                        {{ $category->id == $article->category->id ? 'selected' : '' }}>{{ $category->name }}</option>
                @endforeach
            </select>

            @error('category_id')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-2">
        <label for="tags" class="col-md-12">{{ __('Tags') }}</label>

        <select name="tags[]" id="tags" class="form-control" multiple="multiple">
            @foreach ($tags as $tag)
                <option value="{{ $tag->id }}" {{ in_array($tag->id, $selected_tags) ? 'selected' : '' }}>
                    {{ $tag->name }}</option>
            @endforeach
        </select>
    </div>

    <div class="row mb-2">
        <div class="col-md-12 d-flex align-items-center switch-toggle">
            <p class="mb-0 me-3">Featured article?</p>
            <input class="mt-1" type="checkbox" id="featured" name="featured" value="featured"
                {{ old('featured', $article->featured) ? 'checked' : '' }}>
            <label class="px-1" for="featured">{{ __('Toggle') }}</label>
        </div>
    </div>

    <div class="row mb-2">
        <label for="image" class="col-md-12">{{ __('Article image') }}</label>

        <div class="col-md-12 post-image @error('image') has-error @enderror">
            <input type="file" value="{{ old('image', $article->image) }}" name="image" id="file"
                class="file-upload-btn">

            @error('image')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-2">
        <label for="content" class="col-md-12">{{ __('Content') }}</label>

        <div class="col-md-12 @error('content') has-error @enderror">

            <textarea name="content" id="content" class="form-control @error('content') is-invalid @enderror"
                placeholder="Content" cols="30" rows="6">{{ old('content', $article->content) }}</textarea>

            @error('content')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-0">
        <div class="col-md-12">
            <button type="submit" class="w-100 btn btn-primary">
                {{ __('Update') }}
            </button>
        </div>
    </div>
</form>

For a reason I have not been able to spot, the update method fails with the error below:

Integrity constraint violation: 1062 Duplicate entry '306-1' for key 'PRIMARY' (SQL: insert into `article_tag` (`article_id`, `tag_id`) 

Where is my mistake?


Solution

  • When updating a pivot table, you are not expected to use attach(), you should use sync or syncWithoutDetaching. This ensure you do not have duplicate entries.

    https://laravel.com/docs/11.x/eloquent-relationships#syncing-associations