PHPackages                             ssntpl/data-fields - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ssntpl/data-fields

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

ssntpl/data-fields
==================

Laravel Data Fields.

v0.4.1(1mo ago)159[1 issues](https://github.com/ssntpl/data-fields/issues)[1 PRs](https://github.com/ssntpl/data-fields/pulls)PHPPHP ^8.2

Since Sep 25Pushed 1mo agoCompare

[ Source](https://github.com/ssntpl/data-fields)[ Packagist](https://packagist.org/packages/ssntpl/data-fields)[ Docs](https://github.com/ssntpl/data-fields)[ RSS](/packages/ssntpl-data-fields/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (6)Versions (16)Used By (0)

Laravel Data Fields
===================

[](#laravel-data-fields)

[![Latest Version on Packagist](https://camo.githubusercontent.com/361ab6c32356ca01bba98e21e3846edf318d5642c905528aa7fd3f8398de8d28/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73736e74706c2f646174612d6669656c64732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ssntpl/data-fields)[![Total Downloads](https://camo.githubusercontent.com/504c9a85f318a10532965f0d4b9316da1c6df1c7a959639b06451aad4cfb4465/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73736e74706c2f646174612d6669656c64732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ssntpl/data-fields)[![License](https://camo.githubusercontent.com/e6df80dba06197a9a5886925281403f959e7b84bc29f6eb150f5d9237147f68d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f73736e74706c2f646174612d6669656c64732e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

Typed, dynamic data fields for Eloquent models. Attach admin-defined fields to any model and read them as typed PHP values — booleans cast to `bool`, numbers to `float`, dates to `Carbon`, files to your `File` model, and so on.

Two parallel storage modes:

- **Cast mode** — one JSON column on the owner model holds a self-describing `DataField` document (schema + values together). Ergonomic typed object access, atomic per-column writes, multiple "forms" per model.
- **Row mode** — one row per field in a polymorphic `data_fields` table. Cross-row queries by key/value, per-field granular updates, indexable.

Pick whichever fits the column you're working with — a single model can use both modes on different attributes.

```
use Ssntpl\DataFields\Support\DataField;

class User extends Model
{
    protected $casts = [
        'preferences' => DataField::class,    // cast mode
    ];
    use \Ssntpl\DataFields\Concerns\HasDataFields;   // row mode
}

// Cast mode — work with the column as a typed document
$user->preferences = DataField::section(items: [
    ['key' => 'dark_mode', 'type' => 'bool', 'value' => true],
    ['key' => 'language',  'type' => 'text', 'value' => 'en'],
]);
$user->preferences->dark_mode->value;   // bool true
$user->preferences->dark_mode->value = false;
$user->save();

// Row mode — store fields polymorphically
$user->fields()->create([
    'key' => 'phone', 'type' => 'text', 'value' => '+91-9999900000',
]);
$user->getFieldValue('phone');          // '+91-9999900000'
```

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Choosing a mode](#choosing-a-mode)
- [Cast mode](#cast-mode)
    - [Setup](#setup)
    - [Defining and writing](#defining-and-writing)
    - [Reading values](#reading-values)
    - [Mutating values](#mutating-values)
    - [Adding and removing fields at runtime](#adding-and-removing-fields-at-runtime)
    - [Containers and nesting](#containers-and-nesting)
    - [Validation](#validation)
    - [Default values](#default-values)
    - [Conditional visibility (`visible_if`)](#conditional-visibility-visible_if)
    - [Factories](#factories)
    - [Iteration and array access](#iteration-and-array-access)
- [Row mode](#row-mode)
    - [Setup](#row-mode-setup)
    - [Working with rows](#working-with-rows)
    - [Single-key helpers](#single-key-helpers)
    - [Custom row model](#custom-row-model)
- [Field types reference](#field-types-reference)
- [File and files types](#file-and-files-types)
- [Configuration](#configuration)
- [API reference](#api-reference)
- [Common patterns](#common-patterns)
- [Migrating from 0.2.x](#migrating-from-02x)
- [Testing](#testing)
- [Security](#security)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [Credits](#credits)
- [License](#license)

Installation
------------

[](#installation)

Install via Composer:

```
composer require ssntpl/data-fields
```

Publish the config (optional — only needed if you want to override defaults):

```
php artisan vendor:publish --tag=data-fields-config
```

### Row mode

[](#row-mode)

If you're using row mode (the `data_fields` table), publish and run the migration:

```
php artisan vendor:publish --tag=data-fields-migrations
php artisan migrate
```

If you'd rather skip the publish step, set `data-fields.auto_load_migrations`to `true` in the published config — the service provider will load the package's migration directly.

### Cast mode

[](#cast-mode)

Cast mode doesn't ship a table — the consumer adds a JSON column to whichever model they want:

```
Schema::table('users', function (Blueprint $table) {
    $table->json('preferences')->nullable();
});
```

### Requirements

[](#requirements)

- PHP 8.2+
- Laravel 11.0 or 12.0
- `ssntpl/laravel-files` ^0.1 (required — used by the `file` / `files` field types)

Choosing a mode
---------------

[](#choosing-a-mode)

Both modes share the same conceptual shape (key, type, value, label, description, validations, meta) and the same casting layer. They differ in where the data lives and which read/write patterns they optimise for.

ConcernCast modeRow modeStorageOne JSON column on the owner modelOne row per field in `data_fields`Read one fieldSingle column readOne DB query (or via eager load)Read all fieldsSingle column readOne query (eager-loadable)Update one fieldWhole column rewriteOne row updateQuery across rows by fieldHard (DB-specific JSON path queries)Native SQLMultiple distinct "forms" per recordNatural (one column per form)Needs a discriminator columnContainers (`step` / `section` / `group`)First-classNot supportedConcurrent writes to different fieldsLast-write-wins on the columnField-independentDB-level constraints (FK, unique)None inside JSONNative SQLExternal (non-PHP) consumersMust understand JSON shapeTrivial — normalised rowsBest fitSettings, preferences, structured submissionsEAV, searchable attributes, sparse dataRule of thumb: **start with cast mode**. Reach for row mode when you have a real need for cross-row queries by field value, BI/reporting tooling, or field-level DB constraints.

---

Cast mode
---------

[](#cast-mode-1)

A single JSON column on the owner model holds the entire field document. The Laravel cast hydrates that JSON into a `DataField` object you can read, mutate, and persist with ordinary `$model->save()` semantics.

### Setup

[](#setup)

1. Add a `json` column to your model's table:

    ```
    Schema::table('users', function (Blueprint $table) {
        $table->json('preferences')->nullable();
    });
    ```
2. Add the cast to your model — write `DataField::class` directly; the package resolves to its internal cast via Laravel's `Castable` interface:

    ```
    use Ssntpl\DataFields\Support\DataField;

    class User extends Model
    {
        protected $casts = [
            'preferences' => DataField::class,
        ];

        protected $fillable = ['preferences', /* ... */];
    }
    ```

That's it. Each cast column can hold an entire form's worth of fields. Attach as many as you need:

```
protected $casts = [
    'preferences'       => DataField::class,
    'email_settings'    => DataField::class,
    'shipping_defaults' => DataField::class,
];
```

### Defining and writing

[](#defining-and-writing)

A column casts to a single `DataField` object. The simplest form is a container holding leaf fields:

```
use Ssntpl\DataFields\Support\DataField;

$user->preferences = DataField::section(items: [
    ['key' => 'dark_mode', 'type' => 'bool', 'value' => true],
    ['key' => 'language',  'type' => 'text', 'value' => 'en'],
]);
$user->save();
```

You can also assign a plain array — the cast coerces it to a `DataField` for you:

```
$user->preferences = [
    'type'  => 'section',
    'items' => [
        ['key' => 'dark_mode', 'type' => 'bool', 'value' => true],
    ],
];
$user->save();
```

A `null` column casts to `null`. Assigning `null` clears the column:

```
$user->preferences = null;
$user->save();
```

There's no implicit default — initialise the document explicitly via a factory or by assignment. This matches Laravel's nullable-cast contract.

### Reading values

[](#reading-values)

Property access returns the matching `DataField`; chain `->value` to read the typed value:

```
$user->preferences->dark_mode;          // DataField (leaf)
$user->preferences->dark_mode->value;   // bool true (cast via the field's type)
$user->preferences->language->value;    // 'en'
```

For nested structures, chain through container children:

```
$user->preferences->appearance->dark_mode->value;
```

Or use the explicit dotted-path lookup:

```
$user->preferences->dataField('appearance.dark_mode')->value;
```

If a key doesn't exist, property access returns `null`:

```
$user->preferences->missing_key;        // null
```

### Mutating values

[](#mutating-values)

The `DataField` object is mutable. Mutations persist when you call `$model->save()`:

```
$user->preferences->dark_mode->value = false;
$user->save();
```

Dirty tracking works through Laravel's standard cast-re-serialisation: `isDirty('preferences')` returns `true` after any in-memory change, and `save()` writes the new JSON when it differs from the original.

You can also replace an entire field by assignment:

```
$user->preferences->dark_mode = DataField::leaf('bool', false, ['key' => 'dark_mode']);
// or, equivalently, with a plain array:
$user->preferences->dark_mode = ['key' => 'dark_mode', 'type' => 'bool', 'value' => false];
$user->save();
```

### Adding and removing fields at runtime

[](#adding-and-removing-fields-at-runtime)

Containers support `addField` and `removeField`:

```
$user->preferences->addField([
    'key' => 'fontsize', 'type' => 'number', 'value' => 14,
]);

$user->preferences->removeField('language');

$user->save();
```

Adding a duplicate sibling key throws `\InvalidArgumentException` immediately — structural validation runs at the point of authorship, not at save.

### Containers and nesting

[](#containers-and-nesting)

Three container types are available — they're semantically equivalent inside the package; pick whichever your UI vocabulary prefers:

TypeTypical use`section`Logical grouping of related fields`step`A wizard/multi-step form pane`group`An inline cluster, smaller than a sectionContainers nest arbitrarily:

```
$user->preferences = DataField::section(items: [
    [
        'type' => 'group', 'key' => 'appearance', 'label' => 'Appearance',
        'items' => [
            ['key' => 'dark_mode', 'type' => 'bool', 'value' => true],
            ['key' => 'accent', 'type' => 'select_single', 'value' => 'blue',
             'options' => [['key' => 'blue'], ['key' => 'red']]],
        ],
    ],
    [
        'type' => 'group', 'key' => 'notifications', 'label' => 'Notifications',
        'items' => [
            ['key' => 'frequency', 'type' => 'select_single', 'value' => 'daily',
             'options' => [['key' => 'daily'], ['key' => 'weekly']]],
        ],
    ],
]);
```

Access nested leaves via property chains or dotted-path lookup:

```
$user->preferences->appearance->dark_mode->value;
$user->preferences->dataField('appearance.accent')->value;
$user->preferences->notifications->frequency->value;
```

### Validation

[](#validation)

Each leaf can carry inline Laravel validation rules. Run them with `validate()`:

```
$user->preferences = DataField::section(items: [
    ['key' => 'name', 'type' => 'text', 'validations' => ['required', 'min:2']],
    ['key' => 'age',  'type' => 'number', 'validations' => ['required', 'numeric', 'min:18']],
]);

try {
    $user->preferences->validate();
} catch (\Illuminate\Validation\ValidationException $e) {
    // $e->errors() — dotted paths, e.g. 'step_1.age' for nested
}
```

Notes:

- `validate()` is a **guard, not a filter** — on success it returns the values unchanged. If you want Laravel's "only validated keys" shape, call `Validator::make(...)->validated()` directly.
- For `select_single` / `select_multiple` with an `options` list, an `Rule::in(...)` rule is auto-derived so out-of-options values fail validation without you having to repeat the option keys in `validations`.
- Hidden fields (resolved via `visible_if`) are skipped — their stored values are preserved on read and not deleted on write.

To validate on save, call `validate()` inside your own `saving` observer:

```
static::saving(function ($model) {
    if ($model->preferences) {
        $model->preferences->validate();
    }
});
```

### Default values

[](#default-values)

A leaf's `default` is returned by `->value` when no `value` has been set. Explicit `null` overrides the default — callers chose to clear it.

```
$df = new DataField([
    'key' => 'plan', 'type' => 'text', 'default' => 'free',
]);
$df->value;                  // 'free' (from default)

$df->value = 'pro';
$df->value;                  // 'pro'

$df->value = null;
$df->value;                  // null (explicit override)
```

### Conditional visibility (`visible_if`)

[](#conditional-visibility-visible_if)

Mark a field as visible only when a sibling has a specific value. Currently equality-based; multiple keys mean AND.

```
DataField::section(items: [
    ['key' => 'has_phone', 'type' => 'bool', 'value' => false],
    [
        'key' => 'phone', 'type' => 'text',
        'visible_if' => ['has_phone' => true],
        'validations' => ['required'],
    ],
]);
```

Hidden fields skip validation; their stored values are preserved.

### Factories

[](#factories)

Three container shortcuts plus a generic leaf and a recursive `fromArray`:

```
DataField::section(?string $key = null, array $items = [], array $extra = []): self
DataField::step(?string $key = null, array $items = [], array $extra = []): self
DataField::group(?string $key = null, array $items = [], array $extra = []): self
DataField::leaf(FieldType|string $type, mixed $value = null, array $extra = []): self
DataField::fromArray(array $node): self
```

Examples:

```
use Ssntpl\DataFields\Support\DataField;
use Ssntpl\DataFields\Support\FieldType;

DataField::section('preferences', items: [...]);
DataField::leaf(FieldType::Date, '2026-06-15', ['key' => 'expires_at']);
DataField::leaf('number', 14, ['key' => 'fontsize', 'label' => 'Font size']);
```

### Iteration and array access

[](#iteration-and-array-access)

Containers iterate over their `items`:

```
foreach ($user->preferences as $field) {
    echo $field->key . ' = ' . $field->value . PHP_EOL;
}

count($user->preferences);          // count of items
```

ArrayAccess works by both index and key:

```
$user->preferences[0];              // first DataField
$user->preferences['dark_mode'];    // DataField with key 'dark_mode'
unset($user->preferences['dark_mode']);
```

---

Row mode
--------

[](#row-mode-1)

Use row mode when you need cross-row queries by field key/value (e.g. "find all users with `plan = 'pro'`"), per-field granular updates, BI/ reporting tools that expect normalised data, or DB-level constraints on individual fields.

### Row mode setup

[](#row-mode-setup)

Add the trait to your model:

```
use Ssntpl\DataFields\Concerns\HasDataFields;

class Product extends Model
{
    use HasDataFields;
}
```

That's it — the trait wires up the polymorphic `fields()` relationship against the package's `data_fields` table.

### Working with rows

[](#working-with-rows)

```
$product->fields()->create([
    'key'   => 'sku',
    'type'  => 'text',
    'value' => 'WIDGET-001',
    'label' => 'Stock keeping unit',
]);

// Read
$product->fields;                                      // Collection
$product->fields()->where('key', 'sku')->first()->value;

// Cast across all rows
foreach ($product->fields as $row) {
    echo $row->label . ' = ' . $row->value . PHP_EOL;
}
```

Rows store typed values: `$row->value` returns the cast PHP type (bool, float, Carbon, File, etc.) based on the row's `type`.

### Single-key helpers

[](#single-key-helpers)

The trait provides two convenience methods for working with a single field by key:

```
$product->getFieldValue('sku');                        // cast value or null
$product->setFieldValue('sku', 'NEW-001');             // upsert by key
$product->setFieldValue('weight', 2.5, 'number');      // type on first set
```

`setFieldValue` creates the row if absent and updates if present. The third argument accepts a `FieldType` enum or a raw string; it defaults to `text`on create and preserves the existing type on update.

### Custom row model

[](#custom-row-model)

Subclass `DataRow` to add custom attributes, accessors, or methods:

```
use Ssntpl\DataFields\Models\DataRow;

class CustomDataRow extends DataRow
{
    protected $extraFillable = ['source_system'];

    public function getFillable()
    {
        return array_merge(parent::getFillable(), $this->extraFillable ?? []);
    }

    public function isFromExternalSystem(): bool
    {
        return $this->source_system !== null;
    }
}
```

Point the config at your subclass:

```
// config/data-fields.php
return [
    'data_row_model' => App\Models\CustomDataRow::class,
];
```

The `fields()` relationship will now hydrate as `CustomDataRow` instances.

### Validation rules in row mode

[](#validation-rules-in-row-mode)

Storing `validations` rules alongside the field works — but note that **row mode does not auto-run those rules**. The rules are persisted as field metadata; running them is the consuming application's job (typically before calling `create()` / `update()`):

```
$product->fields()->create([
    'key'         => 'price',
    'value'       => '99.99',
    'type'        => 'number',
    'validations' => ['required', 'numeric', 'min:0'],   // stored only
]);
```

If you want auto-running rules, use cast mode — `$df->validate()` runs them.

---

Field types reference
---------------------

[](#field-types-reference)

The package supports 12 leaf types and 3 container types. All available as both literal strings and as cases on the `FieldType` PHP enum.

### Leaves

[](#leaves)

TypeStored asRead returns`bool``'1'` / `'0'` (row), native bool (cast)`bool``text``string``string``number``string` (row), `float` (cast)`float``select_single``string``string``select_multiple`JSON array of strings`array``date``'YYYY-MM-DD'` string`string``time``'HH:MM:SS'` string`string``datetime``'YYYY-MM-DD HH:MM:SS'` string`\Carbon\Carbon``file``{model_type, model_id}` JSON`\Ssntpl\LaravelFiles\Models\File` or `null``files`array of `{model_type, model_id}``array` (always a list)`json`JSONdecoded `array``array`JSON list`array`**Lenient string decoding on read** — for `json`, `array`, and `select_multiple`, the read path will `json_decode` a stored string if it encounters one (recovery path for double-encoded or migrated legacy data). If you want to store an opaque string verbatim, use the `text` type instead; `json` is for structured data and writes always store the native PHP structure.

### Containers (cast mode only)

[](#containers-cast-mode-only)

TypeNotes`step`A step/page in a wizard form`section`A logical grouping of related fields`group`An inline cluster, smaller than a sectionAll three are semantically equivalent inside the package — the choice is a hint to your UI layer.

### The `FieldType` enum

[](#the-fieldtype-enum)

For type safety in your code, use `Ssntpl\DataFields\Support\FieldType`:

```
use Ssntpl\DataFields\Support\FieldType;

FieldType::Bool->value;              // 'bool'
FieldType::SelectSingle->isLeaf();   // true
FieldType::Section->isContainer();   // true

FieldType::leaves();                 // list of leaf cases
FieldType::containers();             // list of container cases

DataField::leaf(FieldType::DateTime, now(), ['key' => 'last_seen']);
```

The enum is the in-memory type; JSON storage and the row-mode `type` column stay as strings.

---

File and files types
--------------------

[](#file-and-files-types)

The `file` and `files` types store a reference to a `File` model from the [`ssntpl/laravel-files`](https://github.com/ssntpl/laravel-files) package.

Pass a `File` instance, the package handles the rest:

```
$file = File::find(123);

$user->preferences = DataField::section(items: [
    ['key' => 'avatar', 'type' => 'file', 'value' => $file],
]);
$user->save();

$user->preferences->avatar->value;          // File instance
$user->preferences->avatar->value->url;     // works as any File
```

For `files` (multiple), pass an array — even a single `File` is wrapped to a list:

```
$user->preferences->addField([
    'key' => 'attachments', 'type' => 'files',
    'value' => [$f1, $f2, $f3],
]);

$user->preferences->attachments->value;     // array
```

An empty `files` field round-trips as `[]`, not `null`.

---

Configuration
-------------

[](#configuration)

The published config (`config/data-fields.php`) is small:

```
return [
    // Row-mode Eloquent model. Subclass DataRow and point at it to add
    // custom attributes/behaviour.
    'data_row_model' => \Ssntpl\DataFields\Models\DataRow::class,

    // Enable created_at / updated_at on the `data_fields` table.
    // Off by default — most consumers don't need per-row timestamps.
    'data_fields_timestamps' => false,

    // When true, the service provider loads the package's migration
    // directly — no `vendor:publish` needed.
    'auto_load_migrations' => false,
];
```

---

API reference
-------------

[](#api-reference)

### `Ssntpl\DataFields\Support\DataField` (cast value object)

[](#ssntpldatafieldssupportdatafield-cast-value-object)

MethodNotes`new DataField($node)` / `fromArray($node)`Construct from a node array; throws on malformed input`static leaf(FieldType|string $type, $value, array $extra = [])`Leaf factory`static section(?string $key, array $items, array $extra = [])`Section container factory`static step(?string $key, array $items, array $extra = [])`Step container factory`static group(?string $key, array $items, array $extra = [])`Group container factory`isLeaf()` / `isContainer()`Type-based predicates`isVisible(?array $siblingValues = null)`Resolves `visible_if``getValue()` / `setValue($v)`Read/write the leaf value (honours default)`$df->{$childKey}`Property access — returns child `DataField` or `null``$df->dataField($dottedPath)`Explicit path lookup, deep`$df->addField($node)` / `$df->removeField($key)`Container-only mutationIterable, `ArrayAccess`, `Countable`Walk and index children`validate()`Runs Laravel rules; throws `ValidationException``toArray()` / `jsonSerialize()`Storage-form serialisation### `Ssntpl\DataFields\Models\DataRow` (row-mode Eloquent model)

[](#ssntpldatafieldsmodelsdatarow-row-mode-eloquent-model)

MethodNotes`owner()`Polymorphic morphTo`fields()`Children via self-polymorphism (rare in practice)`duplicate()` / `duplicateInto($owner)`Clone with re-parented children`delete()`Transactional cascade to files + children### `Ssntpl\DataFields\Concerns\HasDataFields` (row-mode trait)

[](#ssntpldatafieldsconcernshasdatafields-row-mode-trait)

MethodNotes`fields()``morphMany` to `DataRow``getFieldValue($key)`Cast value or `null``setFieldValue($key, $value, $type = null)`Upsert by key### `Ssntpl\DataFields\Support\FieldType` (enum)

[](#ssntpldatafieldssupportfieldtype-enum)

MethodNotes`isLeaf()` / `isContainer()`Per-case predicates`static coerce($value)`Accept enum or string; throws on unknown`static leaves()` / `static containers()`Enumerate by kind---

Common patterns
---------------

[](#common-patterns)

### Per-environment defaults

[](#per-environment-defaults)

Use the model's `creating` event to seed a default document:

```
static::creating(function (User $user) {
    if ($user->preferences === null) {
        $user->preferences = DataField::section(items: [
            ['key' => 'language', 'type' => 'text', 'value' => 'en'],
            ['key' => 'theme',    'type' => 'select_single', 'value' => 'system',
             'options' => [['key'=>'system'],['key'=>'light'],['key'=>'dark']]],
        ]);
    }
});
```

### Validating on save

[](#validating-on-save)

Hook into `saving`:

```
static::saving(function (User $user) {
    if ($user->preferences) {
        $user->preferences->validate();
    }
});
```

### Schema defined on a parent, values stored per-child

[](#schema-defined-on-a-parent-values-stored-per-child)

When many child records share one schema (e.g., template + responses), keep the schema on the parent and store only the merged document on the child. The cast handles both shapes identically — the schema lives wherever you choose.

### Iterating leaves across containers

[](#iterating-leaves-across-containers)

`dataField('a.b.c')` looks up by full path. To walk every leaf:

```
$walker = function (DataField $node) use (&$walker, &$leaves) {
    if ($node->isLeaf()) {
        $leaves[] = $node;
        return;
    }
    foreach ($node->items as $child) {
        $walker($child);
    }
};
$leaves = [];
$walker($user->preferences);
```

---

Migrating from 0.2.x
--------------------

[](#migrating-from-02x)

The 0.4.x release is a breaking redesign. If you were on 0.2.x with the `HasDataFieldsJson` trait:

**Before (0.2.x):**

```
use Ssntpl\DataFields\Traits\HasDataFieldsJson;

class LogEntry extends Model
{
    use HasDataFieldsJson;
}

$entry->setDataFieldsSchema([
    ['key' => 'performed_by', 'type' => 'text'],
]);
$entry->setFieldValue('performed_by', 'Rahul');
$entry->save();
```

**After (0.4.x):**

```
use Ssntpl\DataFields\Support\DataField;

class LogEntry extends Model
{
    protected $casts = [
        'entry_data' => DataField::class,
    ];
}

$entry->entry_data = DataField::section();
$entry->entry_data->addField(['key' => 'performed_by', 'type' => 'text', 'value' => 'Rahul']);
$entry->save();
```

For row-mode consumers, the rename `DataField` → `DataRow` and trait namespace `Traits\` → `Concerns\` are the main changes:

```
- use Ssntpl\DataFields\Traits\HasDataFields;
+ use Ssntpl\DataFields\Concerns\HasDataFields;

- use Ssntpl\DataFields\Models\DataField;
+ use Ssntpl\DataFields\Models\DataRow;
```

Type-string constants are gone — use either the raw string (`'bool'`, `'text'`, …) or the `FieldType` enum cases (`FieldType::Bool->value`, …).

See `CHANGELOG.md` for the complete list of changes and rationales.

---

Testing
-------

[](#testing)

```
composer install
composer test          # or: vendor/bin/phpunit
```

The test suite runs against SQLite in-memory using Orchestra Testbench.

Security
--------

[](#security)

The `file` / `files` types store a reference to a row in `ssntpl/laravel-files`'s `files` table as `{model_type, model_id}` JSON. On read, the cast resolves `model_type` through Laravel's morph map (`Illuminate\Database\Eloquent\Relations\Relation::morphMap()`) and rejects any class that is not `Ssntpl\LaravelFiles\Models\File` or a subclass — so a tampered value cannot autoload arbitrary classes. If you have subclassed the File model, ensure your subclass extends `Ssntpl\LaravelFiles\Models\File`.

If you discover a security vulnerability, please email `abhishek.sharma@ssntpl.in` instead of opening a public issue.

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for a detailed record of changes per release.

Contributing
------------

[](#contributing)

Issues and pull requests are welcome at [github.com/ssntpl/data-fields](https://github.com/ssntpl/data-fields).

When sending a PR:

1. Fork the repo and create a feature branch.
2. Add tests covering the change.
3. Run `composer test` and make sure everything is green.
4. Update `CHANGELOG.md` under the `[Unreleased]` section.

Credits
-------

[](#credits)

- **Abhishek Sharma** —  —
- All [contributors](https://github.com/ssntpl/data-fields/graphs/contributors)

License
-------

[](#license)

The MIT License (MIT). See [LICENSE.md](LICENSE.md).

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance70

Regular maintenance activity

Popularity12

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 63.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~33 days

Recently: every ~63 days

Total

9

Last Release

59d ago

PHP version history (2 changes)v0.1.1PHP \*

v0.4.0PHP ^8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/6224763?v=4)[Sword Software N Technologies Pvt. Ltd.](/maintainers/ssntpl)[@ssntpl](https://github.com/ssntpl)

---

Top Contributors

[![Abhishek5Sharma](https://avatars.githubusercontent.com/u/160742025?v=4)](https://github.com/Abhishek5Sharma "Abhishek5Sharma (19 commits)")[![sambhav-aggarwal](https://avatars.githubusercontent.com/u/4591834?v=4)](https://github.com/sambhav-aggarwal "sambhav-aggarwal (7 commits)")[![JYOTSANASHARMAA](https://avatars.githubusercontent.com/u/116160861?v=4)](https://github.com/JYOTSANASHARMAA "JYOTSANASHARMAA (4 commits)")

---

Tags

laravelKey valueeavssntpldata-fields

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ssntpl-data-fields/health.svg)

```
[![Health](https://phpackages.com/badges/ssntpl-data-fields/health.svg)](https://phpackages.com/packages/ssntpl-data-fields)
```

###  Alternatives

[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17930.8k](/packages/markwalet-nova-modal-response)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[ronasit/laravel-helpers

Provided helpers function and some helper class.

2087.0k31](/packages/ronasit-laravel-helpers)[team-nifty-gmbh/tall-datatables

Server-side rendered datatables for Laravel and Livewire

1422.0k5](/packages/team-nifty-gmbh-tall-datatables)[crumbls/layup

A visual page builder plugin for Filament 5 — Divi-style grid layouts with extensible widgets.

604.0k2](/packages/crumbls-layup)[tomshaw/electricgrid

A feature-rich Livewire package designed for projects that require dynamic, interactive data tables.

119.8k](/packages/tomshaw-electricgrid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
