Skip to content
laravel-nova-examples.com

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

Reference