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

Field types and storage.

The eleven field types, the column each is stored in, and the PHP type each returns.

Every field declares one of eleven types. The type decides three things: which column on custom_field_values holds the value, what PHP type reading it back gives you, and which validation rules the generator adds before your own.

use ByRcsc\LaravelCustomFields\Enums\FieldType;

FieldType::Text;
FieldType::Textarea;
FieldType::Email;
FieldType::Url;
FieldType::Number;
FieldType::Boolean;
FieldType::Date;
FieldType::DateTime;
FieldType::Select;
FieldType::MultiSelect;
FieldType::Json;

define() takes the enum case or its string value, so 'multi_select' and FieldType::MultiSelect are the same field. An unrecognised string throws and lists the eleven.

The types

TypeReads back asColumnIndexedNotes
textstringstring_valueYesA short string
textareastringtext_valueNoLong form; see below
emailstringstring_valueYesValidated with email
urlstringstring_valueYesValidated with url
numberint or floatnumber_valueYesdecimal(38, 12)
booleanboolboolean_valueYesAccepts true, false, 1, 0
dateCarbondate_valueYesRead back at the start of the day
datetimeCarbondate_valueYesKeeps its time
selectstringstring_valueYesNeeds options
multi_selectarray<int, string>json_valueNoNeeds options; holds a list
jsonwhatever was writtenjson_valueNoNot filterable; no generated type rules

Several types are the same shape with different validation. textarea, email, and url are all strings, and datetime is a date that also carries a time. They are separate cases because what the package checks and how it casts differ, not because they are stored differently.

Typed columns, one per row

A value row names one column and leaves the rest null. Which one is decided by the definition's type, never by the value.

custom_field_definition_id  model_type  model_id  tenant  string_value  number_value  ...
                        12  App\Customer        7    acme       premium          null
                        13  App\Customer        7    acme          null        125000

The alternative is one JSON column holding everything, and the reason against it is filtering: a comparison against a typed column is a plain indexed comparison, while a comparison against a JSON column is a path expression that no index covers and that the three supported engines disagree about.

Every typed column is rewritten on each write, not only the one in use, so a row reused after its field changed type cannot keep the old column's value beside the new one.

Why text and textarea are split

text and textarea are both PHP strings, and they are stored in different columns anyway. A short string sits in an indexed varchar. A textarea cannot, because a composite index over a column long enough to hold one exceeds what MySQL will index.

Rather than give up the index for every short string, the long type gets its own unindexed column. Pick text when you expect a line, textarea when you expect a paragraph, and know that filtering a textarea scans the values for that field.

Numbers

number_value is a decimal(38, 12), which is wide enough to hold money and precise enough not to lose cents to a float.

Reading one back gives an int where the value has no fractional part and a float where it does, so a field holding a count comes back as 9 rather than 9.0. A decimal wider than PHP's integer range stays a float, because casting it would not give back the number that was stored.

Writing accepts an int, a float, or a numeric string. A non-numeric string throws InvalidValueException.

Dates

Both date types are stored in date_value and read back as Carbon instances. date is normalised to the start of the day on read; datetime keeps its time.

Writing accepts a DateTimeInterface or a string Carbon can parse. A string it cannot parse throws InvalidValueException naming the field and the value, rather than a Carbon error from three frames down.

Booleans

Writing accepts true, false, 1, 0, '1', and '0', and nothing else.

That is exactly what Laravel's own boolean validation rule accepts, and it is narrow for that reason: the generator puts boolean on this field, so a wider gate here would mean values the trait stored happily and a form rejected.

Multi-select

A multi-select holds a list of option values, stored as JSON:

$customer->setCustomField('service_interests', ['implementation', 'training']);

$customer->getCustomField('service_interests');   // ['implementation', 'training']

Writing takes a list of strings. Anything else, including a bare string, throws InvalidValueException.

It is the only type that holds several values, and that changes two things elsewhere: filtering it means contains, and it cannot be unique.

JSON

A json field holds any structure that survives a round trip through JSON, and comes back as whatever was written.

It is the one type the generator adds no rules for. The only thing that would be true of every value is that it is JSON, which the storage already guarantees. The per-field rules you set still apply, but min, max, and regex compile to Laravel's own rules, which read the value as whatever PHP type it arrives as, so they are rarely what you want on a structure.

Two limits come with it. A json field cannot be filtered by equality, and it cannot be unique. Both are because comparing a whole JSON value differs across MySQL, PostgreSQL, and SQLite, down to key order and whitespace.

A value with no JSON form, such as a resource or a closure, throws InvalidValueException naming the field, rather than reaching the driver and failing there with a message about encoding.

Type errors are not validation errors

Handing a field a value of a shape it cannot store throws InvalidValueException, and that is a different layer from validation:

  • An array handed to a text field is a type error. It throws whether or not validate_on_write is on.
  • A string longer than the field's max is a validation failure. It throws ValidationException, and turning validate_on_write off lets it through.

With validation on, the rules run first, so most wrong-typed values are reported as validation failures before the type layer sees them. See exceptions.

What to read next

  • Reading and writing values for how values move in and out.
  • Filtering records for which types can be filtered and how.
  • Validation for the rules each type generates.
PreviousDefining fieldsNextTenant scoping
View source

On this page

  1. The types
  2. Typed columns, one per row
  3. Why text and textarea are split
  4. Numbers
  5. Dates
  6. Booleans
  7. Multi-select
  8. JSON
  9. Type errors are not validation errors
  10. What to read next