When I Tried Changing the Storage Destination for Contact Form 7 + Flamingo Submission Data
When using contact forms in WordPress, combining Contact Form 7 (CF7) and Flamingo is a classic choice. By default, however, submitted data is stored in the wp_posts and wp_postmeta tables within the WordPress database.
In this article, I will summarize my attempt to change these storage locations to custom tables named wp_custom_flamingo_posts and wp_custom_flamingo_meta.
Creating a Custom Plugin: Custom Flamingo Mod
To modify Flamingo’s data storage process, I created a custom WordPress plugin called “Custom Flamingo Mod".
Basic Plugin Structure
/wp-content/plugins/custom-flamingo-mod/
└── custom-flamingo-mod.php
What Was Attempted
Changing the Storage Table for Submitted Data
Changed the storage location from wp_posts and wp_postmeta to wp_custom_flamingo_posts and wp_custom_flamingo_meta.
// Create custom tables upon plugin activation
function create_custom_flamingo_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'custom_flamingo_messages';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
post_id bigint(20) unsigned NOT NULL DEFAULT 0,
post_author bigint(20) unsigned NOT NULL DEFAULT 0,
post_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
post_date_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
post_content longtext COLLATE utf8mb4_unicode_ci NOT NULL,
post_title text COLLATE utf8mb4_unicode_ci NOT NULL,
post_excerpt text COLLATE utf8mb4_unicode_ci NOT NULL,
post_status varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'publish',
comment_status varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open',
ping_status varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open',
post_password varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
post_name varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
to_ping text COLLATE utf8mb4_unicode_ci NOT NULL,
pinged text COLLATE utf8mb4_unicode_ci NOT NULL,
post_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
post_modified_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
post_content_filtered longtext COLLATE utf8mb4_unicode_ci NOT NULL,
post_parent bigint(20) unsigned NOT NULL DEFAULT 0,
guid varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
menu_order int(11) NOT NULL DEFAULT 0,
post_type varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'flamingo_message',
post_mime_type varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
comment_count bigint(20) NOT NULL DEFAULT 0,
PRIMARY KEY (ID),
KEY post_name (post_name),
KEY post_parent (post_parent),
KEY post_author (post_author),
KEY post_type (post_type)
) $charset_collate;";
$table_name = $wpdb->prefix . 'custom_flamingo_meta';
$charset_collate = $wpdb->get_charset_collate();
$sql_meta = "CREATE TABLE IF NOT EXISTS $table_name (
meta_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
post_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
meta_key VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
meta_value LONGTEXT COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (meta_id),
KEY post_id (post_id),
KEY meta_key (meta_key)
) ENGINE=InnoDB $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
dbDelta($sql_meta);
}
register_activation_hook(__FILE__, 'create_custom_flamingo_table');
How to Change the Post Data Storage Table
By utilizing the wp_insert_post hook and validating the post_type, I successfully saved the data into wp_custom_flamingo_posts.
// Change the storage table to a custom table
function copy_flamingo_data_to_custom_table($post_id, $post, $update) {
error_log("post_id: " . $post_id);
// Process only `flamingo_inbound` data
if ($post->post_type === 'flamingo_inbound') {
$existing_value = get_option('flamingo_custom_id', '');
// Concatenate new data (e.g., append "_new_data")
$new_value = $existing_value . ',' . $post_id;
// Update
update_option('flamingo_custom_id', $new_value, 'yes');
// Output log (for verification)
error_log("Updated flamingo_custom_id: " . $new_value);
global $wpdb;
$table_name = $wpdb->prefix . 'custom_flamingo_messages';
$data = array(
'ID' => NULL, // AUTO_INCREMENT
'post_id' => $post_id,
'post_author' => $post->post_author,
'post_date' => $post->post_date,
'post_date_gmt' => $post->post_date_gmt,
'post_content' => $post->post_content,
'post_title' => $post->post_title,
'post_excerpt' => $post->post_excerpt,
'post_status' => $post->post_status,
'comment_status' => $post->comment_status,
'ping_status' => $post->ping_status,
'post_password' => $post->post_password,
'post_name' => $post->post_name,
'to_ping' => $post->to_ping,
'pinged' => $post->pinged,
'post_modified' => $post->post_modified,
'post_modified_gmt' => $post->post_modified_gmt,
'post_content_filtered' => $post->post_content_filtered,
'post_parent' => $post->post_parent,
'guid' => $post->guid,
'menu_order' => $post->menu_order,
'post_type' => 'flamingo_inbound', // Type for custom table
'post_mime_type' => $post->post_mime_type,
'comment_count' => $post->comment_count,
);
// Insert data
$result = $wpdb->insert($table_name, $data);
// Delete from `wp_posts` (if necessary)
wp_delete_post($post_id, true);
return $post_id;
} elseif ($post->post_type === 'flamingo_contact') {
$existing_value = get_option('flamingo_custom_id', '');
// Concatenate new data (e.g., append "_new_data")
$new_value = $existing_value . ',' . $post_id;
// Update
update_option('flamingo_custom_id', $new_value, 'yes');
// Output log (for verification)
error_log("Updated flamingo_custom_id: " . $new_value);
global $wpdb;
$table_name = $wpdb->prefix . 'custom_flamingo_messages';
$data = array(
'ID' => NULL,
'post_id' => $post_id,
'post_type' => 'flamingo_contact',
'post_status' => 'publish',
'post_title' => $post->post_title,
'post_name' => $post->post_name,
'post_content' => $post->post_content,
);
// Insert data
$result = $wpdb->insert($table_name, $data);
// Delete from `wp_posts` (if necessary)
wp_delete_post($post_id, true);
return $post_id;
} else {
return;
}
}
add_action('wp_insert_post', 'copy_flamingo_data_to_custom_table', 10, 3);
Changing Metadata Storage
I attempted to use the update_post_meta hook to determine whether it was a Flamingo post based on the post_id stored in wp_options.
// Change metadata storage destination to the custom metadata table
function redirect_flamingo_meta_update( $null, $object_id, $meta_key, $meta_value ) {
error_log("redirect_flamingo_meta_update");
$post_type = 'post';
if( 'post' === $post_type){
$flamingo_custom_id = get_option('flamingo_custom_id', '');
error_log("Hook triggered - flamingo_custom_id: {$flamingo_custom_id}, object_id: {$object_id}, meta_key: {$meta_key}, meta_value: {$meta_value}");
if (strpos($flamingo_custom_id, $object_id) !== false) {
global $wpdb;
$custom_table = $wpdb->prefix . 'custom_flamingo_meta';
error_log("Saving to custom table - post_id: {$object_id}, meta_key: {$meta_key}, meta_value: {$meta_value}");
// Save data to custom table
$wpdb->replace(
$custom_table,
[
'post_id' => $object_id,
'meta_key' => $meta_key,
'meta_value' => $meta_value
]
);
error_log("Data saved successfully!");
// Returning `false` skips saving via `update_post_meta()`
return false;
}
}
return $null;
}
add_action( 'update_post_meta', 'redirect_flamingo_meta_update', 10, 4 );
Issue Encountered: The update_post_meta Hook Isn’t Triggering?
When I first started building this, the update_post_meta hook should have been working normally, but for some reason, it stopped responding halfway through.
Wondering “Why?", I continued debugging—such as switching to update_metadata—but the update_post_meta hook simply refused to fire, and I haven’t been able to identify the cause so far.
Discussion: Does Flamingo Follow WordPress Best Practices for Data Storage?
Looking back at Flamingo’s data storage method, it seems to align with standard WordPress data management workflows.
Initially, I thought that “separating tables would make it easier to check when looking directly at the DB," but when checking the database directly, filtering the wp_posts table with SELECT * FROM `wp_posts` WHERE `post_type` LIKE 'flamingo%' is probably sufficient.
Furthermore, by leveraging the standard WordPress data management workflow, you benefit from features like database optimization tools (e.g., LiteSpeed Cache) that can automatically clean up unnecessary (orphaned) postmeta information. Managing data in custom tables means you lose out on these standard WordPress features. Therefore, going out of your way to separate tables might not even be necessary.
However, because wp_postmeta stores individual data points—such as names, email addresses, and message contents—separately for each inquiry, it is designed so that the number of records increases rapidly. On sites receiving a massive volume of inquiries, this could potentially become a bottleneck.
Conclusion: Is Table Partitioning Necessary?
Although I tried creating custom tables, data management that leverages standard WordPress features might actually be more practical.
That said, since the wp_postmeta approach can impact performance as data grows, sites receiving a heavy volume of inquiries will likely need to carefully consider their optimal storage method.
If I feel up to it, I plan to continue testing this in the future.