Edit invoice line items with the Repeater field
Use Nova's Repeater field to edit invoice line items inline, stored either as a JSON column or as related HasMany records.
- Nova
- 5.x
- Published
- Reading time
- 3 min
Invoices, quotes and order forms all have the same structure: a parent record with a variable number of line items. Before the Repeater field, this meant a separate HasMany resource and a lot of clicking. With Repeater, the line items are edited directly inside the invoice form.
Create the repeatable
A repeatable describes the fields of one row.
php artisan nova:repeatable LineItem
<?php namespace App\Nova\Repeaters; use Laravel\Nova\Fields\Currency; use Laravel\Nova\Fields\Number; use Laravel\Nova\Fields\Repeater\Repeatable; use Laravel\Nova\Fields\Textarea; use Laravel\Nova\Http\Requests\NovaRequest; class LineItem extends Repeatable { public function fields(NovaRequest $request): array { return [ Textarea::make('Description')->rules('required', 'max:255'), Number::make('Quantity')->rules('required', 'integer', 'min:1'), Currency::make('Price')->rules('required', 'numeric', 'min:0'), ]; } }
Option 1: store line items as JSON
The simplest setup stores all rows in one JSON column on the invoice.
use App\Nova\Repeaters; use Laravel\Nova\Fields\Repeater; Repeater::make('Line Items', 'line_items') ->repeatables([ Repeaters\LineItem::make(), ]) ->asJson(),
The column must be cast to array on the Eloquent model:
protected function casts(): array { return [ 'line_items' => 'array', ]; }
Choose JSON when the line items are only ever read together with the invoice, for example to render a PDF.
Option 2: store line items as related records
If you need to query line items on their own, such as reporting revenue per product, store them in their own table with asHasMany():
Repeater::make('Line Items', 'lineItems') ->repeatables([ Repeaters\LineItem::make(), ]) ->asHasMany() ->uniqueField('uuid'),
The second argument is now the relationship name (lineItems), not a column. The repeatable must tell Nova which model each row maps to and include the unique field as a hidden ID:
use Laravel\Nova\Fields\ID; class LineItem extends Repeatable { public static $model = \App\Models\LineItem::class; public function fields(NovaRequest $request): array { return [ ID::hidden('uuid'), Textarea::make('Description')->rules('required', 'max:255'), Number::make('Quantity')->rules('required', 'integer', 'min:1'), Currency::make('Price')->rules('required', 'numeric', 'min:0'), ]; } }
Do not skip uniqueField(). Without it, Nova deletes all related line items and recreates them every time the invoice is saved. Their IDs and timestamps change on every edit, and a foreign key from another table to line_items with restrict makes the save fail. With a unique column such as uuid (add it to the line_items table), Nova updates existing rows in place.
Choosing between the two
asJson() |
asHasMany() |
|
|---|---|---|
| Storage | One JSON column on the parent | One row per item in a related table |
| Query items with SQL | Awkward | Natural |
| Migration needed | One column | A table, a foreign key and a unique uuid column |
| Good for | Snapshots, documents, settings | Data you report on |