›
byrcsc/laravel-custom-fields · 1.x
Three guarantees govern what happens to stored values when a definition changes or is removed.
Fields change after records have already filled them in. Three rules govern what happens to those values, and all three exist to stop data disappearing quietly.
Each type is stored in its own column, so changing a field's type points it at a column its values are not in.
$definition = CustomFields::definition(Customer::class, 'annual_revenue');
$definition->type = FieldType::Text;
$definition->save();The [annual_revenue] field cannot change from number to text while 3 values
exist. Migrate or clear them first, then change the type.CannotChangeFieldTypeException names the field, both types, and how many
values are in the way. The alternative to refusing is a best-effort cast that
nulls whatever it cannot convert, which loses data at the moment nobody is
watching for it.
Every other change to a definition with values goes through untouched: label, description, default, options, rules, section, and sort order.
To change the type, deal with the values first. Clearing them is one query:
$definition->values()->delete();
$definition->type = FieldType::Text;
$definition->save();Migrating them is a loop you write, reading each row's old column and writing the new one. There is no command for it, and there is not planned to be unless the refusal starts blocking real work.
Changing the type while no values exist is fine, and so is changing it again once the values are gone.
Removing an option from a select or multi_select does not rewrite the
records already holding it. They keep reading it back:
$definition->options = ['standard', 'premium'];
$definition->save();
$customer->getCustomField('account_tier'); // still 'enterprise'The value was valid when it was written. Validation applies on the next write,
so writing 'enterprise' again is refused, and a multi-select keeps every element
it holds, including the dropped ones, until something writes over it.
This means the values of a field are not guaranteed to be a subset of its current options. Code that maps a stored value onto an option list should handle a value that is not in it, rather than assuming the two are in step.
$definition->delete();The value rows stay in the table. There is no foreign key between the two tables and no cascade, so removing a field by mistake does not take the data with it. The rows are unreachable: nothing resolves the field any more, so the trait cannot read them.
Restoring the field means defining it again and pointing the old rows at the new definition's id, which is a query you write. Nothing about the values changed while the field was gone.
When the removal was meant, one command takes the orphans:
php artisan custom-fields:prune --dry-run
php artisan custom-fields:pruneOrphaned is the whole test. A value whose definition is still there is never touched, whatever tenant it belongs to and whether or not anything reads it. See console commands.
The type-change guard hangs on the definition model's updating event, which is
what a change made through the model fires. Three kinds of write fire no event
and go straight past it:
saveQuietly()withoutEvents()CustomFieldDefinition::query()->update()A type change made that way is written, and its values are left pointing at the wrong column. There is nothing the package can do about it, so treat the model as the way to change a definition.
The same three bypass the resolved-definitions cache, which is
what CustomFields::flush() is for.