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

Custom Relationship Field Laravel Package

digital-creative/custom-relationship-field

A Laravel Nova field for customizable relationship selection. Define how related records are searched and displayed, giving Nova resources flexible relationship inputs that fit naturally into forms while reducing the need for custom field code.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the gate before anything else: this is a Laravel Nova field. Without Nova in the project it does nothing. Check composer.json for the package's Nova constraint against your installed Nova major — the 2024 release line implies Nova 4, but verify rather than assume.

Pin the install. Zero dependents, a single visible maintainer, MIT, last release July 2024. That is a vendored utility, not a platform dependency:

composer require digital-creative/custom-relationship-field:^1.1.3

v1.1.3 is the floor, not a nice-to-have. It fixes a buildIndexQuery crash when selecting all BelongsToMany items — meaning the package hooks into Nova's index pipeline, not just its public field API. Anything earlier is a latent admin error.

Read three files, in this order, before writing resource code:

  1. vendor/digital-creative/custom-relationship-field/src/ — the whole package is essentially one field class. In five minutes you can read the constructor signature and the methods that set the related resource, the options/query callback, display label, and search columns. Every method name in this assessment (->options(...)) follows documented usage — verify it against the source, because the API surface is small enough that guessing is inexcusable.
  2. Nova's own BelongsToMany field source, side by side. You want to see precisely which parts of the native UX are reimplemented (search, select, attach) and which are skipped (persistence semantics).
  3. The Nova resource where you would otherwise call BelongsToMany::make(...).

First use case — a throwaway resource, not a production one. A "Featured Products" curation on a Collection resource is the canonical shape: admins attach Products, but there is no collection_product pivot. Store IDs in a JSON column and let the field supply the select/search UX.

// app/Nova/Collection.php
use DigitalCreative\CustomRelationshipField\CustomRelationshipField;

public function fields(Request $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Name'),

        CustomRelationshipField::make('Featured Products')
            ->options(fn () => Product::query()
                ->where('is_active', true)
                ->orderBy('name')
                ->pluck('name', 'id')
                ->all()),
    ];
}

Definition of working: search → select → save → re-open shows the identical set, and the stored value round-trips through the model cast. Prove that on the throwaway resource before an editor touches it.

Implementation Patterns

1. JSON-cast column as the backing store. Standardize on this. The "relationship" becomes an ordinary attribute readable in factories, seeders, exports, and controllers with no Nova in the loop:

// Collection model
protected $casts = ['featured_product_ids' => 'array'];

2. Move option queries out of the resource. Never inline a fat closure in fields(). Put it on the model so Nova, seeders, and console commands share one source of truth:

// Product.php
public static function featuredOptions(): array
{
    return static::query()
        ->where('is_active', true)
        ->orderBy('name')
        ->pluck('name', 'id')
        ->all();
}

Then ->options(Product::featuredOptions()). Highest-leverage single change you can make: it makes the field swappable for a native relationship later with zero consumer changes, and it makes the query testable without Nova.

3. Wrap it in a project-local field class. app/Nova/Fields/CuratedRelationship.php extending the package field, used everywhere. When Nova 5 lands, or you fork, you edit one file instead of grepping twenty resources. It is also the correct home for ->help('Curated list — not a database relationship.').

4. Authorization scoping lives inside your options closure. Nova policies on the owning resource do not constrain the options query. Multi-tenant and role scoping must be explicit:

->options(fn () => Product::query()
    ->where('tenant_id', auth()->user()->tenant_id)  // not optional
    ->where('is_active', true)
    ->pluck('name', 'id')
    ->all())

Treat every ->options() closure as a place a data leak can hide, and review it as such in code review.

5. Treat it as write-through, not derived. If the field is editable, the saved value is the truth. Anything downstream — cache, search index, sitemap — needs a hook:

static::saved(function ($collection) {
    if ($collection->wasChanged('featured_product_ids')) {
        Cache::forget("collection:{$collection->id}:featured");
        SearchIndex::reindex($collection);
    }
});

6. Keep it out of your relationship API. Resolve explicitly in controllers and services:

$products = Product::whereIn('id', $collection->featured_product_ids ?? [])->get();

Note whereIn does not preserve order. If curation order matters — and for "featured" it usually does — store [{id, position}] or an ordered array and re-sort in PHP.

7. Roll out one resource at a time. Start with a low-risk surface (CMS collections, internal dashboards). Verify index browsing, search, sorting, filtering, and select-all against your exact Nova build. Then copy the pattern.

8. Memoize the options array per request if the field appears on list views, using once() (Laravel 11) or a static guard. Same closure invoked N times per page render is an easy N+1.

Gotchas and Tips

It is not a relationship, and Eloquent does not know it exists. No ->featuredProducts, no whereHas, no eager loading, no cascades, no foreign keys. Deleting a Product leaves a dangling ID in every JSON column referencing it. If that matters, schedule cleanup:

Collection::each(function ($c) {
    $clean = array_values(array_intersect(
        $c->featured_product_ids ?? [],
        Product::pluck('id')->all()
    ));
    if ($clean !== $c->featured_product_ids) {
        $c->update(['featured_product_ids' => $clean]);
    }
});

The v1.1.3 fix is a compatibility warning, not just a changelog line. It patches buildIndexQuery, confirming the package reaches into Nova's index-query pipeline. Every Nova minor bump is a potential break. Add to your upgrade checklist: branch → bump Nova → open every resource using the wrapper field → test select-all → merge.

Test select-all with realistic volume. It crashed once; now it succeeds, which only moves the question to cost. Select-all over thousands of records materializes a large ID set into a JSON column. Bound the options list (domain filter or a cap) so the UI cannot offer a set you cannot store or render.

Index views can go N+1. A field that resolves per row turns a page render into N queries. If the list view slows after adding it, look there first.

Verify assets land during deploy. If the package ships compiled frontend assets, confirm the publish step (vendor-documented or nova:publish) runs in CI. "Renders as a blank box" is almost always an asset/mix problem — check the browser console before debugging backend code.

Don't double-model. The day a real pivot table appears, delete the custom field in the same PR. Two sources of truth diverge on the first edit through the other path.

Debug by bypassing Nova. Call the options closure from php artisan tinker with dd(). Query right + UI wrong ⇒ Nova/asset issue. Query wrong ⇒ yours. That split resolves most sessions with a package this small.

Write it down as a vendored utility. 33 stars, 0 dependents, one maintainer, last release mid-2024. Pin the version, keep the one-file seam (Pattern 3), and expect to patch a Nova major yourself. Name an internal owner.

Leave a contract comment in the resource or model: what backs the field, that it is admin-only, and that the stored value is curated data — not something any API, export, or report reads. That comment is the difference between a planned migration and an incident six months out.

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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core