All notable changes to ssntpl/data-fields are recorded here.
The format follows Keep a Changelog, and the package adheres to Semantic Versioning.
DataRow::duplicate()— the previous behaviour persisted an orphan row withowner_id = 0andowner_type = '', which was junk-shaped state (a saved row that didn't belong to any real owner). UseDataRow::duplicateInto($owner)to clone onto a real owner, or Laravel's built-in$row->replicate()if you want an unsaved in-memory copy.
- README field-types table now calls out the lenient string-decode path
for
json/array/select_multiplereads. If a stored value happens to be a JSON-encoded string (double-encoded / migrated legacy data), the read decodes it. Usetextfor opaque strings.
A ground-up rework of the JSON-mode storage. All 0.3.x development rolls into this release. Pre-release package — no compatibility shim is provided.
A Laravel custom cast attached to any JSON column, replacing the
trait-based JSON mode. Each cast column holds one self-describing
DataField document that carries schema and values together — no
separate schema column, no envelope, no clearDataFieldsCache.
Ssntpl\DataFields\Support\DataFieldvalue object. ImplementsCastable,IteratorAggregate,JsonSerializable,ArrayAccess,Countable. One class covers leaf and container shapes;typediscriminates.Ssntpl\DataFields\Casts\DataFieldCastLaravel cast — never imported directly; consumers writeDataField::classin$castsand the package resolves viaCastable::castUsing().- Factories:
DataField::leaf(),DataField::section(),DataField::step(),DataField::group(),DataField::fromArray(). - Property proxy through containers —
$user->user_settings->dark_mode->valuereads/writes the leaf. Dotted-path lookup via$df->dataField('a.b.c'). - Mutation:
addField(),removeField(),setValue(),__set,ArrayAccesswrites. Dirty tracking via re-serialise-on-save (Laravel'sAsArrayObjectpattern) — the cast object stays decoupled from its parent model. - Structural validation runs at construction time — bad type, missing key,
duplicate sibling key, malformed
options/visible_ifall throwInvalidArgumentExceptionat the point of authorship. - Rule validation explicit via
$df->validate()— runs Laravel's validator against the tree with dotted error paths andRule::in()auto-derivation forselect_*leaves; hidden fields (visible_if) skipped.
All 15 type strings (12 leaves + 3 containers) live in
Ssntpl\DataFields\Support\FieldType as a backed enum. Used in
DataField, ValueCaster, and consumer-facing signatures. JSON storage
and the row-mode type column stay as strings — the enum is a PHP-side
type-safety / exhaustiveness aid.
HasDataFields::getFieldValue($key)/setFieldValue($key, $value, $type = null)upsert-by-key helpers.setFieldValueaccepts either aFieldTypeenum or a raw string.data-fields.auto_load_migrationsconfig flag — whentruethe service provider registers the package migration directly, novendor:publishneeded.data_fieldstable now indexed on(owner_id, owner_type, key)instead of just(owner_id, owner_type)so$owner->fields()->where('key', X)lookup is index-covered. The composite serves the owner-only prefix too; the previous index is redundant. Folded into the create migration — no upgrade migration needed.
DataField(Eloquent model) renamed toDataRow. The old name belongs to the new cast value object.DataRowis inSsntpl\DataFields\Models\DataRow. Type constants (DataField::BOOLetc.) are gone — useFieldType::Booletc. Custom subclasses extendDataRow(notDataField).HasDataFieldstrait moved fromTraits\toConcerns\. Full path:Ssntpl\DataFields\Concerns\HasDataFields.ValueCasteracceptsFieldType|stringat every entry point (castForRead,castForWrite,castNativeRead,castNativeWrite). String input is coerced viaFieldType::coerce(). Backward compatible for callers passing raw type strings.FieldValueCastrenamed toRowValueCast(Ssntpl\DataFields\Casts\RowValueCast). The old name was ambiguous between row-mode and the newDataFieldCast; the new name makes it explicit that this cast is for thevaluecolumn on aDataRow. It now usesFieldType::Text->valueas the fallback string when thetypeattribute is missing.data_fieldstable schema aligned with the cast-modeDataFieldshape:- Added
label(string, nullable) — the short display string. Previously thedescriptioncolumn was overloaded for this; nowlabelanddescriptioncarry the two distinct concerns from cast mode (label= short,description= long-form helper text). - Renamed
meta_datacolumn →metato match the cast-modemetakey. DataRow::$fillableand$castsupdated accordingly. Migration is a fresh create — no upgrade migration needed.
- Added
DataRow::delete()cascades wrapped inDB::transaction()— a parent- delete failure no longer leaves orphaned child rows or detached files.ValueCasterno longer infers single-vs-multi file shape from the decoded structure. The declared$type(FILE vs FILES) drives the branch. FILES always hydrates as a list (empty list preserved instead of returning null), and a singleFilepassed to a FILES field is wrapped as a list on write.ServiceProvider::boot()gatespublishes()behindrunningInConsole().- Config key renamed:
data_field_model→data_row_model(default nowDataRow::class). - Config keys removed:
json.default_schema_column,json.default_values_column,json.envelope_version,json.write_envelope,json.strict_writes(no longer needed in cast mode).
Ssntpl\DataFields\Traits\HasDataFieldsJsontrait — replaced by theDataFieldcast.Ssntpl\DataFields\Support\JsonModeMigrationhelper — consumer just writes$table->json('column_name')->nullable().Ssntpl\DataFields\Support\SchemaValidator— structural validation folded intoDataField::__construct.Ssntpl\DataFields\Support\FieldValueandSsntpl\DataFields\Support\DataSetValue— superseded byDataField.Ssntpl\DataFields\Contracts\FieldLike— row mode and cast mode now have distinct, storage-specific call sites; no more storage-agnostic iteration use case.Ssntpl\DataFields\Traits\Traits\andContracts\namespaces removed (now empty after deletions).clearDataFieldsCache()method — cast object holds no internal memoisation.- Envelope handling (
{"version": "1.0", "schema": ...}wrapping) — not needed in the merged-document shape. strict_writesconfig — cast is strict by construction (unknown keys in input throw).
\Throwablecatch-all inValueCaster::resolveFiles()— real DB errors now propagate instead of being silently swallowed. Missing rows still returnnull(Eloquentfind()semantics).data_setstable,DataSetmodel,HasDataSetstrait,HasDataSetsJsontrait,DataSetValuevalue object,data_set_model/data_sets_timestampsconfig keys. Grouping is a UX/domain concern — see container types in cast mode.
Before:
class LogEntry extends Model
{
use HasDataFieldsJson;
}
$entry->setDataFieldsSchema([
['key' => 'performed_by', 'type' => 'text'],
]);
$entry->setFieldValue('performed_by', 'Rahul');
$entry->save();After:
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 the two-column shape (data_fields_schema + data_fields_values),
write a one-off script that merges them per row:
foreach (LogEntry::cursor() as $entry) {
$schema = (array) ($entry->data_fields_schema['schema'] ?? $entry->data_fields_schema ?? []);
$values = (array) ($entry->data_fields_values['values'] ?? $entry->data_fields_values ?? []);
$entry->entry_data = DataField::section(items: array_map(
fn ($leaf) => array_merge($leaf, ['value' => $values[$leaf['key']] ?? null]),
$schema,
));
$entry->save();
}Replace Ssntpl\DataFields\Models\DataField → Ssntpl\DataFields\Models\DataRow
and Ssntpl\DataFields\Traits\HasDataFields →
Ssntpl\DataFields\Concerns\HasDataFields. Replace DataField::BOOL etc.
with FieldType::Bool->value (or just the literal strings — they haven't
changed).
First release after the package was extracted from the taillog project to be generic and reusable.
A parallel storage mode that keeps a form's schema and values as JSON columns on
the owner model itself, rather than as rows in data_fields / data_sets. Row
mode is unchanged.
HasDataFieldsJsontrait — schema + values get/set with envelope handling, dotted-pathgetFieldValue/setFieldValue, hydrateddataFields()/dataField(),visible_ifequality resolution with hidden-field-kept-on-read semantics, lenient and strict write modes via config flag, andvalidateDataFields()driving Laravel's validator from rules collected from the schema (with auto-derivedin:rules forselect_*leaves).HasDataSetsJsontrait —dataSets()/dataSet()walking arbitrary-depthstep/section/groupcontainers, preserving schema order.FieldLikeinterface implemented by both row-modeDataFieldand JSON-modeFieldValueso consumer code is storage-agnostic.FieldValueandDataSetValuereadonly value objects returned by hydration (plain PHP objects, not Eloquent subclasses — no leakysave()path).ValueCasterservice — single source of truth for casting in both modes. Honoursconfig('files.model')so consumers who subclass or replace the laravel-filesFilemodel can store and resolve their own class.SchemaValidator— validates a schema array is well-formed (duplicate keys, container shape, reservedversionkey, malformedoptions/visible_if, unknown leaf types). Exposed viavalidateDataFieldsSchema()on the trait.JsonModeMigration::addColumns($table)migration helper.config('data-fields.json')block — column defaults, envelope version, envelope-on-write flag,strict_writesflag.- Per-instance memoisation of schema and values reads. Setters invalidate; raw
attribute writers should call
clearDataFieldsCache().
- Pinned
php ^8.2andlaravel/framework ^11.0|^12.0(was wildcard). - Test suite: PHPUnit 11 + Orchestra Testbench on sqlite in-memory. 71 tests, 184 assertions covering both storage modes.
docs/JSON_MODE_SPEC.md— full canonical specification of the JSON-mode schema and values shape, locked decisions, and remaining open items.- README section 6 documenting the JSON-mode API end-to-end.
DataField::duplicate()— fixes the misnamed$newDataSetvariable and rewrites the recursion to a single-INSERTduplicateInto()per child (was double-saving each duplicated child).DataSet— dropsidfrom$fillableso the primary key can no longer be mass-assigned.HasDataSets—dataSets()is now the canonical morphMany method;data_sets()is kept as a deprecated alias delegating to it.FieldValueCast— refactored to forward toValueCaster. Bool values now store as canonical'1'/'0'strings (was a PHP bool, which platform PDO drivers were free to round-trip as'f'/'t'etc —(bool) 'false' === truein PHP made the round-trip silently corrupt). Date / time / datetime read paths now catchCarbon\Exceptions\InvalidFormatExceptionand returnnullso a malformed DB row can't crash a Builder query.- Migrations converted to anonymous-class form so the test suite can re-run them without class-redeclaration errors. Existing consumers' published migrations are unaffected.
FieldValueCastfile resolution previously called$data['model_type']::find(...)on a class string read from stored data — an autoload-arbitrary-class vector. Resolution now routes through Laravel's morph map and rejects any class that is not the configured file model (or a subclass of it).
Legacy state extracted from existing projects. Single storage mode (row), no test suite, no published spec. Not recommended for new projects — use 0.2.0 or later.