›
›
›
  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

Troubleshooting.

Work out why a field is invisible, a value did not save, or a filter returned nothing.

A field the tenant should see is unknown

UnknownCustomFieldException means the current tenant has no definition under that key for that model. Check them in this order:

Which tenant is current. The message says. A queued job, a console command, or a request that has not resolved a tenant yet all report null, and a field defined for acme is invisible under any of them.

app(ByRcsc\LaravelCustomFields\Contracts\TenantResolver::class)->currentTenant();

Which model type the definition was written against. Definitions are stored against the morph class. Adding a morph map after fields were defined leaves the old rows keyed by class name and the lookup asking for the alias:

ByRcsc\LaravelCustomFields\Models\CustomFieldDefinition::query()
    ->where('key', 'account_tier')
    ->pluck('model_type', 'tenant');

Whether the manager is holding a stale set. A definition written by a mass update, inside withoutEvents(), or with saveQuietly() never reached it. Call CustomFields::flush() and read again. See keeping the cache honest.

A field is the wrong type or has the wrong options

A tenant field with the same key shadows the global one, so you may be reading a different definition than the one you defined. Ask which one is winning:

$definition = CustomFields::definition(Customer::class, 'account_tier');

[$definition->tenant, $definition->type, $definition->options];

A null tenant there means the global field is in play and this tenant has not overridden it. See the override rule.

A value did not save

Nothing was written at all. setCustomFields() resolves every key and validates every value before writing any of them, so one unknown key or one invalid value in the array means none of them landed. The exception says which.

The record was never saved. UnsavedModelException. Values are stored against the record's key.

The write was validated away. A ValidationException from setCustomField() is the field's own rules, not your form's. Read the rules the field carries:

CustomFields::definition(Customer::class, 'account_tier')->rules;

A stale instance is being read. setCustomField() clears the loaded relation on the instance it was called on. A second instance of the same record, loaded before the write, still holds the old values. Call fresh().

A filter returns nothing

The records are reading a default. Filters match value rows, and a record that never set the field has none. A default is not stored and not filterable. See defaults are not matched.

The value was written under a different tenant. Value rows are scoped to the tenant that wrote them, including for a global field, so a filter under globex does not see what acme wrote.

The comparison value is a different shape. Filter values go through the same casting a write does, so this is rarer than it looks, but a date written with a time and filtered as a whole day will not match. A date field is normalised to the start of the day; a datetime keeps its time.

It is a multi-select. Filtering one means contains, and it takes one option rather than a list. See multi-select fields.

A listing is slow

Check the eager load first:

Customer::query()->with('customFieldValues')->paginate();

Without it, every record loads its own values. With it, a page costs one extra query however many records it holds.

If the filter is the slow part, check which column the field uses. text_value and json_value carry no index, so filtering a textarea or a multi_select scans the value rows for that field. See which filters use an index.

A type change is refused

CannotChangeFieldTypeException names how many values are in the way. Clear them, or migrate them, then change the type. See changing and deleting fields.

If the field is new and the values are test data, $definition->values()->delete() is the whole fix.

Values are still in the table after deleting a field

That is deliberate. Deleting a definition leaves its values, so a field removed by mistake does not take the data with it. Run custom-fields:prune when the removal was meant, and check with --dry-run first.

The migration will not run

An unsupported model key type. custom-fields.model_key_type takes int, uuid, ulid, or string, and throws an InvalidArgumentException naming the four.

An index name that is too long. Both migrations name their indexes from the table name. A long custom table name can still push one past MySQL's 64-character limit, and PostgreSQL truncates at 63 without saying so. Shorten the table name.

The table names changed after migrating. The package migration creates the tables named in config at the time it runs. Renaming them afterwards is a migration of your own.

The wrong tenant resolver is in use

InvalidTenantResolverException means the configured class is missing or does not implement the contract, and it names what it found.

Silence is the other failure. A spatie application with the config key left at null gets SpatieTenantResolver by detection, and an application without that package gets NullTenantResolver, under which every field is global. If fields are unexpectedly shared across tenants, check which resolver is bound:

app(ByRcsc\LaravelCustomFields\Contracts\TenantResolver::class)::class;

What to read next

  • Exceptions for every failure and what triggers it.
  • Tenant scoping for what the resolver's answer changes.
  • Testing for reproducing any of the above in a test.
PreviousTesting
View source

On this page

  1. A field the tenant should see is unknown
  2. A field is the wrong type or has the wrong options
  3. A value did not save
  4. A filter returns nothing
  5. A listing is slow
  6. A type change is refused
  7. Values are still in the table after deleting a field
  8. The migration will not run
  9. The wrong tenant resolver is in use
  10. What to read next