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
| Column | Notes |
|---|---|
id | Primary key |
name | Display name |
slug | Unique |
is_singleton | Boolean, default false |
form_layout | Nullable JSON: tabs + sections grouping field_ids (presentation only) |
sort_order | Ordering among collections |
| timestamps / soft deletes | Yes |
Model: App\Models\Collection.
collections_fields
| Column | Notes |
|---|---|
collection_id | FK → collections (cascade) |
name | Unique per collection (collection_id, name) |
type | FieldTypeEnum string value |
translatable | Boolean, default false. Not allowed for hash or relation types — see Field types |
settings | JSON / jsonb bag (required, layout, type options, …) |
sort_order | Field order in forms |
No soft deletes on fields. Model: App\Models\CollectionField.
collections_items
| Column | Notes |
|---|---|
collection_id | FK → collections (cascade) |
| timestamps / soft deletes | Yes |
Model: App\Models\CollectionItem. Route binding can resolve trashed items for restore / force-delete flows.
collections_items_values (EAV)
| Column | Notes |
|---|---|
item_id | FK → items (cascade) |
field_id | FK → fields (cascade) |
locale | Nullable string (max 16). Set for translatable fields; null otherwise |
position | Unsigned smallint, default 0. Array-valued fields use 0..n-1 |
value | JSON / 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
| Mode | is_singleton | Behavior |
|---|---|---|
| Multi | false (default) | Collection has many items; UI lists items |
| Singleton | true | Exactly 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_singletonis immutable after create (UpdateContentCollectionRequest).- Creating a second item on a singleton is rejected (HTTP tests + AI
ManageCollectionItems).
Soft deletes
| Entity | Soft delete? | Cascade notes |
|---|---|---|
| Collection | Yes | Soft-delete soft-deletes items; force-delete force-deletes items; restore restores only-trashed items (Collection::booted) |
| CollectionItem | Yes | Restore / force-delete routes |
| CollectionField | No | Hard-deleted with collection via FK |
| CollectionItemValue | No | Cascades 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/:
| Service | Responsibility |
|---|---|
CollectionItemValuesWriter | sync($item, $collection, $normalizedData) — wipe and rewrite EAV rows (per-locale and per-position for arrays) |
CollectionItemValuesAssembler | assemble($item) — rebuild the in-memory data map from value rows for forms/API |
CollectionItemDataNormalizer | normalize($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 |
CollectionItemDataRuleBuilder | Laravel rules for data.*; skips hidden_in_form; required from settings or validation operators; assertKnownKeysOnly |
FieldValidationRuleEvaluator | Maps settings.validation_rules operators to Laravel/custom rules and localized messages |
CollectionItemOptionsService | Paginated id/label options for relation pickers |
CollectionItemQueryService | Field-based filters for item listing (locale-aware for translatable fields) |
Support helpers (not under Services):
App\Support\Collections\CollectionItemDataAccessor— locale fallback reads via assemblerApp\Support\Collections\CollectionLocaleResolver— active localeApp\Support\Collections\UniqueCollectionSlugGenerator— unique slugs
Data round-trip
HTTP / Inertia payload (data: { fieldName: … })
│
▼
CollectionItemDataNormalizer
│
▼
CollectionItemDataRuleBuilder (validation)
│
▼
CollectionItemValuesWriter (EAV rows)
│
▼
CollectionItemValuesAssembler (read path → data map)