Skip to main content

PHP Hook Reference

Form Plant provides WordPress action / filter hooks at almost every stage of form behavior — submission processing, email sending, redirects, file uploads, fields, form settings, and more. Just add a hook in functions.php ( or similar ) to extend and customize behavior without modifying the plugin itself.

Validation hooks have their own page

Hooks for input validation ( fplant_validate_* / fplant_validation_message_*, etc. ) are collected on Validation Hooks (PHP). This page covers everything else.

Pro hooks

The hooks and JavaScript events added by Form Plant Pro are documented in the Pro Developer Reference.

Migrating from MW WP Form

A rewrite mapping table from mwform_* hooks, with before/after examples, is available in Migrating from MW WP Form. Please refer to it as well.

Hook basics​

Targeting a specific form​

Many hooks fire for all forms on the site. If you want to target only a specific form, check the form ID ( $form_id ) inside your callback. The form ID is the number in the placement shortcode [fplant id="12"] or in the editor URL ( ...&id=12 ) .

add_filter( 'fplant_redirect_url', function ( $url, $form_id, $data ) {
if ( 12 !== $form_id ) {
return $url; // Return as-is for forms other than the target
}
return home_url( '/thanks/' );
}, 10, 3 );

Hooks targeted by field name (dynamic hooks)​

Some hooks let you append a field name to target only a specific field ( e.g. fplant_field_choices_{name} ). Internally these are called in two stages, "common across all fields → per-field version," and the per-field version receives the return value of the common version.

  • Common version: fplant_field_choices … includes $field_name in its arguments
  • Per-field version: embed the name, as in fplant_field_choices_inquiry_type
Field names are alphanumeric keys

The field name used in hook names and callbacks is the name ( alphanumeric key ) you can check on the form editor screen. If you migrated from MW WP Form, Japanese names have been converted to alphanumeric ( e.g. お名前 → your_name ) .


Form display / submission control​

Since v1.2.1, forms respect the post status. Published forms can be viewed and submitted by anyone, while private, draft, and pending-review forms are not shown to or submittable by general visitors — only users who can edit the form can preview and test-submit them. You can override this decision with the following filters.

HookTypeArgumentsPurpose
fplant_form_is_viewablefilter( $viewable, $form )Whether the form may be displayed on the front end 🆕
fplant_form_is_submittablefilter( $submittable, $form, $is_preview )Whether the form accepts submissions 🆕 ( third argument added in 1.5.2 )
fplant_preview_noticefilter( $notice, $form )The HTML of the preview notice shown above an unpublished form 🆕
fplant_form_outputfilter( $html, $form, $context )Replace the rendered form HTML, or add markup around it 1.5.2
fplant_form_unavailable_messagefilter( $message, $form )The text shown when the form does not accept submissions 1.5.2

$form is a form array that includes id and status. Returning true allows display/submission regardless of status.

When a user with edit permission views an unpublished form, a preview notice reading "This form is not published" appears at the top. You can replace the text with fplant_preview_notice or return an empty string to hide it. This is useful for cases like members-only forms ( where fplant_form_is_viewable allows display to members ) , where the form is shown to non-editors too.

Example: revert to allowing all statuses to be viewed/submitted as before​

add_filter( 'fplant_form_is_viewable', '__return_true' );
add_filter( 'fplant_form_is_submittable', '__return_true' );

Example: display only a specific form even when unpublished​

add_filter( 'fplant_form_is_viewable', function ( $viewable, $form ) {
if ( 12 === (int) $form['id'] ) {
return true;
}
return $viewable;
}, 10, 2 );

Example: members-only form ( show an unpublished form only to logged-in members )​

This example lets logged-in members view and submit an unpublished form, and hides the editor-facing preview notice.

// Allow form ID 12 to be viewed/submitted by anyone who is logged in
add_filter( 'fplant_form_is_viewable', function ( $viewable, $form ) {
return ( 12 === (int) $form['id'] && is_user_logged_in() ) ? true : $viewable;
}, 10, 2 );

add_filter( 'fplant_form_is_submittable', function ( $submittable, $form ) {
return ( 12 === (int) $form['id'] && is_user_logged_in() ) ? true : $submittable;
}, 10, 2 );

// Do not show the "unpublished" preview notice to members
add_filter( 'fplant_preview_notice', function ( $notice, $form ) {
return ( 12 === (int) $form['id'] ) ? '' : $notice;
}, 10, 2 );

Close a form and show a message instead ( 1.5.2 )​

Returning false from fplant_form_is_viewable makes the form output nothing at all ( and 404 for iframe / JavaScript embeds ) . To show something like "Registration has closed" in its place, leave visibility alone and combine these three filters.

GoalHook to use
Show a message instead of the formfplant_form_output
Stop the submission itself ( tabs left open, direct POSTs )fplant_form_is_submittable
Change the text shown when submissions are closedfplant_form_unavailable_message
$deadline = strtotime( '2026-12-01 00:00:00' );

// After the deadline, show a message instead of the form
add_filter( 'fplant_form_output', function ( $html, $form, $context ) use ( $deadline ) {
if ( 12 !== (int) $form['id'] || time() < $deadline ) {
return $html;
}
return '<p class="entry-closed">Registration has closed.</p>';
}, 10, 3 );

// Changing the output does not stop submissions — always close this too
add_filter( 'fplant_form_is_submittable', function ( $submittable, $form ) use ( $deadline ) {
if ( 12 !== (int) $form['id'] ) {
return $submittable;
}
return time() < $deadline ? $submittable : false;
}, 10, 2 );

// The text shown on the form once submissions are closed
add_filter( 'fplant_form_unavailable_message', function ( $message, $form ) {
return ( 12 === (int) $form['id'] ) ? 'Registration has closed.' : $message;
}, 10, 2 );

$html in fplant_form_output is the rendered form including its <div class="fplant-form-wrapper"> container, so you can also add markup before or after it. $context tells you how the form was placed: 'shortcode' ( shortcode / block ) , 'iframe' ( iframe embed ) , or 'rest' ( JavaScript embed ) .

Not called for unpublished forms

fplant_form_output is not called for a form where fplant_form_is_viewable is false, so a filter cannot reveal the existence of an unpublished form to visitors.

fplant_form_unavailable_message must return plain text ( tags are stripped ) . The default is "This form is currently unavailable."

Tell preview submissions apart ( 1.5.2 )​

The third argument of fplant_form_is_submittable, $is_preview, is true for a validation-only submission made from the preview in the form editor ( such a submission is never saved and sends no email ) . Use it to let editors test a form that is otherwise closed.

add_filter( 'fplant_form_is_submittable', function ( $submittable, $form, $is_preview ) {
if ( $is_preview ) {
return $submittable; // do not restrict the preview
}
// …your own restriction…
return $submittable;
}, 10, 3 );

This is an added argument, so existing callbacks registered with two arguments keep working.


Submission lifecycle​

A form submission is processed in the following order. Hooks are provided at each stage. They are color-coded as ■ action / ■ filter.

HookTypeArgumentsTiming / purpose
fplant_before_submissionaction( $form_id, $data )Before validation. For logging or preprocessing raw data
fplant_sanitize_field_valuefilter( $sanitized_value, $raw_value, $field, $form_id )Replace the sanitization of field types without a dedicated sanitizer ( text-like and custom field types ) . Return value = the value to store 🆕 v1.4.0
fplant_submission_datafilter( $data, $form_id )Modify data right after sanitization. Return value = the modified data
fplant_submission_gatefilter( $gate, $form, $data )The final gate right before saving. Return a WP_Error to reject the submission 🆕 v1.5.2
fplant_before_save_submission_datafilter( $data, $form_id )Modify data right before saving to the DB ( before password masking ) . Also useful for unset-ing specific keys to exclude them from saving. Return value = the data to save
fplant_after_submissionaction( $submission_id, $form_id, $data )After saving to the DB, before sending emails
fplant_after_submission_completeaction( $data, $form_id, $form, $submission_id )After emails are sent. The go-to hook for integrating with external services

Example: integrate with an external service after submission completes​

add_action( 'fplant_after_submission_complete', function ( $data, $form_id, $form, $submission_id ) {
if ( 12 !== $form_id ) {
return;
}
// e.g. notify Slack or a CRM
wp_remote_post( 'https://example.com/webhook', array(
'body' => array(
'submission_id' => $submission_id,
'email' => $data['email'] ?? '',
),
'timeout' => 5,
) );
}, 10, 4 );

Example: do not save a specific field to the DB​

add_filter( 'fplant_before_save_submission_data', function ( $data, $form_id ) {
// Exclude fields you don't want to store, such as a credit card number
unset( $data['card_number'] );
return $data;
}, 10, 2 );

Example: reject a submission right before it is saved ( fplant_submission_gate , 1.5.2 )​

This final gate is called once after all validation has passed and right before the submission is written to the database. Returning a WP_Error cancels the submission: nothing is saved, no email is sent, no webhook is delivered, and fplant_after_submission does not run. The visitor sees the error message.

Use it when the input is valid but the submission cannot be accepted — capacity limits, stock, duplicate entries, and similar checks.

add_filter( 'fplant_submission_gate', function ( $gate, $form, $data ) {
if ( 12 !== (int) $form['id'] || is_wp_error( $gate ) ) {
return $gate; // not our form, or another hook already rejected it
}

// Reject a duplicate entry from the same email address
$email = $data['email'] ?? '';
if ( '' !== $email && my_already_applied( $email ) ) {
return new WP_Error( 'already_applied', 'You have already applied with this email address.' );
}

return $gate; // to accept, return the value you received ( null by default )
}, 10, 3 );
Which one to use
  • fplant_form_is_submittable — whether the form is closed when it is opened. The notice appears on the input screen
  • fplant_submission_gate — a decision made after seeing what was submitted. Submissions rejected by validation never reach it
The error message is plain text

Tags are stripped from the WP_Error message before it is displayed. If the message is empty, the fplant_form_unavailable_message text is used instead. The filter is not called for submissions made from the form editor's preview.


Post-submission actions (completion message / redirect)​

Depending on the "Post-submission action" in the form settings, exactly one of these fires. All were added in v1.2.0.

HookTypeArgumentsSetting that enables it
fplant_complete_messagefilter( $message, $form_id, $data, $form )"Message" ( after merge tags are expanded )
fplant_success_htmlfilter( $html, $form_id, $data, $form )"Custom page" ( the HTML after {{key}} placeholders and merge tags are expanded )
fplant_redirect_urlfilter( $url, $form_id, $data, $form )"Redirect" ( before esc_url_raw )
fplant_submission_result 1.5filter( $result, $form_id, $data, $form )Always ( after the three above ). Replaces the whole result array

The 4th argument $form ( the full form configuration array ) was added in v1.4.0. Use it for conditions based on form settings.

Since v1.5.0, the success message and the completion page HTML reach these filters with the same merge tags as emails ( {submission_id} / {field:name} / {all_fields} ... ) already expanded. The expansion is available as FPLANT_Email_Handler::replace_tags( $text, $data, $form, $submission_id, $args ) so you can apply it to your own strings ( $args['escape'] = 'html' escapes inserted values, $args['mask_password'] = true masks passwords ).

Example: change the completion message based on input values​

add_filter( 'fplant_complete_message', function ( $message, $form_id, $data ) {
if ( 12 === $form_id ) {
$name = $data['your_name'] ?? 'Customer';
$message = 'Dear ' . $name . ', thank you for your inquiry.';
}
return $message;
}, 10, 3 );

Example: branch the redirect target by plan​

add_filter( 'fplant_redirect_url', function ( $url, $form_id, $data ) {
if ( 12 === $form_id && 'premium' === ( $data['plan'] ?? '' ) ) {
$url = home_url( '/thanks-premium/' );
}
return $url;
}, 10, 3 );

Example: switch the completion action type based on input values ( 1.5.0 )​

fplant_redirect_url and friends only fire inside the branch of the action type chosen in the form settings. To switch the type itself — "show a message by default, but redirect for certain answers" — use fplant_submission_result. The keys of $result are success / message / submission_id / action_type ( message | custom_page | redirect ) / redirect_url / success_page_html.

add_filter( 'fplant_submission_result', function ( $result, $form_id, $data, $form ) {
if ( 12 === $form_id && 'premium' === ( $data['plan'] ?? '' ) ) {
$result['action_type'] = 'redirect';
$result['redirect_url'] = home_url( '/thanks-premium/' );
}
return $result;
}, 10, 4 );

For forms that use [fplant_complete] on the redirect page, the completion token is appended after this filter, so the completion screen also works on a redirect target chosen here.

Limiting the fields shown on the confirmation screen and in emails: fplant_display_fields ( 1.5.0 )​

HookTypeArguments
fplant_display_fieldsfilter( $fields, $data, $form, $context )

When the confirmation screen, the email {all_fields} / default body, or the completion screen {all_fields} is generated, this filter lets you replace the array of field definitions to output for that submission only ( the form definition itself is untouched ). $context is one of confirmation / email / completion.

// Example: hide "company" from the confirmation screen and emails when it is empty
add_filter( 'fplant_display_fields', function ( $fields, $data, $form, $context ) {
if ( empty( $data['company'] ) ) {
$fields = array_values( array_filter( $fields, function ( $field ) {
return 'company' !== $field['name'];
} ) );
}
return $fields;
}, 10, 4 );

Hooks for [fplant_complete] ( completion screen on the redirect page ) ( 1.5.0 )​

HookTypeArgumentsUse
fplant_complete_shortcode_htmlfilter( $html, $form_id, $data, $submission_id, $form )Replace the HTML output by the shortcode
fplant_complete_token_ttlfilter( $ttl, $form_id )Lifetime ( seconds, default 300 ) of a completion token whose page was never opened

See Form Settings › Showing the completion screen on the redirect page for usage.


Webhooks​

Deliveries of the generic webhook feature ( added in v1.4.0; see Webhooks for an overview ) can be customized with filters.

HookTypeArgumentsPurpose
fplant_webhook_should_sendfilter( $should_send, $webhook, $payload, $form_id, $data )Return false to skip the delivery. For conditional delivery
fplant_webhook_payloadfilter( $payload, $form_id, $data, $submission_id )Modify the JSON payload
fplant_webhook_request_argsfilter( $args, $url )Change the wp_safe_remote_post() arguments ( timeout etc. )
fplant_webhook_allow_httpfilter( $allow, $url )Return true to allow http:// URLs ( local development )

Example: skip delivery when a field is empty​

add_filter( 'fplant_webhook_should_send', function ( $should_send, $webhook, $payload, $form_id, $data ) {
if ( 12 === $form_id && empty( $data['company'] ) ) {
return false; // don't forward submissions without a company name
}
return $should_send;
}, 10, 5 );

Example: add a custom item to the payload​

add_filter( 'fplant_webhook_payload', function ( $payload, $form_id, $data, $submission_id ) {
$payload['environment'] = wp_get_environment_type();
return $payload;
}, 10, 4 );

Email sending​

Paired hooks are provided for the admin email and the auto-reply ( user ) email. Replacing admin with user gives you the auto-reply side.

HookTypeArgumentsPurpose
fplant_before_admin_email_sendaction( $email_settings, $form_id )At the start of admin email processing
fplant_skip_admin_emailfilter( $skip, $form, $data, $submission_id )Return true to skip sending 🆕
fplant_before_send_email_datafilter( $data, $form, $submission_id, $type )Modify data before generating the body. $type is 'admin' / 'user' ( shared hook )
fplant_admin_email_tofilter( $to, $form_id, $data )The admin recipient ( array ) . An empty array skips sending 🆕
fplant_admin_email_subjectfilter( $subject, $form_id, $data )Subject ( $data added in v1.4.0 )
fplant_admin_email_bodyfilter( $message, $form_id, $data )Body
fplant_admin_email_headersfilter( $headers, $form_id, $data )Headers ( From / Cc / Bcc / Reply-To ) 🆕
fplant_admin_email_attachmentsfilter( $attachments, $form_id, $data, $submission_id )Files attached to the admin email ( array of absolute paths ) 🆕 v1.5.1
fplant_after_admin_email_sendaction( $email_settings, $form_id, $result )After wp_mail() runs. $result is success/failure

Auto-reply side: fplant_before_user_email_send / fplant_skip_user_email 🆕 / fplant_user_email_to 🆕 / fplant_user_email_subject / fplant_user_email_body / fplant_user_email_headers 🆕 / fplant_after_user_email_send. Attachments are supported on the admin email only.

🆕 = added in v1.2.0 ( entries with a version noted were added in that version ) .

Attachments are limited to one directory

fplant_admin_email_attachments can only add files under wp-content/uploads/fplant_uploads/. Any other path is dropped by a containment check that runs after the filter, so an arbitrary server-side file can never be attached. Files that went through the form's own upload handling are always inside that directory.

Mind the return types of the recipient hooks

The admin recipient ( fplant_admin_email_to ) returns an array, assuming multiple recipients, while the auto-reply recipient ( fplant_user_email_to ) returns a single string. The 2nd argument $form of fplant_skip_admin_email / fplant_skip_user_email is the form array, not the form ID ( get the ID with $form['id'] ) .

Example: route the admin email recipient based on input​

add_filter( 'fplant_admin_email_to', function ( $to, $form_id, $data ) {
if ( 12 === $form_id && 'sales' === ( $data['category'] ?? '' ) ) {
$to = array( 'sales@example.com' );
}
return $to;
}, 10, 3 );

Example: add a BCC​

add_filter( 'fplant_admin_email_headers', function ( $headers, $form_id, $data ) {
if ( 12 === $form_id ) {
$headers[] = 'Bcc: archive@example.com';
}
return $headers;
}, 10, 3 );

Example: do not send the auto-reply under certain conditions​

add_filter( 'fplant_skip_user_email', function ( $skip, $form, $data, $submission_id ) {
if ( (int) $form['id'] === 12 && empty( $data['newsletter'] ) ) {
return true; // Cancel sending
}
return $skip;
}, 10, 4 );

File uploads​

HookTypeArgumentsPurpose
fplant_upload_dirfilter( $custom_dir, $form_id )The destination directory. $custom_dir is array( 'path' => ..., 'url' => ... ) 🆕
fplant_upload_filenamefilter( $filename, $field_config, $form_id )The saved file name 🆕
Re-validation for security

The return value of fplant_upload_dir is restricted to within the WordPress uploads folder ( specifying outside it causes an error ) . The return value of fplant_upload_filename is re-sanitized, and dangerous extensions ( .php, etc. ) are rejected. These hooks are for organization ( date-based folders, naming conventions ) and cannot disable the protection mechanisms.

Point the destination inside fplant_uploads

File downloads from the admin screen and attachments on the admin notification email cover only files under wp-content/uploads/fplant_uploads/. If fplant_upload_dir points outside it, the upload itself still succeeds, but the file cannot be downloaded and is not attached. Create any organizing subfolders inside fplant_uploads.

Example: save into year/month folders​

add_filter( 'fplant_upload_dir', function ( $dir, $form_id ) {
$u = wp_upload_dir();
// Downloads and attachments only cover fplant_uploads, so stay inside it
$sub = '/fplant_uploads/form-uploads/' . gmdate( 'Y-m' );
return array(
'path' => $u['basedir'] . $sub,
'url' => $u['baseurl'] . $sub,
);
}, 10, 2 );

Fields, choices, initial values​

HookTypeArgumentsPurpose
fplant_field_typesfilter( $field_types )Add or change the definitions of available field types
fplant_field_defaultsfilter( $defaults, $field_type )Default settings per field type
fplant_field_initial_valuefilter( null, $field_name, $field, $form_id )A common initial value for all fields. Returning null passes to the next
fplant_field_initial_value_{name}filter( $value, $field, $form_id )A per-field initial value
fplant_field_choicesfilter( $options, $field_name, $field, $form_id )Common choices for all fields ( select / radio / checkbox ) 🆕
fplant_field_choices_{name}filter( $options, $field, $form_id )Per-field choices 🆕
fplant_custom_mail_tag_valuefilter( '', $field_name, $field, $form_id )The value of a custom mail tag ( common version ) 🆕
fplant_custom_mail_tag_value_{name}filter( $value, $field, $form_id )The value of a custom mail tag ( per-field ) 🆕

Choices ( $options ) are an array of array( 'label' => display name, 'value' => value ).

Example: use the logged-in user's email as the initial value​

add_filter( 'fplant_field_initial_value_email', function ( $value, $field, $form_id ) {
if ( is_user_logged_in() ) {
$value = wp_get_current_user()->user_email;
}
return $value;
}, 10, 3 );

Example: dynamically generate select choices from a post list​

add_filter( 'fplant_field_choices_product', function ( $options, $field, $form_id ) {
foreach ( get_posts( array( 'post_type' => 'product' ) ) as $p ) {
$options[] = array( 'label' => $p->post_title, 'value' => (string) $p->ID );
}
return $options;
}, 10, 3 );

Example: inject the member rank into a custom mail tag​

This supplies the value of a custom_mail_tag field ( name = member_rank ) via a hook.

add_filter( 'fplant_custom_mail_tag_value_member_rank', function ( $value, $field, $form_id ) {
return get_user_meta( get_current_user_id(), 'rank', true );
}, 10, 3 );

Extending form settings​

HookTypeArgumentsPurpose
fplant_custom_settings_fieldsfilter( $fields, $form_id )Add your own input fields to the form settings screen 🆕
fplant_form_settings_savedaction( $form_id, $settings )Right after the form settings are saved 🆕
fplant_html_setting_keysfilter( $keys, $form_id )The form setting keys whose value may contain HTML 1.5.2

Fields defined with fplant_custom_settings_fields are automatically rendered and saved on the settings screen, and can be referenced at runtime via $form['settings'][key].

Example: add a CRM endpoint URL input to the settings screen​

add_filter( 'fplant_custom_settings_fields', function ( $fields, $form_id ) {
$fields[] = array(
'key' => 'x_crm_endpoint', // alphanumeric / underscore
'type' => 'text', // text / textarea / checkbox / select / number
'label' => 'CRM endpoint URL',
'description' => 'The CRM API endpoint',
);
return $fields;
}, 10, 2 );

// Use the saved value when submission completes
add_action( 'fplant_after_submission_complete', function ( $data, $form_id, $form, $submission_id ) {
if ( ! empty( $form['settings']['x_crm_endpoint'] ) ) {
// Processing that uses $form['settings']['x_crm_endpoint']
}
}, 10, 4 );

Allowing HTML in a setting ( 1.5.2 )​

Form setting values are stored with their tags stripped by default. For a setting that should hold markup, such as a notice, register its key as one that allows HTML. Those values are sanitized with wp_kses_post() ( the same range as post content ) .

There are two ways to do it.

// (a) When you add your own setting: mark it with allow_html
add_filter( 'fplant_custom_settings_fields', function ( $fields, $form_id ) {
$fields[] = array(
'key' => 'x_closed_notice',
'type' => 'textarea',
'label' => 'Notice shown when closed',
'allow_html' => true, // ← may contain HTML
);
return $fields;
}, 10, 2 );

// (b) When your extension keeps its own block inside the settings array
add_filter( 'fplant_html_setting_keys', function ( $keys, $form_id ) {
$keys[] = 'x_addon_closed_message';
return $keys;
}, 10, 2 );

FPLANT_Form_Manager::get_html_setting_keys( $form_id ) returns the effective list ( by default confirmation_message, after_submit_html, success_page_html and confirmation_template ) .

Keys are matched by name

A registered key matches at any depth of the form settings array. Registering a common name such as message would allow HTML in unrelated settings, so use a distinctive, prefixed name like x_. Note also that the value is only sanitized to the wp_kses_post() range on save — escape it again on output.

Fixed in 1.5.2

In 1.5.1 and earlier, allow_html had no effect when the form was saved from the form editor ( the tags were stripped earlier in the save ) . This is fixed in 1.5.2. The four built-in keys are unchanged.


Templates / rendering​

HookTypeArgumentsPurpose
fplant_locate_templatefilter( $template, $template_name, $template_path )Change the resolved path of a template file ( e.g. override on the theme side )
fplant_allowed_field_typesfilter( $allowed_field_types )The whitelist of field types allowed in HTML templates
fplant_template_valuesfilter( $values, $form_id )Inject values into {{key}} placeholders in the input screen HTML template, the confirmation screen template, and the completion page HTML ( values are esc_html-escaped )
fplant_allowed_form_htmlfilter( $allowed )HTML tags and attributes allowed in form output ( wp_kses() format ) 🆕 v1.5.1

For how to use the HTML template feature itself, see HTML Template.

Example: inject a dynamic value into a template​

Write {{key}} in a template and it is replaced at render time with the value of that key in the array returned by the fplant_template_values filter. The replacement works in all three of the following places.

ScreenWhere to set it
Input screenThe input screen HTML template ( when "Use HTML template" is ON )
Confirmation screenThe confirmation screen template ( when "Use confirmation screen HTML template" is ON )
Completion screen"Completion Page HTML" of the post-submission action
add_filter( 'fplant_template_values', function ( $values, $form_id ) {
$values['today'] = wp_date( 'F j, Y' );
$values['shop_name'] = get_bloginfo( 'name' );
return $values;
}, 10, 2 );

Example confirmation screen template ( {{shop_name}} and {{today}} are replaced ) :

<h2>[fplant_confirmation_title]</h2>
<p>The following will be sent to {{shop_name}} ( received {{today}} ).</p>
[fplant_all_fields]
[fplant_back] [fplant_confirm_submit]

[fplant_all_fields] and the other tags above are confirmation-screen template tags ( they can be inserted from "Available Shortcodes" on the form edit screen ).

Since version 1.4.1, WordPress shortcodes are also expanded inside the confirmation screen template ( they are not expanded in 1.4.0 and earlier ). The value of this filter can also be the ( text ) output of a shortcode:

add_filter( 'fplant_template_values', function ( $values, $form_id ) {
// Example: show the output of a custom business-hours shortcode on the confirmation screen
$values['business_hours'] = do_shortcode( '[business_hours]' );
return $values;
}, 10, 2 );
Limitations
  • Values are escaped with esc_html(), so only plain text can be injected ( HTML tags in a value are displayed as literal text ).
  • Replacement in iframe / JavaScript embeds works since version 1.4.1 ( 1.4.0 and earlier replace only for forms placed with the shortcode / block ).
  • A {{key}} whose key is not defined by the filter is left as is.
  • To inject values only into a specific form, branch on the second argument $form_id.

Registering custom field types (for extension developers)​

Plugins and themes can add their own field types ( the extension API was consolidated in v1.4.0 ). Registration takes just three filters:

StepHookRole
1. Register the typefplant_field_typesShow the type in the add-field dialog ( label, icon, description )
2. Allow renderingfplant_allowed_field_typesFront-end rendering whitelist ( the field will not render at all if you forget this )
3. Resolve the templatefplant_locate_templateServe form-fields/{type}.php from your own directory

Example: adding a custom "rating" field type​

// 1. Register the type
add_filter( 'fplant_field_types', function ( $types ) {
$types['rating'] = array(
'label' => 'Rating',
'icon' => 'dashicons-star-filled', // dashicons or your own CSS class
'description' => '5-star rating',
);
return $types;
} );

// 2. Add it to the rendering whitelist
add_filter( 'fplant_allowed_field_types', function ( $allowed ) {
$allowed[] = 'rating';
return $allowed;
} );

// 3. Resolve the template from your own directory
// ( $template arrives as an empty string when neither the theme nor
// the plugin itself provides the template )
add_filter( 'fplant_locate_template', function ( $template, $template_name, $template_path ) {
if ( '' !== (string) $template ) {
return $template; // respect theme overrides etc.
}
$candidate = plugin_dir_path( __FILE__ ) . 'templates/' . $template_name;
return file_exists( $candidate ) ? $candidate : $template;
}, 10, 3 );

For how to write the template file ( e.g. templates/form-fields/rating.php ), the built-in field templates are a good reference ( see Overriding templates from a theme ).

Also implement the following:

  • Validation: custom types pass through server-side validation except the required check. Add server-side rules with the validation hooks ( fplant_validation_errors etc. ), and instant client-side feedback with fplant.addValidator() if needed.
  • Sanitization: for types without a dedicated sanitizer, the stored value can be replaced per type with fplant_sanitize_field_value ( see Submission lifecycle ).
  • Confirmation screen: the standard confirmation screen works as is. Only when a custom confirmation template uses [fplant_value name="..."] do you also need a confirm-fields/{type}.php template ( resolvable via fplant_locate_template as well; [fplant_all_fields] does not need it ).

Registering a type with no value ( layout type ) ( 1.5.2 )​

Field types that hold no input value — headings, separators and the like — are registered as layout types. The built-in html type is one.

add_filter( 'fplant_layout_field_types', function ( $types ) {
$types[] = 'section_heading';
return $types;
} );

Once registered, fields of that type are treated as follows.

WhereTreatment
ValidationSkipped ( including the required check )
Confirmation screen, {all_fields}, default email bodyNot included
CSV export ( single form )No column is created
Webhook payloadNot included
Email tag list in the form editorNot listed
Rendering with the default layoutOutput directly, with no label and no error area

FPLANT_Field_Manager::get_layout_field_types() returns the current list, and FPLANT_Field_Manager::is_layout_type( $type ) tests a single type.

This is separate from registering the type

You still need fplant_field_types ( registering the type ) and fplant_allowed_field_types ( allowing it to render ) . fplant_layout_field_types only declares that the type holds no value.

Storing structured (array) values​

Since v1.4.0, custom field types can store array ( nested ) values as is.

  • Submission data is stored as JSON, so array structures survive saving and re-reading
  • Inputs with bracketed names such as name="items[0][price]" are automatically collected as nested values ( row arrays / objects ) on submit ( see JavaScript Customization )
  • The default sanitization applies text sanitization while preserving the structure ( replaceable via fplant_sanitize_field_value )

Formatting submission values for display: fplant_format_submission_value​

A shared filter ( v1.4.0 ) that formats a stored submission value as plain text. Every output path — the confirmation screen, emails, CSV export and the admin submission detail — goes through this formatting, so one hook unifies how your custom type is displayed.

ArgumentDescription
$formattedThe default formatting ( flat arrays are comma-joined; row arrays become numbered "label: value / …" lines )
$valueThe raw stored value ( may be an array )
$fieldThe field configuration array
$contextOutput context: confirmation / email_all_fields / email_tag / admin_detail / csv_single / csv_all
$form_idForm ID ( 0 when unknown )
$submission_idSubmission ID ( added in v1.5.1 ) . 0 when the value is not stored yet, as on the confirmation screen. An extension that wants to show an old entry with the field definitions it was submitted with can use this to look up its own snapshot
add_filter( 'fplant_format_submission_value', function ( $formatted, $value, $field, $context, $form_id, $submission_id ) {
if ( 'rating' !== ( $field['type'] ?? '' ) ) {
return $formatted;
}
return str_repeat( '★', (int) $value ); // e.g. 3 → ★★★
}, 10, 6 );

Existing 5-argument callbacks keep working — adding an argument does not break them.

The return value is plain text

HTML is not supported here. Escaping and newline-to-<br> conversion are handled by each output site, so returned HTML tags would be displayed as literal characters.

Replacing a submission-detail cell with HTML: fplant_submission_detail_value_html ( 1.5.1 )​

fplant_format_submission_value is plain text only, but the admin submission detail can be replaced with HTML. Use it to show rows as a table, add a per-row download link, or otherwise render your own type richly.

ArgumentDescription
$cell_htmlThe default cell HTML ( nl2br( esc_html( formatted ) ) )
$raw_valueThe value before formatting. Passwords arrive already masked and acceptance is already replaced by its display wording
$field_defField definition ( empty array when unknown )
$submissionThe whole submission record
$formThe form configuration
add_filter( 'fplant_submission_detail_value_html', function ( $cell_html, $raw_value, $field_def ) {
if ( 'rating' !== ( $field_def['type'] ?? '' ) ) {
return $cell_html;
}
return '<strong>' . esc_html( str_repeat( '★', (int) $raw_value ) ) . '</strong>';
}, 10, 3 );
The output goes through wp_kses_post()

The return value is narrowed to what post content allows. <script> and on* attributes are removed, so JavaScript is not possible. Top-level file fields ( the cell with the download link ) do not go through this filter.

Downloading a nested file from the submission detail ( 1.5.1 )​

The file download endpoint accepts the optional parameters row ( row index ) and sub ( sub-field key ), so you can build links to files inside a group or a repeater without your own download route.

$url = add_query_arg(
array(
'action' => 'fplant_download_file',
'submission_id' => $submission['id'],
'field' => 'members',
'row' => 0, // index in a row array ( optional )
'sub' => 'photo', // sub-field key ( optional )
'nonce' => wp_create_nonce( 'fplant_download_file' ),
),
admin_url( 'admin-ajax.php' )
);

The nonce, the manage_options capability check and the fplant_uploads containment check all still apply, unchanged.

Reading the allowed field types ( 1.5.1 )​

FPLANT_Template_Loader::get_allowed_field_types() returns the effective list, after fplant_allowed_field_types has been applied.

if ( in_array( 'rating', FPLANT_Template_Loader::get_allowed_field_types(), true ) ) {
// the rating type will be rendered
}
Fields of an unregistered type are passed over

Since v1.5.1, a field whose type is not in the allowed list — i.e. the extension that provided it is deactivated — has its validation skipped, including the required check, and its value is not saved. This keeps a required field from a deactivated extension from blocking the whole form. In the field editor the type shows as type_name (unavailable) and saving keeps it.

Attaching extra data to a submission (extras)​

To attach your own data to a submission record ( e.g. a snapshot of settings at submit time ), use these static methods ( v1.4.0 ):

FPLANT_Database::set_submission_extra( $submission_id, $key, $value );
FPLANT_Database::get_submission_extra( $submission_id, $key, $default_value );

The submission ID is available from the fplant_after_submission / fplant_after_submission_complete hooks.


Data export​

HookTypeArgumentsPurpose
fplant_export_encodingfilter( $encoding, $form_id )The character encoding of the submission data CSV ( default UTF-8 ) 🆕

Example: output as Shift-JIS for Excel compatibility​

add_filter( 'fplant_export_encoding', function ( $encoding, $form_id ) {
return 'sjis-win'; // Shift-JIS ( CP932 )
}, 10, 2 );

Hook list (quick reference)​

🆕 = added in v1.2.0 / v1.2.1. 1.4 = added or arguments extended in v1.4.0. 1.5 = added in v1.5.0. 1.5.1 = added or arguments extended in v1.5.1. 1.5.2 = added or arguments extended in v1.5.2.

HookTypeArguments
fplant_form_is_viewable 🆕filter( $viewable, $form )
fplant_form_is_submittable 🆕 1.5.2filter( $submittable, $form, $is_preview )
fplant_preview_notice 🆕filter( $notice, $form )
fplant_form_output 1.5.2filter( $html, $form, $context )
fplant_form_unavailable_message 1.5.2filter( $message, $form )
fplant_before_submissionaction( $form_id, $data )
fplant_sanitize_field_value 1.4filter( $sanitized_value, $raw_value, $field, $form_id )
fplant_submission_datafilter( $data, $form_id )
fplant_submission_gate 1.5.2filter( $gate, $form, $data )
fplant_before_save_submission_datafilter( $data, $form_id )
fplant_after_submissionaction( $submission_id, $form_id, $data )
fplant_after_submission_completeaction( $data, $form_id, $form, $submission_id )
fplant_complete_message 🆕 1.4filter( $message, $form_id, $data, $form )
fplant_success_html 🆕 1.4filter( $html, $form_id, $data, $form )
fplant_redirect_url 🆕 1.4filter( $url, $form_id, $data, $form )
fplant_submission_result 1.5filter( $result, $form_id, $data, $form )
fplant_display_fields 1.5filter( $fields, $data, $form, $context )
fplant_complete_shortcode_html 1.5filter( $html, $form_id, $data, $submission_id, $form )
fplant_complete_token_ttl 1.5filter( $ttl, $form_id )
fplant_webhook_should_send 1.4filter( $should_send, $webhook, $payload, $form_id, $data )
fplant_webhook_payload 1.4filter( $payload, $form_id, $data, $submission_id )
fplant_webhook_request_args 1.4filter( $args, $url )
fplant_webhook_allow_http 1.4filter( $allow, $url )
fplant_before_admin_email_sendaction( $email_settings, $form_id )
fplant_skip_admin_email 🆕filter( $skip, $form, $data, $submission_id )
fplant_before_send_email_datafilter( $data, $form, $submission_id, $type )
fplant_admin_email_to 🆕filter( $to, $form_id, $data )
fplant_admin_email_subject 1.4filter( $subject, $form_id, $data )
fplant_admin_email_bodyfilter( $message, $form_id, $data )
fplant_admin_email_headers 🆕filter( $headers, $form_id, $data )
fplant_admin_email_attachments 1.5.1filter( $attachments, $form_id, $data, $submission_id )
fplant_after_admin_email_sendaction( $email_settings, $form_id, $result )
fplant_before_user_email_sendaction( $email_settings, $form_id )
fplant_skip_user_email 🆕filter( $skip, $form, $data, $submission_id )
fplant_user_email_to 🆕filter( $to, $form_id, $data )
fplant_user_email_subject 1.4filter( $subject, $form_id, $data )
fplant_user_email_bodyfilter( $message, $form_id, $data )
fplant_user_email_headers 🆕filter( $headers, $form_id, $data )
fplant_after_user_email_sendaction( $email_settings, $form_id, $result )
fplant_upload_dir 🆕filter( $custom_dir, $form_id )
fplant_upload_filename 🆕filter( $filename, $field_config, $form_id )
fplant_field_typesfilter( $field_types )
fplant_layout_field_types 1.5.2filter( $types )
fplant_field_defaultsfilter( $defaults, $field_type )
fplant_field_initial_value ( _{name} )filter( $value, [$field_name,] $field, $form_id )
fplant_field_choices 🆕 ( _{name} )filter( $options, [$field_name,] $field, $form_id )
fplant_custom_mail_tag_value 🆕 ( _{name} )filter( $value, [$field_name,] $field, $form_id )
fplant_custom_settings_fields 🆕filter( $fields, $form_id )
fplant_form_settings_saved 🆕action( $form_id, $settings )
fplant_html_setting_keys 1.5.2filter( $keys, $form_id )
fplant_locate_templatefilter( $template, $template_name, $template_path )
fplant_allowed_field_typesfilter( $allowed_field_types )
fplant_template_valuesfilter( $values, $form_id )
fplant_allowed_form_html 1.5.1filter( $allowed )
fplant_submission_detail_value_html 1.5.1filter( $cell_html, $raw_value, $field_def, $submission, $form )
fplant_format_submission_value 1.4 1.5.1filter( $formatted, $value, $field, $context, $form_id, $submission_id )
fplant_export_encoding 🆕filter( $encoding, $form_id )

For validation hooks ( fplant_validate_* / fplant_validation_message_* / fplant_before_validation / fplant_validation_errors ), see Validation Hooks (PHP).


Security notes​

  • Always escape input received through hooks with esc_html() / esc_attr() / esc_url(), etc. before output.
  • For processes that call external APIs, set a timeout and cache with the Transient API as needed.
  • The protection mechanisms of fplant_upload_dir / fplant_upload_filename ( restricted to under uploads, extension blacklist ) cannot be disabled.