optimistdigital/nova-simple-repeatable
A Laravel Nova field package for adding simple repeatable rows to resource forms. It lets editors create, reorder, and remove structured JSON-backed entries without a related model, making flexible repeated inputs easy to manage in Nova.
Architecture fit. This is a Nova form field, not a standalone library — it does nothing outside a Nova resource, which sharply bounds its architectural footprint. The integration surface is one nullable json column on an existing table plus a field definition; no child table, no Eloquent relationship, no new model, no shipped migration. The real decision it encodes is storage shape: JSON blob vs. normalized child model. It fits display-bound, human-edited, bounded row sets (opening hours, FAQs, spec tables, contact blocks, feature bullets). It explicitly does not fit anything requiring per-row identity, per-row permissions, audit history, filtering/joining/aggregation, or a public API contract.
Integration feasibility. Three steps: add a nullable JSON column, cast it 'array' on the model (non-optional — without it, saves blow up or read back as null), and declare SimpleRepeatable::make(...)->fields([...]) in the resource. Effort is hours-to-days per resource, and the dominant cost is editor UAT and validation design, not code. Two gating prerequisites: a licensed Nova install (a commercial cost that dwarfs the package), and a Nova major inside the supported matrix. The vendor namespace moved (optimistdigital → outl1ne), so confirm which Packagist name resolves for your Nova version and pin an explicit constraint.
Technical risk. Structurally a small-maintainer package — 75 stars, 0 dependents, effectively unvalidated in the wild. Principal risks: Nova-major upgrade lag (the field will trail Nova releases), no row identity (rows are ordered array entries; removing one renumbers the rest), last-write-wins concurrency with no diff or version, file uploads inside rows leaking storage when rows are deleted, BelongsTo inside rows storing bare IDs with no FK, and likely-unsupported reactive/nested inner fields. The 3.0.1 notes list ->stacked() and an unspecified "updated packages" — treat dependency refreshes as unreviewed until you diff the lockfile.
Key questions to force before committing scope.
Stack fit. Laravel + Nova only. Composer dependency with no meaningful provider configuration and no migrations. Works best where the product lives on admin/ops throughput — content sites, catalogs, internal back-office tools — because it converts a schema-plus-UI workstream into a dependency, a column, and UAT.
Migration path. Forward: one nullable JSON column per use case, cast, field definition. Exit: the JSON stays human-readable, so normalizing later is a decode-and-backfill job — a migration, not a redesign. MIT licensing means the worst case is a fork, not a rewrite. Mitigate exit cost by keeping the resource definition thin — put the inner field set on a static method on the model so a fork or swap is a contained change.
Compatibility. Pin to the exact tag verified against your Nova major; never float the constraint. Verify two things in a branch before designing screens around them: (1) that inner ->rules() validation actually surfaces at the right level in your Nova version — nested validation is the classic weak spot in fields like this; (2) whether dependsOn / conditional visibility keyed off sibling row values works, since reactive and nested-repeatable behavior is the usual breakage point. Treat ->stacked() as cosmetic only — it changes layout, not the stored shape or ordering.
Sequencing.
WHERE or JOIN.make() calls so JSON keys are explicit rather than label-derived.->fill() bypass the form.Maintenance. Low steady-state: no tables to prune, no migrations to run, small surface area. The recurring cost is upgrade ownership — at each Nova major this field will lag, and someone must validate or fork. Cache clearing (view:clear, config:clear, nova:publish) is the first step after any install or upgrade; a blank or unstyled field is almost always compiled assets.
Support. Load shifts from developer tickets to editor UX questions, which is the point. Contain it cheaply by publishing an explicit "do not use this field for X" list at adoption time — that stops the shortcut from spreading into pricing, eligibility, reporting, or integration data where it becomes expensive to unwind. Standardize a first-line debug order: network payload → raw column value → model attribute after fresh() → Vue rendering. That resolves most reports in under a minute and tells you whether it's a field bug or a cast/shape problem.
Scaling. Editorial scale only — tens of records, tens of rows each. The entire document is rewritten on every save, and MySQL JSON is bounded by max_allowed_packet, so thousands of rows is not viable. Also audit whether the column lands in any API response; a large blob there is a real payload cost. If you find yourself writing the third whereJsonContains, that is the signal to migrate to a child model — the documented escape hatch is a generated column extracting and indexing the value.
Failure modes to plan for.
array cast → "array to string conversion" on save, or a JSON-encoded string decoding to null on read.null vs [] ambiguity after "delete all rows" → normalize in one accessor rather than branching in fifteen places.BelongsTo IDs inside rows → application-level integrity check in an observer, or avoid relational fields in rows.->searchable() or filters on the column producing nonsense matches and slow scans.Ramp-up. Low for a Laravel developer — a few hours to a first working field; the learning that matters is shape discipline and validation layering, and it transfers across every subsequent use. Budget separately for editor UAT, which is the real schedule item. Rough sizing: half a day for the pilot field plus tests, plus UAT; subsequent resources materially faster. Because the bet is cheap, reversible, and contained, the correct posture is to adopt it tactically, name an owner, and price the JSON-to-normalized migration into the estimate now rather than discovering it at reporting time.
How can I help you explore Laravel packages today?