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.
Hooks for input validation ( fplant_validate_* / fplant_validation_message_*, etc. ) are collected on Validation Hooks (PHP). This page covers everything else.
The hooks and JavaScript events added by Form Plant Pro are documented in the Pro Developer Reference.
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_namein its arguments - Per-field version: embed the name, as in
fplant_field_choices_inquiry_type
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.
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_form_is_viewable | filter | ( $viewable, $form ) | Whether the form may be displayed on the front end 🆕 |
fplant_form_is_submittable | filter | ( $submittable, $form, $is_preview ) | Whether the form accepts submissions 🆕 ( third argument added in 1.5.2 ) |
fplant_preview_notice | filter | ( $notice, $form ) | The HTML of the preview notice shown above an unpublished form 🆕 |
fplant_form_output | filter | ( $html, $form, $context ) | Replace the rendered form HTML, or add markup around it 1.5.2 |
fplant_form_unavailable_message | filter | ( $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.
| Goal | Hook to use |
|---|---|
| Show a message instead of the form | fplant_form_output |
| Stop the submission itself ( tabs left open, direct POSTs ) | fplant_form_is_submittable |
| Change the text shown when submissions are closed | fplant_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 ) .
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.
| Hook | Type | Arguments | Timing / purpose |
|---|---|---|---|
fplant_before_submission | action | ( $form_id, $data ) | Before validation. For logging or preprocessing raw data |
fplant_sanitize_field_value | filter | ( $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_data | filter | ( $data, $form_id ) | Modify data right after sanitization. Return value = the modified data |
fplant_submission_gate | filter | ( $gate, $form, $data ) | The final gate right before saving. Return a WP_Error to reject the submission 🆕 v1.5.2 |
fplant_before_save_submission_data | filter | ( $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_submission | action | ( $submission_id, $form_id, $data ) | After saving to the DB, before sending emails |
fplant_after_submission_complete | action | ( $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 );
fplant_form_is_submittable— whether the form is closed when it is opened. The notice appears on the input screenfplant_submission_gate— a decision made after seeing what was submitted. Submissions rejected by validation never reach it
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.
| Hook | Type | Arguments | Setting that enables it |
|---|---|---|---|
fplant_complete_message | filter | ( $message, $form_id, $data, $form ) | "Message" ( after merge tags are expanded ) |
fplant_success_html | filter | ( $html, $form_id, $data, $form ) | "Custom page" ( the HTML after {{key}} placeholders and merge tags are expanded ) |
fplant_redirect_url | filter | ( $url, $form_id, $data, $form ) | "Redirect" ( before esc_url_raw ) |
fplant_submission_result 1.5 | filter | ( $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 asFPLANT_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'] = truemasks 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 )
| Hook | Type | Arguments |
|---|---|---|
fplant_display_fields | filter | ( $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 )
| Hook | Type | Arguments | Use |
|---|---|---|---|
fplant_complete_shortcode_html | filter | ( $html, $form_id, $data, $submission_id, $form ) | Replace the HTML output by the shortcode |
fplant_complete_token_ttl | filter | ( $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.
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_webhook_should_send | filter | ( $should_send, $webhook, $payload, $form_id, $data ) | Return false to skip the delivery. For conditional delivery |
fplant_webhook_payload | filter | ( $payload, $form_id, $data, $submission_id ) | Modify the JSON payload |
fplant_webhook_request_args | filter | ( $args, $url ) | Change the wp_safe_remote_post() arguments ( timeout etc. ) |
fplant_webhook_allow_http | filter | ( $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.
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_before_admin_email_send | action | ( $email_settings, $form_id ) | At the start of admin email processing |
fplant_skip_admin_email | filter | ( $skip, $form, $data, $submission_id ) | Return true to skip sending 🆕 |
fplant_before_send_email_data | filter | ( $data, $form, $submission_id, $type ) | Modify data before generating the body. $type is 'admin' / 'user' ( shared hook ) |
fplant_admin_email_to | filter | ( $to, $form_id, $data ) | The admin recipient ( array ) . An empty array skips sending 🆕 |
fplant_admin_email_subject | filter | ( $subject, $form_id, $data ) | Subject ( $data added in v1.4.0 ) |
fplant_admin_email_body | filter | ( $message, $form_id, $data ) | Body |
fplant_admin_email_headers | filter | ( $headers, $form_id, $data ) | Headers ( From / Cc / Bcc / Reply-To ) 🆕 |
fplant_admin_email_attachments | filter | ( $attachments, $form_id, $data, $submission_id ) | Files attached to the admin email ( array of absolute paths ) 🆕 v1.5.1 |
fplant_after_admin_email_send | action | ( $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 ) .
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.
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
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_upload_dir | filter | ( $custom_dir, $form_id ) | The destination directory. $custom_dir is array( 'path' => ..., 'url' => ... ) 🆕 |
fplant_upload_filename | filter | ( $filename, $field_config, $form_id ) | The saved file name 🆕 |
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.
fplant_uploadsFile 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
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_field_types | filter | ( $field_types ) | Add or change the definitions of available field types |
fplant_field_defaults | filter | ( $defaults, $field_type ) | Default settings per field type |
fplant_field_initial_value | filter | ( 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_choices | filter | ( $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_value | filter | ( '', $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
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_custom_settings_fields | filter | ( $fields, $form_id ) | Add your own input fields to the form settings screen 🆕 |
fplant_form_settings_saved | action | ( $form_id, $settings ) | Right after the form settings are saved 🆕 |
fplant_html_setting_keys | filter | ( $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 ) .
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.
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
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_locate_template | filter | ( $template, $template_name, $template_path ) | Change the resolved path of a template file ( e.g. override on the theme side ) |
fplant_allowed_field_types | filter | ( $allowed_field_types ) | The whitelist of field types allowed in HTML templates |
fplant_template_values | filter | ( $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_html | filter | ( $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.
| Screen | Where to set it |
|---|---|
| Input screen | The input screen HTML template ( when "Use HTML template" is ON ) |
| Confirmation screen | The 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 );
- 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:
| Step | Hook | Role |
|---|---|---|
| 1. Register the type | fplant_field_types | Show the type in the add-field dialog ( label, icon, description ) |
| 2. Allow rendering | fplant_allowed_field_types | Front-end rendering whitelist ( the field will not render at all if you forget this ) |
| 3. Resolve the template | fplant_locate_template | Serve 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_errorsetc. ), and instant client-side feedback withfplant.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 aconfirm-fields/{type}.phptemplate ( resolvable viafplant_locate_templateas 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.
| Where | Treatment |
|---|---|
| Validation | Skipped ( including the required check ) |
Confirmation screen, {all_fields}, default email body | Not included |
| CSV export ( single form ) | No column is created |
| Webhook payload | Not included |
| Email tag list in the form editor | Not listed |
| Rendering with the default layout | Output 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.
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.
| Argument | Description |
|---|---|
$formatted | The default formatting ( flat arrays are comma-joined; row arrays become numbered "label: value / …" lines ) |
$value | The raw stored value ( may be an array ) |
$field | The field configuration array |
$context | Output context: confirmation / email_all_fields / email_tag / admin_detail / csv_single / csv_all |
$form_id | Form ID ( 0 when unknown ) |
$submission_id | Submission 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.
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.
| Argument | Description |
|---|---|
$cell_html | The default cell HTML ( nl2br( esc_html( formatted ) ) ) |
$raw_value | The value before formatting. Passwords arrive already masked and acceptance is already replaced by its display wording |
$field_def | Field definition ( empty array when unknown ) |
$submission | The whole submission record |
$form | The 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 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
}
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
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
fplant_export_encoding | filter | ( $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.
| Hook | Type | Arguments |
|---|---|---|
fplant_form_is_viewable 🆕 | filter | ( $viewable, $form ) |
fplant_form_is_submittable 🆕 1.5.2 | filter | ( $submittable, $form, $is_preview ) |
fplant_preview_notice 🆕 | filter | ( $notice, $form ) |
fplant_form_output 1.5.2 | filter | ( $html, $form, $context ) |
fplant_form_unavailable_message 1.5.2 | filter | ( $message, $form ) |
fplant_before_submission | action | ( $form_id, $data ) |
fplant_sanitize_field_value 1.4 | filter | ( $sanitized_value, $raw_value, $field, $form_id ) |
fplant_submission_data | filter | ( $data, $form_id ) |
fplant_submission_gate 1.5.2 | filter | ( $gate, $form, $data ) |
fplant_before_save_submission_data | filter | ( $data, $form_id ) |
fplant_after_submission | action | ( $submission_id, $form_id, $data ) |
fplant_after_submission_complete | action | ( $data, $form_id, $form, $submission_id ) |
fplant_complete_message 🆕 1.4 | filter | ( $message, $form_id, $data, $form ) |
fplant_success_html 🆕 1.4 | filter | ( $html, $form_id, $data, $form ) |
fplant_redirect_url 🆕 1.4 | filter | ( $url, $form_id, $data, $form ) |
fplant_submission_result 1.5 | filter | ( $result, $form_id, $data, $form ) |
fplant_display_fields 1.5 | filter | ( $fields, $data, $form, $context ) |
fplant_complete_shortcode_html 1.5 | filter | ( $html, $form_id, $data, $submission_id, $form ) |
fplant_complete_token_ttl 1.5 | filter | ( $ttl, $form_id ) |
fplant_webhook_should_send 1.4 | filter | ( $should_send, $webhook, $payload, $form_id, $data ) |
fplant_webhook_payload 1.4 | filter | ( $payload, $form_id, $data, $submission_id ) |
fplant_webhook_request_args 1.4 | filter | ( $args, $url ) |
fplant_webhook_allow_http 1.4 | filter | ( $allow, $url ) |
fplant_before_admin_email_send | action | ( $email_settings, $form_id ) |
fplant_skip_admin_email 🆕 | filter | ( $skip, $form, $data, $submission_id ) |
fplant_before_send_email_data | filter | ( $data, $form, $submission_id, $type ) |
fplant_admin_email_to 🆕 | filter | ( $to, $form_id, $data ) |
fplant_admin_email_subject 1.4 | filter | ( $subject, $form_id, $data ) |
fplant_admin_email_body | filter | ( $message, $form_id, $data ) |
fplant_admin_email_headers 🆕 | filter | ( $headers, $form_id, $data ) |
fplant_admin_email_attachments 1.5.1 | filter | ( $attachments, $form_id, $data, $submission_id ) |
fplant_after_admin_email_send | action | ( $email_settings, $form_id, $result ) |
fplant_before_user_email_send | action | ( $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.4 | filter | ( $subject, $form_id, $data ) |
fplant_user_email_body | filter | ( $message, $form_id, $data ) |
fplant_user_email_headers 🆕 | filter | ( $headers, $form_id, $data ) |
fplant_after_user_email_send | action | ( $email_settings, $form_id, $result ) |
fplant_upload_dir 🆕 | filter | ( $custom_dir, $form_id ) |
fplant_upload_filename 🆕 | filter | ( $filename, $field_config, $form_id ) |
fplant_field_types | filter | ( $field_types ) |
fplant_layout_field_types 1.5.2 | filter | ( $types ) |
fplant_field_defaults | filter | ( $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.2 | filter | ( $keys, $form_id ) |
fplant_locate_template | filter | ( $template, $template_name, $template_path ) |
fplant_allowed_field_types | filter | ( $allowed_field_types ) |
fplant_template_values | filter | ( $values, $form_id ) |
fplant_allowed_form_html 1.5.1 | filter | ( $allowed ) |
fplant_submission_detail_value_html 1.5.1 | filter | ( $cell_html, $raw_value, $field_def, $submission, $form ) |
fplant_format_submission_value 1.4 1.5.1 | filter | ( $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
timeoutand 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.