Skip to content

Custom Fields ​

Field groups and custom fields, compatible with Advanced Custom Fields (ACF) data.

kitloom/custom-fields · admins: kitloom/custom-fields-filament, kitloom/custom-fields-nova

Video lessons ​

Recorded in the Filament admin; the Nova admin works the same way.

Custom Fields

Custom fields on the data model of Advanced Custom Fields: field groups and fields are rows laid out as the plugin and its free fork Secure Custom Fields lay them out, values sit under the same meta keys. A site moved from WordPress keeps its fields and values; lists and maps kitloom writes are JSON, what the plugin serialized is read as well.

The admins live in kitloom/custom-fields-filament and kitloom/custom-fields-nova; the frontend needs only this package.

Where to find it ​

Custom Fields → Field Groups in the admin, with Add New Field Group under it, as in the plugin's menu. The screens need the "Manage field groups" permission (manage_field_groups), which administrators have. How it works at the end of the section is this page; with kitloom/post-types the section also holds Post Types, Taxonomies and How post types work.

Making a field group ​

A field group is a set of fields that shows on the edit form of the records its location rules point at: "Book details" on books, "Hero" on the front page, "Social links" on every user.

  • Title — the heading of the block on the record form.
  • Active — an inactive group keeps its fields and their values but shows nowhere.
  • Fields — each has a Field Label (what the editor sees), a Field Name (the key its value is stored under: one word, underscores and dashes allowed — renaming it later leaves the old values behind) and a Field Type. The type's own settings follow: choices, limits, defaults, what a picker may choose. Instructions are shown to whoever fills the field; Required refuses an empty value; Conditional Logic shows the field only while other fields have the values you set. A group, repeater or flexible content field holds fields of its own.
  • Location Rules — "show this field group if": each rule is a parameter (post type, page template, taxonomy, user role…), an operator and a value. Rules in one rule group must all hold; any one rule group is enough.
  • Style — Standard draws the group as a box with its title; Seamless puts the fields on the form without a box or title.
  • Label Placement — labels above the inputs, or to their left.
  • Instruction Placement — instructions under the label, or under the input.
  • Order — groups with a lower order come first on the form.
  • Description — shown in the list of field groups only.

The record form puts each group that matches in the Custom Fields block. Style and the two placements are followed by the Filament admin; Nova shows every group under a heading with its title, labels and instructions in Nova's own places. The plugin's Position (after the title, after the content, in the side column) has no counterpart on these forms: it is not offered, and a group brought from WordPress keeps the value it had.

Field types ​

All 37 types of the plugin, PRO ones included:

CategoryTypes
Basictext, textarea, number, range, email, url, password
Contentimage, file, wysiwyg, oembed, gallery
Choiceselect, checkbox, radio, button_group, true_false, nav_menu
Relationallink, post_object, page_link, relationship, taxonomy, user
Advancedgoogle_map, date_picker, date_time_picker, time_picker, color_picker, icon_picker
Layoutmessage, accordion, tab, group, repeater, flexible_content, clone

Types\Catalog holds each type's defaults and the settings the editors show. The defaults are those Secure Custom Fields 6.9.5 writes for a new field (tests/Fixtures/scf-6.9.5-types.json, taken on a live install).

Three types of the earlier admin are kept for fields that already exist and are not offered for new ones: media_hub (an image as a file URL), media_hub_collection (a JSON list of URLs) and boolean (1/0).

A module adds its own type by implementing Types\FieldType and tagging it with Types\FieldTypes::TAG.

Your own input for a type ​

How a type looks on the record form is up to each admin. A module or the application gives a type its own input — for a type it added, or instead of a built-in one (a map for google_map, a slider for number) — with a class per admin, tagged in that admin's provider:

AdminContractTag
FilamentKitloom\CustomFields\Filament\Contracts\FieldInput — types(), component(Field $field)Kitloom\CustomFields\Filament\Forms\FieldInputs::TAG
NovaKitloom\CustomFields\Nova\Contracts\FieldInput — types(), field(Field $field, string $attribute)Kitloom\CustomFields\Nova\Forms\FieldInputs::TAG
php
$this->app->tag([MapInput::class], FieldInputs::TAG);

The input gets the value as stored and draws only itself: the label, instructions, required and conditional logic are added by the form, and what it sends is made storable by the type, as any other writer's value. For each type the last input tagged wins, so the application overrides a module. Types that hold or arrange other fields (group, clone, repeater, flexible content, tab, accordion, message) are drawn by the form; an input for one of them is refused. A type with no input in an admin is a text box there.

The html editor and the media picker of wysiwyg, image, file and gallery fields are the site-wide HtmlEditor and AttachmentPicker of kitloom/wp-schema-filament and kitloom/wp-schema-nova: replace those to change every editor, a FieldInput to change only the custom field.

How values are stored ​

Laid out as the plugin lays them out (tests/Fixtures/scf-6.9.5-storage.json):

  • every value has its reference: _{name} holds the field key;
  • ids are strings; lists (checkbox, gallery, relationship, multiple selects) are JSON lists of strings;
  • a link, a map location and an icon are JSON objects;
  • a date is Ymd, a date with time Y-m-d H:i:s, a time H:i:s, a yes/no 1/0;
  • a group's sub fields are {group}_{sub}, the group itself an empty string;
  • a repeater keeps its row count, rows are {repeater}_{row}_{sub}; rows that go are deleted;
  • flexible content keeps a JSON list of row layouts and _{name}_layout_meta;
  • a clone keeps the copied fields under their own names, or {clone}_{sub} with prefixed names;
  • the options screen keeps values in wp_options as options_{name};
  • a taxonomy field may save the chosen terms as the post's terms and load them back.

Emptied fields are empty strings, as the plugin saves them. Group and field settings in post_content are JSON too.

What the plugin serialized — values and settings — is read as is (objects are never created) and becomes JSON the next time it is saved.

In revisions ​

A record's field values go into its revisions, as ACF copies them: each value with its _name reference, and the rows of repeaters and flexible content. A change of the fields alone keeps a revision, and restoring one brings back the values and the rows it had (Values\RevisionedFields, a RevisionedMeta of kitloom/wp-schema).

Reading fields in a theme ​

php
$post->field('subtitle');               // 'Under the title'
$post->field('slides');                 // [['caption' => 'One'], ['caption' => 'Two']]

use Kitloom\CustomFields\Facades\Fields;

Fields::get($model, 'subtitle');        // the same, for any model
Fields::option('footer_text');          // the options screen

Values come back the way get_field($name, $post, false) returns them: ids stay ids, a repeater is a list of rows, a flexible row carries acf_fc_layout. A name no group declares reads as the raw meta.

field() works on every Kitloom\WpSchema\Contracts\HasCustomFields model — posts, terms and users of kitloom/wp-schema and their subclasses. A model of your own implements that contract and HasMetaTable (where the values live) and uses the HasFields and Extensible traits of Kitloom\WpSchema\Models\Concerns.

In other languages ​

field(), Fields::get() and Fields::option() give what the site shows, in the request's language: with kitloom/languages a page in French reads the French texts of its fields. Text, textarea, wysiwyg and url fields and a link's text and address are translated; numbers, choices, yes/no, dates, colors, emails, pictures, files and relations are the same in every language. A group, a repeater or flexible content has its texts in its sub fields, found by path: cta.title, benefits.0.title (Values\TranslatableFields). Fields::all(), the edit forms, validation and every write keep the saved values. The point to plug a translation into is Kitloom\CustomFields\Contracts\FieldTranslator — translate(Model|string $owner, Field $field, mixed $value), the owner being the record or options; without a languages module it is Values\NoFieldTranslator, which changes nothing.

Writing fields ​

php
$post->setField('subtitle', 'Under the title');
$post->setFields(['rating' => 4.5, 'featured' => true], $user);   // as that user

Fields::set($model, 'subtitle', 'Under the title');               // the same, for any model
Fields::setMany($model, ['rating' => 4.5], $user);

The field is the one the model's own groups show; the value is checked as the edit form checks it (required, choices, range, dates, ids it may pick, rows and layouts, conditional logic) and refused as a ValidationException keyed by field path. With a user, the write is refused unless they may edit the model (edit_post, edit_term, edit_user); without one the system writes. Once written, a post's fields changed is a PostSaved event and a term's a TermSaved, one per call — as the plugin saves fields inside wp_update_post — so whatever indexes content sees the change.

Without kitloom/custom-fields installed, field() reads nothing and setField() throws: a value that silently went nowhere would be found missing much later.

Groups ​

Groups\FieldGroups reads all groups once per request and assembles each into a tree: sub fields, layouts, and the fields a clone copies (a field by key or a whole group). forSubject() gives the groups whose location rules match what is being edited (Location\Subjects builds it from a model).

Groups\GroupWriter writes a group from the array the plugin exports to JSON:

php
app(GroupWriter::class)->save([
    'key' => 'group_book',
    'title' => 'Book',
    'location' => [[['param' => 'post_type', 'operator' => '==', 'value' => 'book']]],
    'fields' => [
        ['key' => 'field_isbn', 'label' => 'ISBN', 'name' => 'isbn', 'type' => 'text'],
        ['key' => 'field_quotes', 'label' => 'Quotes', 'name' => 'quotes', 'type' => 'repeater', 'sub_fields' => [
            ['key' => 'field_quote', 'label' => 'Quote', 'name' => 'quote', 'type' => 'textarea'],
        ]],
    ],
]);

New groups, fields and layouts get keys like the plugin's (field_ and thirteen hex digits). Settings the caller does not send stay as they were; a new field gets its type's defaults. Fields missing from the array are deleted, their values stay in meta — as with the plugin. Admins, migrations and imports all go through the writer.

Location rules and conditional logic ​

Location\LocationRules reads the rule sets the way the plugin does: post type, post, status, format, category, taxonomy terms, template, page, page parent, page type, taxonomy, user form, user role, current user. A rule about something else (an options page, a block) does not match.

Conditions\Conditions evaluates a field's conditional logic against the current values of other fields: ==, !=, has value, has no value, pattern, contains, greater and less than.

AI agents ​

With kitloom/ai installed, an agent reads the fields of a record or a term — the groups its edit form shows, each field with its type, choices, sub fields and value (fields_get) — and sets them (fields_set), checked by the field writer as the form checks them and written only when all pass, as its user, who must be allowed to edit the record — to read its fields too, and only of a type the site declares. describe_site lists the active groups, where each shows (post_type == book) and their fields. With manage_field_groups it also reads the groups as ACF exports them (field_groups_get) and makes a group or adds and changes fields (field_groups_save), merged by field name: a field it does not send stays, and it removes none — a field whose values would lose their form is removed by a person. kitloom-custom-fields.ai.register false — no field tools.

Not here yet ​

The options pages screen (values are stored and read already), ACF blocks, bidirectional relationships, REST output.

kitloom — modules and themes for Laravel.
Pricing · Services · License · Refunds · Privacy · Community · [email protected]