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 Inline Relationship Laravel Package

kirschbaum-development/nova-inline-relationship

Laravel Nova field for creating related resources inline without leaving the form. Search and select existing records, or open a modal to add a new one. Supports BelongsTo, MorphTo, and HasOne relationships, with searchable selects, custom modal forms, and automatic parent refresh.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the Nova constraint before you type composer require. This package is a Nova field — no migrations, no service provider to register, no config file to publish, no queue or storage. The entire integration surface is one PHP field class plus a compiled Vue component. Open the package's composer.json and compare its illuminate/nova constraint against php artisan nova:version before committing. The last release (2.1.1, Jan 2024) predates broad Nova 5 deployment; treat Nova 5 as unproven until you have rendered the field once in a browser and completed a round-trip save.

2. Install pinned to the timestamps fix.

composer require kirschbaum-development/nova-inline-relationship:^2.1.1

Do not land on 2.1.0. That release mishandled $timestamps on inline saves — models declaring $timestamps = false could receive timestamp writes for columns that don't exist, and normal models could be saved without touching updated_at. That is a write-path integrity bug inside an admin surface, not a cosmetic one. Pinning ^2.1.1 costs nothing.

3. Read src/ before you register the field anywhere. The README tells you it works; your day-to-day questions live in the PHP field class and the Vue component. Spend ten minutes answering:

  • What exactly does the inline save call — Model::save(), create(), or something that writes around Eloquent? The answer decides whether observers, model events, booted hooks, and updated_at-driven jobs fire from a modal.
  • Does it authorize against the related model's policy, or only against the parent resource that hosts the field? Creating a User from an Order form should not be a policy bypass.
  • What is the actual constructor signature, and is the third argument the related attribute being displayed? Verify against source; the class is short and the check takes a minute.

4. First use case: inline creation on a BelongsTo, one resource. The canonical win is Order → Customer, where an operator is entering a new order and the customer doesn't exist yet. Instead of leaving the form, creating the customer in a second tab, and coming back, they open the modal, fill in the related resource's fields, save, and the new record is selected:

use Kirschbaum\NovaInlineRelationship\InlineRelationship;

public function fields(NovaRequest $request)
{
    return [
        // ...
        InlineRelationship::make('Customer', 'customer'),
    ];
}

Confirm the argument order against the source before you commit it. Because the modal reuses the related resource's fields() and validation rules, your customer form just got a second entry point — check that every field it exposes is one you're comfortable creating inline.

5. Pilot on one low-traffic resource with a throwaway record, exercising three cases explicitly: parent with an existing related model, parent with a missing/deleted related model, and an inline create against a child with $timestamps = false. Then dump DB::getQueryLog() on the index request before expanding to a second resource.

Implementation Patterns

Wrap the package field in your own class. This is the single highest-leverage habit for a 193-star dependency with a low release cadence:

namespace App\Nova\Fields;

use Kirschbaum\NovaInlineRelationship\InlineRelationship;

class InlineCustomer extends InlineRelationship
{
    public function __construct(string $label = 'Customer', string $relationship = 'customer')
    {
        parent::__construct($label, $relationship);

        // normalise defaults, force readonly(), attach rules(),
        // or absorb a fork's behaviour — all in one place
    }
}

Every resource then uses InlineCustomer::make(). A Nova major upgrade, an upstream stall, or a swap to a maintained fork becomes a one-file change rather than a sweep across a dozen resources.

Decide per field whether you're replacing or pairing with BelongsTo. Two fields on the same relationship is a genuine option (the plain select for existing records, the inline field for display), but it doubles the confusion for operators and the surface for review. Usually the cleaner choice is to drop the plain BelongsTo and let this field own the relationship — but only once you've confirmed it can select existing records as well as create new ones.

Declare eager loading on every resource that uses it — this is the performance contract. Without it the related attribute resolves lazily per row and the index endpoint degrades linearly with perPage:

public static $with = ['customer'];

Or, when the relation set is conditional:

public static function indexQuery(NovaRequest $request, $query)
{
    return $query->with('customer');
}

Prove it rather than assume it:

DB::enableQueryLog();
$this->actingAs($admin)->getJson('/nova-api/orders?perPage=25');
$this->assertLessThan(10, count(DB::getQueryLog()));

A flat query count as perPage rises is the only acceptable result.

Use it as the consolidation target for existing resolveUsing closures. Most Nova codebases already carry ad-hoc Text::make('Customer Email', fn () => $this->customer?->email) scattered across resources. In that context this package is a refactor, not a new capability. Migrate one field, diff the rendered output, then sweep the rest. Success metric: no hand-rolled inline-resolution closures left.

Scope the modal's fields deliberately. The modal re-renders the related resource's field set, which means every field on that resource is now part of your inline-create flow — including a password field, a File upload, or a nested relationship that drags its own queries and its own UX into a modal that was supposed to be a 15-second form. Where the resource's field set is broader than what you want creatable inline, gate fields with canSee() conditions keyed on the request, or maintain a slimmer resource for the inline path.

Gate the write path with rules() on the field and the related model's policy. Nova authorizes the parent resource; the child model has its own policy that would block the equivalent edit on its own screen. Confirm both layers, attach explicit rules to the attributes you expose, and write down the list of attributes editable inline.

Pin the write behaviour with tests, not assumptions. A page-renders-200 test tells you nothing here. Cover at minimum:

  • field renders with a null relation
  • field renders with a populated relation
  • inline create against a $timestamps = true child touches created_at/updated_at
  • inline create against a $timestamps = false child leaves timestamps alone

That last case is the 2.1.1 regression; a test prevents a future dependency bump from quietly undoing it.

Record the semantics in your Nova style guide. One line — "inline relationship fields may write to the related model; verify policy and timestamp behaviour" — spares the next developer the same source-reading session.

Gotchas and Tips

N+1 is the default failure mode, and development hides it. Ten rows looks fine; five hundred rows is a latency ticket. Apply eager loading before rollout, and treat any resource already fighting N+1 as blocked until that's resolved — stacking this field on top makes it worse, not obviously better.

The write path is the risk surface, not the render path. Rendering bugs are visible and embarrassing; write bugs corrupt records. Find out whether the save goes through save(). If it does, observers, model events, cache invalidation, and notification or sync jobs can fire from a modal inside someone else's form — usually desirable, occasionally a real surprise. If it bypasses save(), find out precisely what it skips.

$timestamps = false is a live case, not theoretical. Legacy tables, view-backed models, pivot-like children — any of them paired with an inline create needs an explicit test. The 2.1.1 fix exists because someone hit it in production.

No sorting, filtering, or search on the inline attribute. The related value is presentation and form-input only. If operators will immediately try to sort the index by it, this is the wrong tool — plan a real column or a custom field instead. Shipping it on a sortable-looking table is a guaranteed follow-up ticket.

Decide the null case deliberately. Rendering a placeholder is different from rendering an empty string, and inline editing against a missing relation — dangling foreign key, soft-deleted parent — raises a "create or refuse?" question the package can't answer for you. Design it rather than discovering it on a broken record.

Unique constraints appear as database errors, not nice validation messages, if the related resource doesn't declare the rule. Inline create is the path most likely to hit a duplicate, because the operator can't see the existing record list while typing. Make sure the related resource carries a unique rule on the identifying column.

Nova majors are the maintenance cliff, and semver won't warn you. A Nova 4→5 jump can break the field with no corresponding upstream release. Keep the wrapper class; if upstream goes quiet, fork — MIT permits it, the PHP class is small, and the Vue component builds locally with npm run prod. Budget the fork as a realistic fallback, not a last resort.

Debugging checklist when the field misbehaves: dump DB::getQueryLog() on the index request to confirm eager loading fired; verify the relation's cardinality matches what you asked the field to do; check whether the wrapped related field is itself index-safe (a File or nested BelongsTo drags its own cost in); run php artisan nova:publish and php artisan optimize:clear if the component renders as empty markup after a deploy; and check the browser console for a JS asset version mismatch before assuming the PHP is at fault.

Extension points, in preference order: (1) subclass and override resolution or defaults in your wrapper; (2) constrain behaviour with readonly(), onlyOnIndex(), canSee(), and rules(); (3) fork and carry the patch, normally in the PHP field class and occasionally the Vue component. Never patch vendor/ — you'll lose it on the next composer update and nobody will remember why the admin looks different.

Pin the version and read the changelog on each bump. At this cadence, releases are rare enough that a manual read takes seconds. The 2.1.1 entry is five lines and materially changed the field's risk profile — treat future entries with the same attention rather than letting composer update decide for you.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource