php 90 lines · 4 tabs

Syncing Article Tags Through a Many-to-Many Pivot in Laravel

Shared by codesnips Jul 2026
4 tabs
<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('article_tag', function (Blueprint $table) {
            $table->id();
            $table->foreignId('article_id')->constrained()->cascadeOnDelete();
            $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
            $table->timestamps();

            $table->unique(['article_id', 'tag_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('article_tag');
    }
};
4 files · php Explain with highlit

This snippet shows how a blog assigns tags to articles using Eloquent's many-to-many relationship and the sync method, which is the idiomatic way to reconcile a set of related records against a pivot table in a single call. The example spans a migration that defines the join table, two models that declare the relationship, and a controller that persists the selection.

In create_article_tag_table migration, the pivot table article_tag follows Laravel's naming convention: the two related tables in singular, alphabetical order. Each foreign key uses constrained() with cascadeOnDelete() so removing an article or a tag automatically clears the corresponding pivot rows, avoiding orphaned links. A composite unique index on article_id and tag_id enforces at the database level that the same tag can only attach once, which protects against duplicate rows even if application code slips.

In Article model, the tags() method returns a belongsToMany relation. Because the migration follows conventions, no extra pivot-table or key arguments are needed. The withTimestamps() call tells Eloquent to maintain created_at/updated_at on the pivot rows, which is useful for auditing when a tag was applied. Tag model declares the inverse articles() relation so the graph can be traversed from either side.

The interesting logic lives in ArticleController. The update method validates that tags is an array and that every element is the id of a real row via the exists rule, which prevents attaching phantom tags. The core call is $article->tags()->sync($validated['tags'] ?? []). Unlike attach, which only adds, sync computes the difference: it inserts missing links, removes links no longer present, and leaves untouched ones alone, returning an array of what was attached, detached, and updated. Passing an empty array detaches everything, which is why the ?? [] fallback matters when no tags are submitted.

A common alternative is syncWithoutDetaching, used when tags should only ever be added; sync is the right choice here because the form represents the complete desired state. Wrapping related writes and eager-loading with load('tags') afterward keeps the response consistent and avoids an N+1 query when rendering the result.


Related snips

Share this code

Here's the card — post it anywhere.

Syncing Article Tags Through a Many-to-Many Pivot in Laravel — share card
Link copied