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.

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 )Whether the form accepts submissions 🆕
fplant_preview_noticefilter( $notice, $form )The HTML of the preview notice shown above an unpublished form 🆕

$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 );

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_submission_datafilter( $data, $form_id )Modify data right after sanitization. Return value = the modified data
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 );

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 )"Message"
fplant_success_htmlfilter( $html, $form_id, $data )"Custom page" ( the HTML after placeholder replacement )
fplant_redirect_urlfilter( $url, $form_id, $data )"Redirect" ( before esc_url_raw )

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 );

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 )Subject ( no 3rd argument )
fplant_admin_email_bodyfilter( $message, $form_id, $data )Body
fplant_admin_email_headersfilter( $headers, $form_id, $data )Headers ( From / Cc / Bcc / Reply-To ) 🆕
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.

🆕 = added in v1.2.0.

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.

Example: save into year/month folders

add_filter( 'fplant_upload_dir', function ( $dir, $form_id ) {
$u = wp_upload_dir();
$sub = '/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 🆕

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 );

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 an HTML template ( values are esc_html-escaped )

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

Example: inject a dynamic value into a template

add_filter( 'fplant_template_values', function ( $values, $form_id ) {
$values['today'] = wp_date( 'F j, Y' );
return $values;
}, 10, 2 );
// Writing {{today}} in the template replaces it with today's date

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.

HookTypeArguments
fplant_form_is_viewable 🆕filter( $viewable, $form )
fplant_form_is_submittable 🆕filter( $submittable, $form )
fplant_preview_notice 🆕filter( $notice, $form )
fplant_before_submissionaction( $form_id, $data )
fplant_submission_datafilter( $data, $form_id )
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 🆕filter( $message, $form_id, $data )
fplant_success_html 🆕filter( $html, $form_id, $data )
fplant_redirect_url 🆕filter( $url, $form_id, $data )
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_subjectfilter( $subject, $form_id )
fplant_admin_email_bodyfilter( $message, $form_id, $data )
fplant_admin_email_headers 🆕filter( $headers, $form_id, $data )
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_subjectfilter( $subject, $form_id )
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_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_locate_templatefilter( $template, $template_name, $template_path )
fplant_allowed_field_typesfilter( $allowed_field_types )
fplant_template_valuesfilter( $values, $form_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.