metasync-site/nova-belongs-to-many
A Laravel Nova field for working with belongs-to-many relationships. It offers a cleaner interface for attaching, detaching, and searching related records, making many-to-many management simpler inside Nova resources.
Install into a Nova app, not a plain Laravel app — Nova is not on Packagist, so version resolution will silently fail if laravel/nova isn't already required.
composer require metasync-site/nova-belongs-to-many
Before running it, open the package's own composer.json and check the laravel/nova constraint. At 3 stars and 0 dependents this package is effectively unproven, and a stale Nova constraint (e.g. ^4.0 only) is the most common reason it "doesn't work" on a Nova 5 app.
Where to look first, in order:
README.md — for the exact field class name. The namespace is not guaranteed to match the package handle, so confirm whether you import something like MetasyncSite\NovaBelongsToMany\BelongsToMany or a shorter alias. If the README is thin, go to step 2.src/ — find the field class. It will extend Nova's BelongsToMany or Field; the former means every Nova method still applies, the latter means you lose things like ->fields() for pivots and need to check.resources/js/components/ — the Vue component shows you which endpoint is called on keystroke. That endpoint tells you what "searchable" actually means here (a Nova associatable route vs. a package-published route) and what column it queries.config/ — if a config file exists, run php artisan vendor:publish --provider="..." and read it before wiring fields; it usually controls search columns and result limits.First use case: a Resource whose BelongsToMany relation has more than a few hundred rows, where Nova's default multi-select loads every option on form render and the browser hangs. Swap the field:
use MetasyncSite\NovaBelongsToMany\BelongsToMany; // confirm from README
public function fields(NovaRequest $request)
{
return [
// ...
BelongsToMany::make('Tags'),
];
}
If the page loads and typing in the select fires an XHR, the package is installed correctly. If the select still renders every Tag up front, the field class isn't the one you imported.
Use it as a drop-in only where it earns its keep. Most Resources should keep Nova's stock BelongsToMany; the searchable variant is for large relations (tags, categories at scale, users across a big tenant). Mixing both in one app is fine and is the safest rollout — swap one field, verify, then expand.
Scope the searchable set in the Resource, not the field. The dominant lever for this field is Nova's relatable query. Constrain what users can find and pick:
public static function relatableTags(NovaRequest $request, $query)
{
return $query->where('tenant_id', $request->user()->tenant_id)
->where('active', true);
}
This is also the right place to eager-load or restrict columns. Whatever column the package searches on should be indexed in the database — a search-as-you-type field over an unindexed name column is a latency problem you will blame on the package.
Pivot attributes still work the Nova way. If the field extends Nova's BelongsToMany, keep using ->fields() with withPivot on the Eloquent relation:
BelongsToMany::make('Roles')
->fields(fn () => [
Text::make('Assigned At', 'assigned_at'),
]),
// Model
public function roles()
{
return $this->belongsToMany(Role::class)->withPivot('assigned_at')->withTimestamps();
}
If ->fields() is absent from the package's field, that's a hard signal it reimplements the select rather than extending Nova's, and pivot editing will need a separate solution.
Handle index/detail rendering explicitly. A searchable form field says nothing about how to display many values in the index. Set displayUsing for a compact summary and eager-load in indexQuery to avoid N+1:
BelongsToMany::make('Tags')
->displayUsing(fn ($tags) => $tags->pluck('name')->take(3)->join(', ')),
public static function indexQuery(NovaRequest $request, $query)
{
return $query->with('tags:id,name');
}
Validate as an array of foreign keys, regardless of component:
'roles' => ['nullable', 'array'],
'roles.*' => ['integer', Rule::exists('roles', 'id')],
Nova's relationship controllers do most of this, but if you also accept the same payload through your own endpoints (importers, API resources), don't assume the field's client-side filtering is validation.
Use relatableQuery for self-referential and polymorphic relations. Many-to-many to the same table (related products, parent/child categories) is where users hit empty results or timeouts; restricting depth in the relatable callback is easier than patching the package.
Treat it as a template if you need behavior changes. At this maturity level, forking and owning the Vue component is a legitimate implementation pattern — cheaper than arguing with an unmaintained package's semantics.
relatableQuery respects viewAny/view policies on the related resource. Before touching the package, verify a user can browse the related Resource in Nova at all.name) or a config-defined column list. If your model uses title or a full name, you'll get zero hits with no error. Confirm in the Vue component or config, and if it's hardcoded, override the resource's relatable query to alias/search differently, or fork.nova constraint before blaming your field config. Rebuilding assets inside vendor/ is a temporary fix at best — Nova::script() / a fork is the durable one.with() in indexQuery.(parent_id, related_id) or pivot-level rules belongs in a Model observer or a custom Attach handler, not in fields().composer.json, and if you rely on it in production, fork it into your own VCS. There is no community to catch regressions for you.php artisan tinker; (3) confirm the policy allows it; (4) check the Network tab for the search XHR; (5) check storage/logs/laravel.log for 403/500 on the search route. In that order — it isolates config, authorization, and asset problems from each other.relatableQuery (scoping), and displayUsing (presentation). Only reach into the Vue component if you need to change the request shape or result template itself.How can I help you explore Laravel packages today?