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

Nova Polymorphic Field Laravel Package

michielkempen/nova-polymorphic-field

A Laravel Nova field for managing polymorphic relationships, letting a resource link to models of different types through a single field. Simplifies selecting and displaying polymorphic relations in Nova.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer — the package ships its compiled assets, so there is no npm run dev step and no vendor:publish tag in your host app:

composer require michielkempen/nova-polymorphic-field

Laravel package discovery registers MichielKempen\NovaPolymorphicField\FieldServiceProvider. If discovery is disabled, add it to config/app.php manually. There are no migrations, no config file, and no publishable assets. The field container itself is purely a presentation concern.

Where to look first. The README is thin; the source is the documentation.

  • src/PolymorphicField.php — the fluent API. It collects a map of discriminator values to arrays of ordinary Nova fields.
  • resources/js/components/FormField.vue — how the container reacts to the value of the field it depends on, and how it reads from Nova's field value store.
  • composer.json — your first stop, before anything else, to check constraints against your Nova/Laravel/PHP matrix.

Before you start, run the gate. Nova 4+ ships a native MorphTo field covering much of this ground. Confirm your pinned Nova version doesn't already do the job; if it does, close this out. If it doesn't, budget half a day to prove the package installs and renders on your exact Nova + Laravel + PHP + Node combination. Treat a failed install as a decision, not a problem to solve.

First use case. Two shapes you'll hit immediately:

Polymorphic attachment — a comment, note, or media record attached to any one of several models:

use MichielKempen\NovaPolymorphicField\PolymorphicField;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;

Select::make('Type', 'commentable_type')
    ->options([
        \App\Models\Post::class  => 'Post',
        \App\Models\Video::class => 'Video',
    ])
    ->rules('required'),

PolymorphicField::make('Comment')
    ->fields([
        \App\Models\Post::class => [
            Textarea::make('Body')->rules('required'),
        ],
        \App\Models\Video::class => [
            Textarea::make('Body')->rules('required'),
            Text::make('Timestamp')->help('Seconds into the video'),
        ],
    ]),

Discriminator-driven field groups — a "Contact Type" of email / phone where each branch needs a different set of inputs (see Implementation Patterns).

The discriminator must be declared above the container in your fields() method. On create, the container has nothing to render until that value changes.

Implementation Patterns

Keep the discriminator a native Nova field. Select, Radio, or Status. The container reads from Nova's field value store, so a custom input that doesn't live in this.fields will never trigger a re-render. This is the single most common integration bug — the form renders, nothing swaps, and there's no error.

Edit screens. On update the discriminator is resolved from the model before the form paints, so the correct branch renders immediately — provided the discriminator is a real, persisted attribute. If it's derived (an enum cast, an accessor, a related model's class), give the field a resolveUsing so the container has a concrete string to match:

Select::make('Type', 'commentable_type')
    ->resolveUsing(fn ($value, $resource) => $resource->commentable_type)
    ->options([...]),

Persistence — two distinct approaches.

Inline save. The container stores nothing. Inner fields save under their own attribute names exactly as if you'd written them out in the form. This is the path of least surprise; use it unless you have a reason not to.

Single JSON column. Cast the attribute on the model ('metadata' => 'array') and use fillUsing / resolveUsing on each inner field to read and write into that array. More code, but it keeps a heterogeneous blob out of your schema:

Textarea::make('Body')
    ->resolveUsing(fn ($value, $resource) => $resource->metadata['body'] ?? null)
    ->fillUsing(function ($request, $model, $attribute) {
        $model->metadata = array_merge($model->metadata ?? [], [
            'body' => $request->input('body'),
        ]);
    }),

Validation — scope it server-side. Rules on inner fields are collected normally, but they only behave correctly if you scope them to the branch that's actually rendered. The pragmatic pattern is a FormRequest that inspects the discriminator and applies the right rule set, rather than sprinkling rules() across branches. For simple two-branch cases, required_if:contact_type,email on each field is enough. Do not rely on the UI hiding fields as a validation mechanism — a hand-crafted POST will still send them.

Composition. Works alongside NovaDependencyContainer for orthogonal show/hide logic. Also useful inside fieldsForCreate / fieldsForUpdate when the field set legitimately differs between create and edit, or inside canSee callbacks to gate branches by policy.

Repeat the selector across resources. The real payoff is when the same polymorphic selector appears in five Nova resources — comment on Posts, on Videos, on Products. That repetition is where a shared field beats hand-rolled per-resource conditional logic. If you only need it once, write it inline.

Nullable relationships. When nothing is attached yet, the discriminator starts empty and the container renders nothing. Pair with a nullable select, or default the discriminator to the most common type so admins see a form on first load.

Gotchas and Tips

It's abandoned — fork on day one. Last release was 2020-05-04, there are zero Packagist dependents, and 66 stars means nobody is absorbing compatibility fixes for you. Do not add it as a transitive dependency of a shared internal package. Fork it into your repo, pin by tag or commit, add a smoke test that asserts the field renders and swaps, and document the owner. The MIT license makes this straightforward; the maintenance burden is the cost.

Asset pipeline breakage on Nova upgrades. Nova major versions have historically changed the custom-field JS contract. The compiled assets will not be rebuilt for you. Every Nova upgrade (v3 → v4 → v5) is a line item against this field, and it's a known upgrade blocker. Keep a rollback path to native fields or a plain, flatter form layout.

Never --ignore-platform-reqs to make it install. Five years of drift means the constraints probably exclude your PHP/Laravel version. Ignoring platform requirements hides incompatibility rather than resolving it — you'll get a working composer install and a broken field, or worse, a broken Nova panel.

Discriminator ordering. Declare it above the container in the fields() array. This is not stylistic; the container's Vue component needs the value present in the store when it mounts.

Derived discriminators need an explicit resolver. Enums, accessors, and morphTo relation classes won't match the string keys in your map unless you normalise them with resolveUsing. Symptom: works on create, blank container on edit.

Debugging. Open Vue devtools and inspect the component's fields store — the container's branch decision is a pure function of that value. If the branch doesn't swap, the value isn't reaching the store (usually a non-native discriminator or wrong ordering). If it swaps but values don't persist, check fillUsing on the inner fields, not the container.

UI hiding is not authorization. Gating a branch with canSee is a UX convenience. Mirror the rule in policies and validation rules, because the underlying endpoint is unchanged. This matters more here than usual: the admin-only surface area lowers the blast radius of the abandoned package, but it doesn't lower the blast radius of a validation gap inside your own app.

Keep it internal and non-critical. Use it for polymorphic comments, tagging, media attachments, and audit entries inside Nova — not for anything customer-facing, and not for workflows you can't drop without impact. Put it on the "internal, unsupported, at-risk" list from day one and revisit it at your next Nova major upgrade.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager