boyemagnus/nova-attach-many
A Laravel Nova field for attaching many related records at once. Provides a searchable multi-select interface for belongsToMany relationships, letting you manage pivot-linked models directly from the resource form. Simplifies many-to-many management in Nova.
Install and verify compatibility first. The package is a Nova field, so its constraint on laravel/nova matters more than anything else:
composer require boyemagnus/nova-attach-many
Before writing any field definitions, open composer.json in vendor/boyemagnus/nova-attach-many and confirm the require block supports your Nova major version (Nova 4/5 changed field signatures — fields(NovaRequest $request), resolveUsing, and the Field base class). If the constraint is loose (^4.0|^5.0) you are fine; if it pins an old Nova, stop here and use the native BelongsToMany field instead.
Where to look first. Read the field class itself (src/AttachMany.php or similar) rather than the README. You need three things from it:
make() — does it take (string $name, ?string $relationship = null, ?string $resource = null) like BelongsToMany, or something custom?BelongsToMany (you inherit attach/detach/search behaviour) or Field (you inherit almost nothing).resolve() / fill() methods — this tells you whether it writes via sync(), attach(), or syncWithoutDetaching().First use case. The field exists to flatten Nova's built-in BelongsToMany UX (a detail screen plus a modal) into a single inline multi-select on the form. The canonical first use is a Post belongsToMany Tag relationship:
use BoyeMagnus\AttachMany\AttachMany;
public function fields(NovaRequest $request)
{
return [
AttachMany::make('Tags'),
];
}
Load the create and update screens, attach two records, save, and confirm rows land in the pivot table. If that works end-to-end, the field is doing its job and everything else is configuration.
Explicit relationship and resource. Relying on convention works until it doesn't — always pass the relationship name and the related resource class explicitly so refactors and non-standard naming don't silently break the field:
AttachMany::make('Assignees', 'assignees', User::class)
->rules('required', 'array')
->help('Select one or more owners for this ticket');
Keep native BelongsToMany for pivot data. The moment you need pivot attributes (role, position, expires_at), pivot fields on the form, or withPivot values surfaced in the UI, this field is the wrong tool — it only manages the attach/detach set. A pragmatic split that works well day-to-day: use AttachMany on the create form for speed, and the native BelongsToMany field on the detail page for pivot editing.
Searchability. On large related tables the select needs to query. Two things must line up: the field must expose a searchable()-style modifier, and the related resource must define public static $search = ['name', 'email']. If the dropdown only ever shows the first page of results, it is almost always the resource's $search array, not the field.
Authorization and scoping. Scope the candidate list through the resource, not the field — override relatableQuery() on the related Nova resource, or the field's own query hook if it exposes one. This keeps authorization logic in one place and applies to every field pointing at that resource:
public static function relatableQuery(NovaRequest $request, $query)
{
return $query->where('tenant_id', $request->user()->tenant_id);
}
Read-only / conditional variants. Use ->readonly() for display contexts, ->canSee(fn ($request) => $request->user()->isAdmin()) for gating, and ->hideFromIndex() if the field renders poorly in the table (multi-selects rarely belong in an index column). Combine ->onlyOnForms() with a native BelongsToMany on the detail view if you want attach speed on forms and pivot visibility afterwards.
Testing. Don't trust the UI — assert the pivot table directly in feature tests. Post a Nova update request with tags as an array and assertDatabaseHas('post_tag', [...]), then post an empty array and confirm rows are removed. That second assertion is where attach-many fields most often misbehave.
Zero stars and zero dependents is a real signal. This is a single-maintainer package with no adoption footprint. Verify against your Nova version before building on it, and be prepared to fork it. The good news: it's a Nova field, so the blast radius is one class — vendoring or replacing it with a 60-line custom field is cheap if maintenance stalls.
The empty-submission semantics. Check whether fill()/resolve() uses sync(), attach(), or syncWithoutDetaching(). sync() removes everything not in the submitted array, which is usually what you want — but if the field is conditionally hidden (canSee), the attribute may not be present on the request at all, and you can wipe an entire relationship by submitting a form where the field wasn't rendered. Guard with ->hideWhenUpdating() or exclude hidden fields from sync.
Pivot timestamps and events. If the pivot table has created_at/updated_at, plain attach() won't populate them unless the relationship declares ->withTimestamps(). Also confirm the write goes through Eloquent rather than a raw query builder call — otherwise attached/detached model events never fire and your observers silently don't run.
Nova version drift. Nova 4+ moved field definitions to NovaRequest and changed how Field::make() resolves metadata. If the field was written for Nova 3, you may see missing options, a broken resolve for index views, or a Vue component that can't find its field.formField prop. Symptom to watch for: the field renders but the dropdown is permanently empty — that's a meta/payload mismatch, not a relationship bug.
Index view is nearly always wrong. Multi-select attach fields serialize as arrays; Nova's index component expects a scalar. If you leave it on the index, you'll get Array to string conversion or [object Object]. Add ->hideFromIndex() reflexively.
Debugging checklist. (1) dd() the field's resolve() output in the resource; (2) inspect the field meta in the JSON payload of the resource detail endpoint — the options array should be populated before the browser renders; (3) if options are present but saving does nothing, the field isn't calling fill(), meaning the attribute name doesn't match the relationship or the field is missing from fields() for update. In that order, it takes minutes instead of an afternoon.
Extension points. Because it's a thin field class, the cheapest fix for a gap is a local subclass rather than a fork: override resolve() to reshape options, override the query builder to add scoping, or override fill() to switch sync() for syncWithoutDetaching(). Keep the subclass in app/Nova/Fields/ so composer update can never clobber it.
How can I help you explore Laravel packages today?