›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-custom-fields
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Defining fields
  • Field types and storage
  • Tenant scoping
  • Reading and writing values
  • Validation
  • Filtering records

Operations

  • Changing and deleting fields
  • Queries and eager loading
  • Events and listeners

Reference

  • Configuration
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Defining fields
  • Field types and storage
  • Tenant scoping
  • Reading and writing values
  • Validation
  • Filtering records

Operations

  • Changing and deleting fields
  • Queries and eager loading
  • Events and listeners

Reference

  • Configuration
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

byrcsc/laravel-custom-fields · 1.x

Changing and deleting fields.

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.

A type change is refused while values exist

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.

Dropped options

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.

Deleting a definition leaves its values

$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:prune

Orphaned 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.

What the guard cannot see

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()
  • anything inside withoutEvents()
  • a mass 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.

What to read next

  • Console commands for what prune deletes and reports.
  • Queries and eager loading for the cache the same blind spot applies to.
  • Defining fields for updating a definition safely.
PreviousFiltering recordsNextQueries and eager loading
View source

On this page

  1. A type change is refused while values exist
  2. Dropped options
  3. Deleting a definition leaves its values
  4. What the guard cannot see
  5. What to read next