When is wp_mail NOT pluggable?

Have you heard about Pluggable Functions in WordPress? A while ago I wrote a tutorial about how to log email by plugging wp_mail which is a pluggable function.

Then I went off skipping on my way in to a land of bliss knowing I’d never again email clients by accident.

Cue the dark clounds and ominous music.

wp_mail is not plugged when…

So there I was skipping along through a field of flowers and troubleshooting some password reset issues on a staging site only to find that the password reset emails were not getting logged.

I had actually just sent a password reset email to a user by accident which made me frown and my client frowned. Lucky for us the user was a developer of a different stripe (not a WordPress developer) so when I emailed him about the mistake he understood.

We still looked silly.

wp_mail is not pluggable during a password reset it will send your emails so my previous plugin didn’t work.

We were also running in to issues with plugins like [wpMandril][mandril] which also ‘plug’ wp_mail. You can’t plug a function twice though so all we got were errors.

Logging all wp_mail without plugging it

Now let’s look at how I log wp_mail without using the pluggable function pattern. We’ll start by taking a look at the full wp_mail code.

if ( !function_exists( 'wp_mail' ) ) :
 * Send mail, similar to PHP's mail
 * A true return value does not automatically mean that the user received the
 * email successfully. It just only means that the method used was able to
 * process the request without any errors.
 * Using the two 'wp_mail_from' and 'wp_mail_from_name' hooks allow from
 * creating a from address like 'Name <email@address.com>' when both are set. If
 * just 'wp_mail_from' is set, then just the email address will be used with no
 * name.
 * The default content type is 'text/plain' which does not allow using HTML.
 * However, you can set the content type of the email by using the
 * 'wp_mail_content_type' filter.
 * The default charset is based on the charset used on the blog. The charset can
 * be set using the 'wp_mail_charset' filter.
 * @since 1.2.1
 * @uses apply_filters() Calls 'wp_mail' hook on an array of all of the parameters.
 * @uses apply_filters() Calls 'wp_mail_from' hook to get the from email address.
 * @uses apply_filters() Calls 'wp_mail_from_name' hook to get the from address name.
 * @uses apply_filters() Calls 'wp_mail_content_type' hook to get the email content type.
 * @uses apply_filters() Calls 'wp_mail_charset' hook to get the email charset
 * @uses do_action_ref_array() Calls 'phpmailer_init' hook on the reference to
 *      phpmailer object.
 * @uses PHPMailer
 * @param string|array $to Array or comma-separated list of email addresses to send message.
 * @param string $subject Email subject
 * @param string $message Message contents
 * @param string|array $headers Optional. Additional headers.
 * @param string|array $attachments Optional. Files to attach.
 * @return bool Whether the email contents were sent successfully.
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
    // Compact the input, apply the filters, and extract them back out
    extract( apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) ) );
    if ( !is_array($attachments) )
        $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
    global $phpmailer;
    // (Re)create it, if it's gone missing
    if ( !is_object( $phpmailer ) || !is_a( $phpmailer, 'PHPMailer' ) ) {
        require_once ABSPATH . WPINC . '/class-phpmailer.php';
        require_once ABSPATH . WPINC . '/class-smtp.php';
        $phpmailer = new PHPMailer( true );
    // Headers
    if ( empty( $headers ) ) {
        $headers = array();
    } else {
        if ( !is_array( $headers ) ) {
            // Explode the headers out, so this function can take both
            // string headers and an array of headers.
            $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
        } else {
            $tempheaders = $headers;
        $headers = array();
        $cc = array();
        $bcc = array();
        // If it's actually got contents
        if ( !empty( $tempheaders ) ) {
            // Iterate through the raw headers
            foreach ( (array) $tempheaders as $header ) {
                if ( strpos($header, ':') === false ) {
                    if ( false !== stripos( $header, 'boundary=' ) ) {
                        $parts = preg_split('/boundary=/i', trim( $header ) );
                        $boundary = trim( str_replace( array( "'", '"' ), '', $parts[1] ) );
                // Explode them out
                list( $name, $content ) = explode( ':', trim( $header ), 2 );
                // Cleanup crew
                $name    = trim( $name    );
                $content = trim( $content );
                switch ( strtolower( $name ) ) {
                    // Mainly for legacy -- process a From: header if it's there
                    case 'from':
                        if ( strpos($content, '<' ) !== false ) {
                            // So... making my life hard again?
                            $from_name = substr( $content, 0, strpos( $content, '<' ) - 1 );
                            $from_name = str_replace( '"', '', $from_name );
                            $from_name = trim( $from_name );
                            $from_email = substr( $content, strpos( $content, '<' ) + 1 );
                            $from_email = str_replace( '>', '', $from_email );
                            $from_email = trim( $from_email );
                        } else {
                            $from_email = trim( $content );
                    case 'content-type':
                        if ( strpos( $content, ';' ) !== false ) {
                            list( $type, $charset ) = explode( ';', $content );
                            $content_type = trim( $type );
                            if ( false !== stripos( $charset, 'charset=' ) ) {
                                $charset = trim( str_replace( array( 'charset=', '"' ), '', $charset ) );
                            } elseif ( false !== stripos( $charset, 'boundary=' ) ) {
                                $boundary = trim( str_replace( array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset ) );
                                $charset = '';
                        } else {
                            $content_type = trim( $content );
                    case 'cc':
                        $cc = array_merge( (array) $cc, explode( ',', $content ) );
                    case 'bcc':
                        $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
                        // Add it to our grand headers array
                        $headers[trim( $name )] = trim( $content );
    // Empty out the values that may be set
    // From email and name
    // If we don't have a name from the input headers
    if ( !isset( $from_name ) )
        $from_name = 'WordPress';
    /* If we don't have an email from the input headers default to wordpress@$sitename
     * Some hosts will block outgoing mail from this address if it doesn't exist but
     * there's no easy alternative. Defaulting to admin_email might appear to be another
     * option but some hosts may refuse to relay mail from an unknown domain. See
     * http://trac.wordpress.org/ticket/5007.
    if ( !isset( $from_email ) ) {
        // Get the site domain and get rid of www.
        $sitename = strtolower( $_SERVER['SERVER_NAME'] );
        if ( substr( $sitename, 0, 4 ) == 'www.' ) {
            $sitename = substr( $sitename, 4 );
        $from_email = 'wordpress@' . $sitename;
    // Plugin authors can override the potentially troublesome default
    $phpmailer->From     = apply_filters( 'wp_mail_from'     , $from_email );
    $phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name  );
    // Set destination addresses
    if ( !is_array( $to ) )
        $to = explode( ',', $to );
    foreach ( (array) $to as $recipient ) {
        try {
            // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
            $recipient_name = '';
            if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                if ( count( $matches ) == 3 ) {
                    $recipient_name = $matches[1];
                    $recipient = $matches[2];
            $phpmailer->AddAddress( $recipient, $recipient_name);
        } catch ( phpmailerException $e ) {
    // Set mail's subject and body
    $phpmailer->Subject = $subject;
    $phpmailer->Body    = $message;
    // Add any CC and BCC recipients
    if ( !empty( $cc ) ) {
        foreach ( (array) $cc as $recipient ) {
            try {
                // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
                $recipient_name = '';
                if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                    if ( count( $matches ) == 3 ) {
                        $recipient_name = $matches[1];
                        $recipient = $matches[2];
                $phpmailer->AddCc( $recipient, $recipient_name );
            } catch ( phpmailerException $e ) {
    if ( !empty( $bcc ) ) {
        foreach ( (array) $bcc as $recipient) {
            try {
                // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
                $recipient_name = '';
                if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
                    if ( count( $matches ) == 3 ) {
                        $recipient_name = $matches[1];
                        $recipient = $matches[2];
                $phpmailer->AddBcc( $recipient, $recipient_name );
            } catch ( phpmailerException $e ) {
    // Set to use PHP's mail()
    // Set Content-Type and charset
    // If we don't have a content-type from the input headers
    if ( !isset( $content_type ) )
        $content_type = 'text/plain';
    $content_type = apply_filters( 'wp_mail_content_type', $content_type );
    $phpmailer->ContentType = $content_type;
    // Set whether it's plaintext, depending on $content_type
    if ( 'text/html' == $content_type )
        $phpmailer->IsHTML( true );
    // If we don't have a charset from the input headers
    if ( !isset( $charset ) )
        $charset = get_bloginfo( 'charset' );
    // Set the content-type and charset
    $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
    // Set custom headers
    if ( !empty( $headers ) ) {
        foreach( (array) $headers as $name => $content ) {
            $phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
        if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) )
            $phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
    if ( !empty( $attachments ) ) {
        foreach ( $attachments as $attachment ) {
            try {
            } catch ( phpmailerException $e ) {
    do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );
    // Send!
    try {
        return $phpmailer->Send();
    } catch ( phpmailerException $e ) {
        return false;

That’s a lot of code!

The important parts are in the last 5 lines of the main function though. Specifically do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );. Here we have a WordPress action that we can hook and it passes in all of the stuff thats been set up inwp_mail` to the PHPMailer class.

At this point in wp_mail everything is setup. We have a full message contents, proper headers, all the from/to/cc email addresses are set. The very next thing that wp_mail tries to do is send the email from the server.

So that phpmailer_init hook is where we need to be.

function wptt_mail_log( $phpmailer ){
    return $phpmailer;
} // mail_log
add_action( 'phpmailer_init', 'wptt_mail_log' );

Now we have a function that turns off all email going out of WordPress by clearing all the recipients for the email. There are literally no email addreses to send to so it goes no where.

Now of course that doesn’t create the log so take a look at the full code of my WPTT Basic Email Logging plugin to see how we create the logs and how we limit it to only capturing emails if we are not on a live environment.


Screen Shot 2014-03-20 at 8.06.42 AM

Change product category order in WooCommerce

The default setting for WooCommerce product category listing is to order them however you entered them.

I’ve never found this to be useful. Every client I’ve had wants categories to be ordered alphabetically. Today I’ll show you how.

woocommerce_product_subcategories_args filter

WooCommerce provides a nice filter to change the product category archive parameters which you can find with the rest of the documented filters.

Here’s an excerpt of the filter out of the main function in WooCommerce that displays categories. You can see the whole thing by opening wc-template-functions.php found inside the WooCommerce plugin includes folder.

        // NOTE: using child_of instead of parent - this is not ideal but due to a WP bug ( http://core.trac.wordpress.org/ticket/15626 ) pad_counts won't work
        $args = apply_filters( 'woocommerce_product_subcategories_args', array(
            'child_of'      => $parent_id,
            'menu_order'    => 'ASC',
            'hide_empty'    => 1,
            'hierarchical'  => 1,
            'taxonomy'      => 'product_cat',
            'pad_counts'    => 1
        ) );
        $product_categories     = get_categories( $args );

You can see here that the filter is allowing you to change the args that are sent to a standard WordPress get_categories call.

That means we can use any of the get_categories arguments here. So we’re going to change the orderby and order parameters.

 * Changes the order of the product categories to be by slug ASC.
 * Essentially alphabetical.
 * @since 1.0
 * @author WPTT, Curtis McHale
function wptt_cat_order( $args ){
    $args['orderby'] = 'slug';
    $args['order'] = 'ASC';
    return $args;
} // wptt_cat_order
add_filter( 'woocommerce_product_subcategories_args', 'wptt_cat_order' );

Now you might be wondering why I’m using ‘slug’ instead of name. See if your category titles have fancy characters like & the alphabetical order on name gets thrown off. Slug is a normalized field, which means that it gets all the fancy text stripped out.

Setting up Stripe with WooCommerce

Today’s screencast will show you how to set up Stripe with WooCommerce since I’ve had a few questions about it.

Screen Shot 2014-02-01 at 9.19.18 PM

New Guide to deployment with Beanstalk

Quite a while ago I wrote about deployment with Beanstalk and that’s still how I do it.

Beanstalk just also released a guide to deploying WordPress for a slightly different take on the matter.

Screen Shot 2014-01-22 at 2.13.02 PM

Serializing and Saving a complete form with jQuery Form in WordPress

WordPress comes bundled with a great jQuery library called jQuery Form which makes saving forms via AJAX super easy.

Today I’m going to show you why you shouldn’t be using standard AJAX calls to save forms and how to use jQuery Form in WordPress.

You can download the plugin I use in the screencast from Github[plugin].


Screen Shot 2014-01-22 at 1.19.02 PM

Easier Documentation Searching with Dash

Part of being a great developer is simply knowing how to search for your current issue. There is no way I could keep all the documention for WordPress, PHP, jQuery, Sass…in my head.

My typical workflow has been to simply Google for the term and then click the first link since it’s usually the WordPress Codex entry.

Wouldn’t it be nicer to be able to search across many languages right out of your editor?

How about StackExchange and Google results mixed right in?

Well you can.

It is called Dash

Dash is a Mac App that is free to trial and has a $19.99 in app purchase.

At it’s simplest level dash is just a dedicated application to search documentation than includes Google Results and StackExchange results.

Where it really gets powerful (and what convinced me to part with $19.99 in 2 minutes) is the integration with your editor.

I use Vim so for me I hop on to GitHub and download the dash Vim plugin. That gives me access to the :Dash command from within my editor.

:Dash wp_send_json will bring up all the json functions in WordPress with Google and Stackexchange right under.

:Dash esc_ wordpress will give me all the escaping functions in WordPress (since I limited it) along with the Google and StackExchange results under it. If I wanted anything with esc_ in it then I’d omit the wordpress exclusion and I’d see PHP options as well.


Installing documention is a simple as clicking download from the Dash preferences.


Dash is awesome, go get it.


Screen Shot 2013-12-19 at 2.50.07 PM

Add a custom folder on Plugin activation

Recently I needed to do a custom export of WooCommerce orders so we could send them to an external service. The external service could already read orders formatted in an XML file, so all I needed to do was get the WooCommerce orders exported in the XML format and put in a directory that could be accessed by the external service.

The first part of that was creating a new place to store files in WooCommerce which meant I needed to create a new folder when my plugin was activated.

Today we’ll take a look at how to add a folder on plugin activation.

Plugin Skeleton

The first thing we need is a plugin skeleton.

Plugin Name: WPTT mkdir
Plugin URI: http://wpthemetutorial.com
Description: Demo to make a directory on plugin activation
Version: 1.0
Author: WPTT, Curtis McHale
Author URI: http://wpthemetutorial.com
License: GPLv2 or later
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
class WPTT_Mkdir{
    function __construct(){
        add_action( 'admin_notices', array( $this, 'check_required_plugins' ) );
        // Register hooks that are fired when the plugin is activated, deactivated, and uninstalled, respectively.
        register_activation_hook( __FILE__, array( $this, 'activate' ) );
        register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
        register_uninstall_hook( __FILE__, array( __CLASS__, 'uninstall' ) );
    } // construct
     * Checks for WooCommerce and GF and kills our plugin if they aren't both active
     * @uses    function_exists     Checks for the function given string
     * @uses    deactivate_plugins  Deactivates plugins given string or array of plugins
     * @action  admin_notices       Provides WordPress admin notices
     * @since   1.0
     * @author  SFNdesign, Curtis McHale
    public function check_required_plugins(){
        if( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ){ ?>
            <div id="message" class="error">
                WPTT mkdir expects WooCommerce to be active. This plugin has been deactivated.
            deactivate_plugins( '/wptt-mkdir/wptt-mkdir.php' );
        } // if
    } // check_required_plugins
     * Fired when plugin is activated
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
    public function activate( $network_wide ){
    } // activate
     * Fired when plugin is deactivated
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
    public function deactivate( $network_wide ){
    } // deactivate
     * Fired when plugin is uninstalled
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
    public function uninstall( $network_wide ){
    } // uninstall
} // WPTT_Mkdir
new WPTT_Mkdir();

The function we’re going to spend our time in today is activate. activate is called by register_activation_hook which makes any of the content of the function run on plugin activation. Sure we could also run it on some WordPress hook like init but then it would run every time that the site is visited which would be dump.

Adding to register_activation_hook

The first thing we want to add to register_activation_hook is our function that will create the directory.

     * Fired when plugin is activated
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
    public function activate( $network_wide ){
    } // activate

Now let’s take a look at our $this->create_custom_dir function.

     * Creates our custom upload directory
     * @since 1.0
     * @author SFNdesign, Curtis McHale
     * @access private
     * @uses $this->check_custom_dir()          Returns true if the dir already exists
     * @uses wp_mkdir_p()                       Creates dirpath recursively and sets file permissions if possible
    public function create_custom_dir(){
        if ( $this->check_custom_dir() === true ) return;
        $url = WP_CONTENT_DIR . '/uploads/wpttcustom';
        wp_mkdir_p( $url );
    } // create_custom_dir

The first thing we do is check if the directory exists with another custom function (I’ll show you that in a second). If our directory already exists, because the user has already activated the plugin once, then we stop what we are doing.

If the directory hasn’t been created then we want to create it.

To do that we first use the WordPress constant WP_CONTENT_DIR which will give us the path to the wp-content folder on the server.

To that constant we append the path we want to create a directory in. For our case we use /uploads/wpttcustom.

Then we pass the path to wp_mkdir_p. wp_mkdir_p will do it’s best to create our directory recursively. So if we really wanted a path like /uploads/wptt/custom/anotherlevel it would create the whole path for us.

Checking if the directory exists

At the top of my function I had a check to see if the directory existed. Lets look at that now.

     * Returns true if the custom directory is in the uploads folder
     * @since 1.0
     * @author SFNdesign, Curtis Mchale
     * @access private
    private function check_custom_dir(){
        $url = WP_CONTENT_DIR . '/uploads/wpttcustom';
        if ( file_exists( $url ) ){
            return true;
        return false;
    } // check_custom_dir

Here we use the same constant WP_CONTENT_DIR and define the same path and then use the PHP function file_exists to see if the directory exists.

If it does we will return true. If it doesn’t our function will send false, which would mean that we create the directory.

Now you may wonder why I broke 5 lines out in to it’s own function. A programming convention is the Single Responsibility Principle. So the only job of our create function is to create the directory. The only job of our check function is to check if the directory exists. You can read more about SOLID on Wikipedia.

Wrap up

That’s it, we have now created a custom directory on plugin activation.


Screen Shot 2013-12-19 at 2.07.23 PM

Adding a new domain to an existing VVV (Vagrant) box

I’ve already written 2 posts on Vagrant, specifically using VVV to run your WordPress development boxes.

  1. Working with WordPress and Vagrant – Basics
  2. Vagrant and Custom Domains with WordPress

The second post did show you how you could add a custom domain to a Vagrant install, but didn’t specifically call out my day to day workflow for using Vagrant.

My VVV workflow

Most of the time I use the same VVV box for all projects. So all theme builds and plugin builds…To add a new site to an existing VVV box I use the instructions you’ll find below.

If I have to build a WordPress MU site I usually build a custom VVV box that has all the domains mapped and the dabatase setup for the user. Then the next developer just has to clone our VVV box and add the hosts file entries to get the project running.

Adding the new domain

Our first step is to open you terminal program and change directory (cd) to the VVV install you want to add a domain to. From the root type cd config/nginx/sites.

Here you should find a few files ending in the .conf extension. Copy one and rename it to screencast.wpthemetutorial.com.conf then open it in your editor of choice. You’ll need to change 2 lines.

  1. On line 28 change the server name to match the domain you want to see the site at: server_name screencast.wpthemetutorial.com;
  2. On line 31 you should see the path to the install location in your www directory: root /srv/www/wptt-screen;

Now we need to go back to the root of the Vagrant box and go in to our database directory by typing cd database.

Then open init-custom.sql so we can add our custom database commands. I copy the last entry and pasted it below. I’ll change all our values to swptt. I don’t think it’s worth making it super long/hard since this is not a live web sever and anyone that can access this has access to your machine.

GRANT ALL PRIVILEGES ON `swptt`.* TO 'swptt'@'localhost' IDENTIFIED BY 'swptt';

Now we need to go back to the root of the VVV box and type cd www to get to the web directory on our server. Here we need to move all the WordPress files in to their proper spot in the wptt-screen folder.

You can do this by typing cp -rv wordpress-default/ wptt-screen.

Finally I want to remove the wp-config.php file that was in the copied WordPress install. Type rm -rf wptt-screen/wp-config.php and the wp-config.php file will be gone.

Now you need to type vagrant up to start the VVV box.

Once Vagrant comes up all you should need to do is type screencast.wpthemetutorial.com in to your browser and then follow the WordPress install steps.


WordPress Environment Configuration

I always work locally on client sites. It’s simply way faster to save a file and refresh the browser instead of waiting for it to upload then refreshing. You can also make mistakes locally and kill the whole site without worrying about downtime for your client.

It’s also pretty common for me to have a staging environment so clients can approve work before it launches to the live site.

These different environments do present some issues though. We don’t want users of the site to be able to access the staging site and we certainly don’t want to send any automated emails (like orders from WooCommerce) from the staging site as we test things. We do want to know if the emails got sent though and what their content was.

To catch emails I wrote a plugin that replaces the wp_mail function. There I recommended you set a constant in wp-config.php of DEVELOPMENT but there can be issues with setting those. Specifically if you’re using WPEngine and make use of their live to staging push features.

When you push live->staging on WPEngine it ends up changing wp-config.php back to it’s default values. That means your DEVELOPMENT constant is gone and you’re sending emails to clients again from the staging site. So any of the other cool options based on wp-config.php won’t work either.

To get around this quirk of how WPEngine operates I’ve released a plugin called WPTT Developer Constants. It’s meant to be run as an mu-plugin on your site. That means it will always be run and the constants will be available before your theme or plugins are loaded.

That means you could hook plugins_loaded and check to see if certain plugins are turned on based on the environment you are in. Like turn on the email logging plugin for STAGING_ENV and LOCAL_ENV and make sure it’s off for LIVE_ENV.

If you were using the older style DEVELOPMENT constant then I’ve kept that around as well. I’m not using it anymore since I found I want different environment configuration locally (like why restrict access to outsiders, it’s my computer it’s running on).

Go check the plugin out.

Screen Shot 2013-10-11 at 3.52.53 PM

Pre-filled product inquiry form with Gravity Forms and WooCommerce

Today we’re going to conitue from last weeks screencast on removing the purchase button from all products inside a category. It was great to get the button removed but makes the page almost totally useless.

Now we’re going to take a Gravity Forms form and add it to our page so that users can fill out the form and inquire about the product.

Setting up the form

To start with we need to set up our form with the fields we need. I have mine set to ask for the name, email, phone number. It also provides a textarea for the site user to ask their question.

The final field is called product name and we’re going to dynimically populate it with the name of the product that is currently on the page. Asking the customer to fill out that extra bit of information is just sucky so we’ll do it for them.

Under the field advainced tab you need to check off “Allow field to be populated dynamically” and then you need to enter a parameter. We’ll use ‘west_boat_name’.


The parameter is just a way for Gravity Forms to be able to reference the field for our dynamic population.

Adding the form to our product

Now that we have our form ready we want to add it to our product and here is our code.

 * Our custom text
 * @uses get_product()      Returns the product object for the current product in the loop
 * @uses get_the_title()    Returns the title given the post_id
 * @uses do_shortcode()     Does the shortcode content
function western_boat_purchase_text(){
    $product = get_product();
    $product_title = get_the_title( $product->ID );
    <section class="west-product-inquire">
        You can call us at 1-866-644-8111 or fill out the form below.
        <?php echo do_shortcode( '[gravityform id="4" name="Boat Inquiry Form" title="false" description="false" field_values="west_boat_name='.$product_title.'"]'); ?>
    </section><!-- /.west-product-inquire -->
} // western_boat_purchase_text

We start by getting the full product object with get_product. Once we have that we want the name of the product and we can use get_the_title to do that by passing it the $post_id of the product.

Now we have a basic HTML wrapper for our text and code. We provide the site user with a contact phone number and then we use do_shortcode to call out the form.

You can get your form_id from the URL of the gravity form. In my case it’s form_id 4.

gf-form-id 13-10-11 3.50.18 PM

While there are a number of ways to dynamically populate a form field we’re going to pass in our custom values in the shortcode. That means we use the field_values parameter and pass in our custom value of $product_title.

That’s it, we now have a product inquiry form on our page that fills in the product title for the user.