Concepts

Collections data model

Collections store structured content with a flexible entity–attribute–value (EAV) model: a collection defines fields, items are rows, and typed values live in a separate values table with locale and position.

Related

Field type identifiers and settings JSON are documented in Field types. HTTP endpoints are covered in Collections API.

Schema overview

collections
    └── collections_fields          (schema: name, type, settings, …)
    └── collections_items           (rows; soft-deletable)
            └── collections_items_values   (EAV slots: locale, position, JSON value)

Migration: database/migrations/2026_03_29_123848_create_collections_tables.php.

collections

ColumnNotes
idPrimary key
nameDisplay name
slugUnique
is_singletonBoolean, default false
form_layoutNullable JSON: tabs + sections grouping field_ids (presentation only)
sort_orderOrdering among collections
timestamps / soft deletesYes

Model: App\Models\Collection.

collections_fields

ColumnNotes
collection_idFK → collections (cascade)
nameUnique per collection (collection_id, name)
typeFieldTypeEnum string value
translatableBoolean, default false. Not allowed for hash or relation types — see Field types
settingsJSON / jsonb bag (required, layout, type options, …)
sort_orderField order in forms

No soft deletes on fields. Model: App\Models\CollectionField.

collections_items

ColumnNotes
collection_idFK → collections (cascade)
timestamps / soft deletesYes

Model: App\Models\CollectionItem. Route binding can resolve trashed items for restore / force-delete flows.

collections_items_values (EAV)

ColumnNotes
item_idFK → items (cascade)
field_idFK → fields (cascade)
localeNullable string (max 16). Set for translatable fields; null otherwise
positionUnsigned smallint, default 0. Array-valued fields use 0..n-1
valueJSON / jsonb (cast to array/mixed on the model)

Unique slot: (item_id, field_id, locale, position) as collections_items_values_unique_slot.

EAV write behavior (CollectionItemValuesWriter): delete all value rows for the item, then insert fresh rows; null values are skipped.

Singleton vs multi-item

Modeis_singletonBehavior
Multifalse (default)Collection has many items; UI lists items
SingletontrueExactly one content row; create seeds an empty item

Details:

  • Creating a singleton collection seeds one empty item (ContentCollectionController::store).
  • Show route: singleton opens the collection content editor; multi redirects to the items index.
  • Upsert endpoint: PUT collections/{collection}/singleton-content.
  • is_singleton is immutable after create (UpdateContentCollectionRequest).
  • Creating a second item on a singleton is rejected (HTTP tests + AI ManageCollectionItems).

Soft deletes

EntitySoft delete?Cascade notes
CollectionYesSoft-delete soft-deletes items; force-delete force-deletes items; restore restores only-trashed items (Collection::booted)
CollectionItemYesRestore / force-delete routes
CollectionFieldNoHard-deleted with collection via FK
CollectionItemValueNoCascades with item/field; writer replaces all rows on sync

Locales

Source of truth: project settings (content_locales, default_content_locale, fallback_content_locales), seeded from config/collections.php until configured.

Shared to the SPA as collectionLocales / collectionLocaleMeta / defaultContentLocale.

Active locale resolution (CollectionLocaleResolver) considers query locale (must be enabled or 422), Accept-Language, default content locale, then app.locale. Used by the normalizer, rule builder, and translated display_name / note helpers on fields.

Translatable fields

When collections_fields.translatable is true, item data for that field is a locale map (e.g. { en: "…", it: "…" }), stored as separate value rows with locale set. Non-translatable fields store a single slot with locale = null. Disabling a content locale does not delete orphan rows.

Services

All under app/Services/Collections/:

ServiceResponsibility
CollectionItemValuesWritersync($item, $collection, $normalizedData) — wipe and rewrite EAV rows (per-locale and per-position for arrays)
CollectionItemValuesAssemblerassemble($item) — rebuild the in-memory data map from value rows for forms/API
CollectionItemDataNormalizernormalize($collection, $data, $creating) — coerce types (strings, numbers, bools, map GeoJSON Point/MultiPoint with legacy {lat,lng} compat, file/relation IDs, arrays, M2A blocks, hash auto-generation); apply transforms (trim, slugify, lowercase, alphabetize, leaf combining); fill default_value on create
CollectionItemDataRuleBuilderLaravel rules for data.*; skips hidden_in_form; required from settings or validation operators; assertKnownKeysOnly
FieldValidationRuleEvaluatorMaps settings.validation_rules operators to Laravel/custom rules and localized messages
CollectionItemOptionsServicePaginated id/label options for relation pickers
CollectionItemQueryServiceField-based filters for item listing (locale-aware for translatable fields)

Support helpers (not under Services):

  • App\Support\Collections\CollectionItemDataAccessor — locale fallback reads via assembler
  • App\Support\Collections\CollectionLocaleResolver — active locale
  • App\Support\Collections\UniqueCollectionSlugGenerator — unique slugs

Data round-trip

HTTP / Inertia payload (data: { fieldName: … })


CollectionItemDataNormalizer


CollectionItemDataRuleBuilder  (validation)


CollectionItemValuesWriter     (EAV rows)


CollectionItemValuesAssembler  (read path → data map)
Previous
Effective permissions