Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sync Many Attribute Laravel Package

illuminatech/sync-many-attribute

Laravel package for synchronizing many-to-many related records and their attributes through a single model. Save related model attributes, including pivot data, using familiar form binding and validation, simplifying complex multi-record editing without custom sync logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer:

composer require illuminatech/sync-many-attribute

There is no service provider or config to publish in the typical setup — the package is opt-in per model. The shortest path is:

  1. Add the package trait to the model that owns the many-to-many relation.
  2. Declare the virtual attribute(s) you want, mapping each attribute name to a relation method.
  3. Add the virtual attribute to $fillable if you plan to mass-assign it.
use Illuminate\Database\Eloquent\Model;
use Illuminatech\SyncManyAttribute\SyncManyAttribute;

class Post extends Model
{
    use SyncManyAttribute;

    protected $fillable = ['title', 'body', 'tag_ids'];

    public function syncManyAttributes(): array
    {
        return [
            'tag_ids' => 'tags', // virtual attribute => relation method
        ];
    }

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

First use case — replace this:

$post->tags()->sync($request->input('tag_ids', []));

with this:

$post->update($request->validated()); // tag_ids included in payload

The attribute reads back as an array of related IDs, so $post->tag_ids is the natural value to seed a multi-select in a form.

Where to look first: the package README for the exact method name on the trait (syncManyAttributes() vs. a config array), then src/ to confirm what is overridden on save(), and finally tests/ — with a package this small (15 stars, NOASSERTION license) the test suite is effectively the spec. Read the license file before shipping it into a client project.

Implementation Patterns

Form-to-model round trip. The core value is that a multi-select becomes a plain Eloquent attribute. Validate the incoming IDs as an array ('tag_ids' => ['array'], 'tag_ids.*' => ['integer', 'exists:tags,id']), drop them into validated(), and let save()/update() do the sync. This keeps controllers free of relation-specific plumbing.

Reading state for edit forms. $post->tag_ids resolves the current pivot IDs, so an edit view can do:

<select name="tag_ids[]" multiple>
  @foreach ($tags as $tag)
    <option value="{{ $tag->id }}" @selected(in_array($tag->id, $post->tag_ids))>
      {{ $tag->name }}
    </option>
  @endforeach
</select>

Multiple many-to-many relations. Map several attributes in the same array — categories, tags, audiences — and one save() syncs all of them:

public function syncManyAttributes(): array
{
    return [
        'tag_ids'        => 'tags',
        'category_ids'   => 'categories',
        'audience_ids'   => 'audiences',
    ];
}

Consistent create/update flow. Because syncing is tied to persistence rather than to an explicit sync() call, Post::create($data) and $post->update($data) behave the same way. That symmetry is the main reason to reach for this over manual sync() calls in each action.

API and console commands. Anywhere you hydrate a model from a payload — JSON resources, seeders, import commands — you can set $model->tag_ids = [...] and save(), without branching into relation queries. Useful when a repository/service layer already speaks in attribute arrays.

Guard against no-op syncs. Set the attribute only when the key is present in the request ($request->has('tag_ids')), otherwise a partial update silently detaches everything. Pair with sometimes in the validation rules.

Gotchas and Tips

Mass assignment is the first tripwire. The virtual attribute must be in $fillable (or covered by a $guarded exception) or update($data) will silently drop it. This is the most common "it doesn't work" report.

Sync semantics are destructive. The underlying call is almost certainly sync(), meaning IDs not in the submitted array are detached. If a form section is optional and omitted, you will wipe the relation. Use sometimes validation and only assign when the key exists.

syncWithoutDetaching may not be exposed. Check the source for whether additive behavior is configurable. If not, and you need it, wrap the relation call manually — the package is thin enough that escaping to $model->tags()->syncWithoutDetaching(...) for edge cases is fine.

Dirty tracking and wasChanged(). The virtual attribute is not a column, so it won't appear in getDirty() or wasChanged() even when the relation changed. If downstream logic (audit logs, search indexing) depends on "did the tags change?", compare $post->tag_ids before and after, or hook the pivot events (pivotAttached, pivotDetached).

Pivot data. Plain arrays of IDs won't carry pivot columns (ordering, roles, timestamps). If your relation needs pivot payload, this package's simple attribute contract stops short — either post-process the relation after save or skip the package for that relation.

Eager loading. Accessing $post->tag_ids on a collection without with('tags') causes lazy loads. Add the relation to eager loads in list/index queries, or the attribute becomes an N+1 footgun in Blade loops.

Event ordering. The sync fires during the model's save lifecycle, so saved observers will see the relation in its new state; saving observers may not. If you have observers that read the relation, verify the order in the package source rather than assuming.

Transactions. Wrap save + any follow-up relation writes in DB::transaction() — the sync is a separate statement from the model update and can partially fail.

Null vs. empty array. Confirm how null is treated: does it detach all, skip, or error? Normalize in the FormRequest ($request->input('tag_ids', [])) so behavior is explicit.

Naming collisions. Do not name the virtual attribute the same as the relation method or an existing accessor. tag_ids alongside tags() is safe; tags alongside tags() is not.

Testing. Assert both directions: $post->tag_ids after save, and $post->tags()->pluck('id'). The first can pass from stale in-memory state; the second proves the pivot actually changed.

Low-maintenance risk. 15 stars and an unasserted license mean you should treat this as vendored behavior: pin the version in composer.json, and if the sync semantics matter to your domain, write a thin wrapper trait of your own so a future swap is a one-file change.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource