marshmallow/nova-flexible
marshmallow/nova-flexible is a Laravel Nova package that provides a Flexible field for defining repeatable content blocks with nested fields, stored as JSON. It’s useful for page builders, dynamic layouts, and flexible editorial content in Nova.
Install via Composer alongside Laravel Nova and the WhiteCube flexible content foundation:
composer require marshmallow/nova-flexible:^5.5.1
Since this package is explicitly built on top of WhiteCube's flexible package, the first thing to do is read the WhiteCube documentation first — the layout system, field definitions, and storage model come from there. The Nova field is largely a UI wrapper. Treat the WhiteCube docs as your primary reference and this package's README as a thin adapter layer.
Pin at least 5.5.1: it contains a fix for Layout compatibility with HasAttributes and Media fields (PR #446). Earlier 5.5.x releases can misbehave when a layout class uses attribute traits or embeds a Media field.
Minimal first use case:
vendor/marshmallow/nova-flexible for a config/ or database/ directory — don't assume a service provider auto-registers them).App\Nova\Flexible\Layouts\HeroSection) extending the layout base class from the WhiteCube package.Flexible::make('Content')
->addLayouts([
HeroSection::class,
TextBlock::class,
]),
Where to look first: the package's src/ for the Field class, then the WhiteCube package's Layout and Flexible classes it depends on.
Layout-as-class pattern. The idiomatic workflow is one PHP class per content block. Each layout class declares its own Nova fields and a name()/title()/key(). This keeps the Nova form, the storage schema, and the frontend partial in sync:
class HeroSection extends Layout
{
public function name() { return 'Hero'; }
public function fields() {
return [Text::make('Heading'), Image::make('Background')];
}
}
Media inside layouts. Attaching a Media field (or any field that relies on model attributes / HasAttributes-style accessors) directly inside a layout is supported as of 5.5.1. If you inherited layouts that mix media handling with trait-driven attributes, upgrade first before debugging — the symptom on older versions looks like a field-level bug rather than a version mismatch.
Repeater vs. Flexible. Use the repeater field for homogeneous lists (FAQ items, team members) and the flexible field for heterogeneous, editor-controlled page sections. Mixing the two — a flexible layout that contains a repeater — is the common real-world pattern (e.g. a "Pricing" section with a repeater of plan rows).
Resource → presenter → Blade pipeline. Store the flexible data on the model, expose a presenter/accessor that maps each layout to its own Blade partial (resources/views/sections/{layout-key}.blade.php), and render server-side. This is the pattern WhiteCube recommends and the one that survives Nova upgrades best.
Seeding and testing. Build factories that construct layout instances programmatically, not raw JSON. That way when you rename a field inside a layout class, your seeders fail loudly rather than silently writing orphan keys.
Integration tip: keep layout classes in a dedicated namespace (App\Nova\Flexible\Layouts) and register them via a single array in config or a service provider. Centralizing the layout list makes it trivial to feature-flag or conditionally expose sections per model type.
Adoption is small but the package is actively patched — verify each release yourself. 5.5.1 is a community PR (#446), not a maintainer-led rewrite: read the diff before adopting. Open composer.json and compare its Nova version constraint and the WhiteCube flexible version it pins against your app's versions. Nova's major-version compatibility is the most common breakage point.
Read the vendor source. With this adoption level, the README may be incomplete or stale. The field's resolve() and fill()/save() methods are short — read them to confirm exactly how data is stored (single JSON column vs. a flexible_* table). This prevents nasty surprises during model serialization or ->with() eager loads.
Do not assume the WhiteCube Nova field is interchangeable. The WhiteCube nova-flexible-content and this package both sit on the same foundation but their public APIs (method names like addLayouts vs. addLayout, maxLayouts, confirmRemoval) may differ. Copy-pasting snippets from WhiteCube's Nova docs into this field is a common source of "method not found" errors.
Version pin aggressively. Pin the exact version in composer.json and never let a ^ constraint update unattended. Patch releases here can change layout-level behavior (HasAttributes, Media fields) with no API surface change, so semver alone won't tell you whether a deploy is safe. Read the changelog for every bump and smoke-test one Media-bearing layout and one trait-using layout before shipping.
Debugging empty fields. If a flexible field renders but doesn't persist, the usual culprits are: (a) the layout's key() returning an unstable value across requests, (b) a missing layout class registered on the field, (c) the underlying migration/pivot not run, or (d) attribute access silently failing inside a layout (see 5.5.1). Log $model->getRawOriginal('content') right after save to confirm what was actually written.
Media field sanity check. After any upgrade touching layouts, load a record with a media-bearing layout in Nova and confirm both the existing stored value and a fresh upload round-trip. Regression here surfaces as an empty preview rather than an exception, so it will not fail your test suite unless you assert on it.
Extension points. Prefer overriding layout base methods and adding custom Nova fields inside layouts rather than forking the package's Field class. If you must alter the field's behavior, publish and override — but track the diff, since upstream (WhiteCube) changes you'll want to inherit will conflict with local modifications.
Migration safety. Flexible content often lands in a single JSON column. That's great for editors and painful for SQL reporting. If business requirements include querying by section type or reporting on nested values, add a normalized index/projection column alongside, or accept that reporting will go through PHP.
Frontend fallback. Always guard Blade partials against missing layouts (e.g. a section type that was removed from code but still exists in saved content). A class_exists/view()->exists() check in your renderer turns a fatal error into a silent skip — far better than a 500 on an old page during a deploy.
How can I help you explore Laravel packages today?