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

Belongs To Many Field Laravel Package

benjacho/belongs-to-many-field

Benjacho's BelongsToMany field for Laravel Nova adds a searchable modal for attaching and detaching related models, supports inline creation, and simplifies managing many-to-many relationships in Nova resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin. The package is published on a 3.0.1-beta line, so Composer will not resolve it under default stability. Pin it exactly rather than floating:

composer require benjacho/belongs-to-many-field:3.0.1-beta

If you must allow range resolution, use "benjacho/belongs-to-many-field": "^3.0@beta" and commit the lock file. No manual provider registration is needed; the service provider is auto-discovered.

First use case — the 5-minute win. Pick one resource with a genuine belongsToMany and a small related set (e.g. Post → Tag):

use Benjacho\BelongsToManyField\BelongsToManyField;

public function fields(Request $request)
{
    return [
        BelongsToManyField::make('Tags', 'tags', Tag::class)
            ->options(Tag::pluck('name', 'id')->toArray())
            ->rules('required'),
    ];
}

Prerequisites on the model, not the field: the relation method must be named tags (matching the field's second argument), must be belongsToMany, and must declare withPivot(...) if you intend to touch pivot columns.

Where to look first, in this order:

  1. vendor/benjacho/belongs-to-many-field/src/BelongsToManyField.php — the entire PHP API surface is one small class. Read it before the README; it is the source of truth and the README lags behind it.
  2. vendor/benjacho/belongs-to-many-field/README.md — method examples, but verify each against the class.
  3. vendor/benjacho/belongs-to-many-field/dist/js/field.js — only when something is broken in the browser; this compiled bundle is what couples you to Nova's asset pipeline.

Run a smoke test immediately: render the form, attach two records, save, re-open, detach one, save. If that round-trips, the integration is sound.

Implementation Patterns

Two modes, and the choice is the whole design.

Situation Use Why
Small, fixed, or tenant-scoped set (roles, statuses, categories < ~500 rows) ->options([id => label]) Instant render, no search round-trip, no server load
Large or dynamic table (tags, products, users at 10k+) ->label('name')->searchable() Defers loading; only queries on keystroke

Never call Model::all() inside options() on a large table — that runs on every form render, for every user, forever. If you can't enumerate the set comfortably in a config file, use searchable().

Searchable mode:

BelongsToManyField::make('Products', 'products', Product::class)
    ->label('name')          // column used for display + search matching
    ->searchable()
    ->placeholder('Search products…')

Ensure the label column is indexed and that the related Nova resource exists and is itself searchable — the picker's behaviour is tied to Nova's search plumbing, and a resource with a broken searchableColumns() will produce a picker that returns nothing while looking healthy.

Single-select for "one of" semantics (primary role, owning team):

BelongsToManyField::make('Primary Role', 'roles', Role::class)
    ->options(Role::pluck('name', 'id')->toArray())
    ->singleSelect()
    ->rules('required')
    ->placeholder('Choose a role');

Always pair singleSelect() with ->rules('required') if the domain requires one, and with a placeholder() so an unset state is visually distinct from a preselected one.

Pivot data. Declare ->withPivot('position', 'assigned_at') on the model; the field then has columns to write into. Treat pivot editing as the highest-risk surface in the package: verify writes persist after save, verify defaults, and confirm validation covers the extra attributes. If the pivot carries business logic, approvals, or audit requirements, this field is the wrong tool — stay on a dedicated pivot CRUD resource.

Coexistence and rollback. Keep Nova's native BelongsToMany on every resource you haven't explicitly migrated. Migrate one resource per PR, touch only fields(), and leave the native field in a comment above the block:

// Rollback: replace BelongsToManyField below with Nova's BelongsToMany::make('Tags')
BelongsToManyField::make('Tags', 'tags', Tag::class)->options(...),

That comment is the entire rollback plan and costs nothing.

Index and detail views. The field is a form control first. On index views, either hide it (->hideFromIndex()) or use the read-only display variants (->displayAsBadges(), ->displayAsList()) — rendering an interactive picker per row is a payload and memory problem, not just a cosmetic one. Add ->onlyOnForms() when the detail page doesn't need it.

Testing. Feature-test the persistence layer, not the Vue component:

$post->tags()->sync([$tagA->id]);
$this->assertDatabaseHas('post_tag', ['post_id' => $post->id, 'tag_id' => $tagA->id]);

Then one browser test (Dusk or similar) per resource asserting the picker opens, searches, and saves — that's your early-warning system for Nova upgrades breaking the bundle.

Gotchas and Tips

The beta constraint is the first trap. Fresh setups fail with "could not be found in any version" until you pin the exact version or add a stability flag. Document it in the README of your own app, because the next developer will hit it during onboarding.

Stale published assets. The field ships a compiled dist/js bundle. After any deploy touching Nova, the field can silently go blank or fail on save while the PHP side looks fine. Symptom: works locally, broken in staging. Fix: re-run your asset publish step (nova:publish / vendor:publish) and confirm the manifest points at the current bundle. Put "revalidate this field and republish assets" as a standing line in your Nova upgrade runbook.

Field name vs. relation name. The second argument to make() must match the relationship method on the parent model, and the third must be the related model class. A typo manifests as an empty picker or an exception on save, not a friendly error.

options() type strictness. Pass a plain array, not a Collection — ->pluck(...)->toArray() not ->pluck(...). Key/value shape is [id => label]; confirm this against the class source for your pinned version rather than trusting a blog post.

Search that returns nothing. Usually one of: the related Nova resource is missing or has no searchableColumns(), the ->label() column is null/ambiguous, or the dataset is large enough that the picker paginates and the operator's term doesn't rank. Test with production-like volume before rollout — a picker that mis-searches persists wrong associations, which is a data-integrity incident, not a UI bug.

Inline creation bypasses your policies. The description advertises "inline create"; if you enable it, the created record may be instantiated outside the authorization and validation path your normal Nova resource enforces. Review the code path, add explicit rules() and authorize() coverage, and test with a user who shouldn't be able to create the related model. If you can't verify it, leave inline creation off.

Validation lives on the field. Nova validates what rules() declares on this field; it will not inherit the related resource's rules. Add ->rules(['array']) plus per-relation constraints yourself if the relation is load-bearing.

No filtering or sorting by this relation. This field edits associations; it doesn't create index filters. If operators need "show me posts tagged X," that's a Nova filter you write separately — don't expect it from the package.

Maintenance posture. Last release February 2023, single maintainer, ~156 stars. Budget a named internal owner and a few engineer-days a year, concentrated around Nova/Laravel upgrades. Because it's MIT, the escape hatch is vendoring a fork under your namespace with a pinned constraint — decide who can execute that before you need it, not after an upgrade breaks the bundle. Two practical habits keep the exit cheap: keep usage isolated per resource, and never extend the field class in application code if you can avoid it.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle