›
byrcsc/laravel-custom-fields · 1.x
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.
| Type | Reads back as | Column | Indexed | Notes |
|---|---|---|---|---|
text | string | string_value | Yes | A short string |
textarea | string | text_value | No | Long form; see below |
email | string | string_value | Yes | Validated with email |
url | string | string_value | Yes | Validated with url |
number | int or float | number_value | Yes | decimal(38, 12) |
boolean | bool | boolean_value | Yes | Accepts true, false, 1, 0 |
date | Carbon | date_value | Yes | Read back at the start of the day |
datetime | Carbon | date_value | Yes | Keeps its time |
select | string | string_value | Yes | Needs options |
multi_select | array<int, string> | json_value | No | Needs options; holds a list |
json | whatever was written | json_value | No | Not 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.
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 125000The 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.
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.
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.
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.
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.
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.
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.
Handing a field a value of a shape it cannot store throws
InvalidValueException, and that is a different layer from validation:
text field is a type error. It throws whether or not
validate_on_write is on.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.