Create Your Own Tokens for Fun and Profit

Published
2012-01-16 12:14
Written by

One of my favorite features in CiviCRM 4.1 is the improved support for custom tokens via hooks. It's really opened up the possibilities for building some great functionality and new workflows in CiviCRM. If you already know what tokens and hooks are, skip down to see some cool examples.

Wait a minute, what are hooks and tokens?

When composing a mass-email, letter, etc. you can personalize it with tokens. Tokens are little placeholders that can get replaced with something different for each contact. Traditionally a token is simply a field in the database, so if you start your letter off with "Dear {contact.first_name}" it will become "Dear Robert" when sent to Bob. But as we will see, you can do a lot more with tokens than just pull a single contact field.

That's where hooks come into play. Hooks are opportunities CiviCRM gives you to inject your own functionality during certain tasks, adding-to or modifying whatever it was doing. Let's see how a hook can improve the above example:

Some "OR" Logic

Say you're composing a mass-email, and two of the recipients are Bob and Coleman. In the database, Bob's first name is Robert, and Bob is his nick name. You want to send a nice, friendly letter, so you compose your message thusly:

"Dear {contact.nick_name}, have you seen our blog lately?"

The message sent to Robert will replace the token with his nick-name, and it will say:

"Dear Bob, have you seen our blog lately?"

Awesome, but since Coleman doesn't have a nick name, his message will say:

"Dear , have you seen our blog lately?"

Oops. No name got printed. Wouldn't it be nice if that token could have defaulted to first_name if nick_name was missing? There's also the case of a contact that didn't have a first name or a nick name (sometimes happens with newsletter sign-ups, when all we get is an email address). In that case it would have been nice to say "Dear Friend." Enter hook_civicrm_tokenValues to the rescue! Here's some code that does just that:

function hook_civicrm_tokenValues(&$values, $cids, $job = null, $tokens = array(), $context = null) {
  $contacts = implode(',', $cids);
  $tokens += array(
    'contact' => array(),
  );
  // Fill first name and nick name with default values
  if (in_array('first_name', $tokens['contact']) || in_array('nick_name', $tokens['contact'])) {
    $dao = &CRM_Core_DAO::executeQuery("
      SELECT first_name, nick_name, contact_type, id
      FROM civicrm_contact
      WHERE id IN ($contacts)"
    );
    while ($dao->fetch()) {
      $cid = $dao->id;
      if (!($values[$cid]['first_name'] = $dao->first_name)) {
        $values[$cid]['first_name'] = $dao->contact_type == 'Individual' ? 'Friend' : 'Friends';
      }
      if (empty($values[$cid]['nick_name']) || $dao->contact_type != 'Individual') {
        $values[$cid]['nick_name'] = $values[$cid]['first_name'];
      }
    }
  }
}

A couple things to notice in this code: Prior to version 4.1 the first two params ($values and $cids) were the only ones available. The biggest shortcoming of that was that it was impossible to know if particular tokens were even called for. Now we have the $tokens param which tells us exactly what tokens are in the message. That's critical for allowing us to do fancy things with tokens only when needed, without bogging down every single message the system ever sends! You'll also notice that we were able to process all the contacts in bulk with a single query - much faster than calling the api once per contact! And since large mailings are prone to timeout anyway, it's important to not add any unnecessary overhead.

Beyond Existing Tokens

Just because core CiviCRM tokens are all database fields doesn't mean yours have to be. Here's a simple example of tokens for today's date (useful for form letter templates). First define your new tokens with hook_civicrm_tokens, then fill their values with hook_civicrm_tokenValues:

function hook_civicrm_tokens(&$tokens) {
  $tokens['date'] = array(
    'date.date_short' => 'Today\'s Date: mm/dd/yyyy',
    'date.date_med' => 'Today\'s Date: Mon d yyyy',
    'date.date_long' => 'Today\'s Date: Month dth, yyyy',
  );
}
function hook_civicrm_tokenValues(&$values, $cids, $job = null, $tokens = array(), $context = null) {
  // Date tokens
  if (!empty($tokens['date'])) {
    $date = array(
      'date.date_short' => date('m/d/Y'),
      'date.date_med' => date('M j Y'),
      'date.date_long' => date('F jS, Y'),
    );
    foreach ($cids as $cid) {
      $values[$cid] = empty($values[$cid]) ? $date : $values[$cid] + $date;
    }
  }
}

That was really easy since we neither had to look anything up in the database, nor give each contact a different value. But it can also get way more complex.

Advanced Usage: Contribution Thanks

It always felt like there was a missing piece in CiviCRM in terms of a workflow for sending thank-you letters for donations. Thanks to some fancy custom tokens, I've finally plugged that leak. Here's my new flow:

  1. Use advanced contact search to find all contacts who have donated but haven't yet been thanked
  2. From the search results, choose "PDF letter"
  3. Compose a message (using that nice "date" token at the top, and our smart name token)
  4. Insert another token at the bottom which generates a table of recent (unthanked) contribuions
  5. Generate a test letter. If I'm happy with it, I'll add a final token which updates those contributions and sets their thank-you date to today

Here's the code for those new tokens:

function hook_civicrm_tokens(&$tokens) {
  $tokens['donor'] = array(
    'donor.unthanked' => 'Donations: To Thank',
    'donor.set_thank_you' => 'Donations: MARK AS THANKED',
    'donor.clear_thank_you' => 'Donations: CLEAR TODAYS THANKED',
  );
}
function hook_civicrm_tokenValues(&$values, $cids, $job = null, $tokens = array(), $context = null) {
  $contacts = implode(',', $cids);
  // Dontation info for contact and spouse
  if (!empty($tokens['donor'])) {
    $spouses = array();
    $contacts_and_spouses = $cids;
    $dao = &CRM_Core_DAO::executeQuery("
      SELECT contact_id_a, contact_id_b
      FROM civicrm_relationship
      WHERE relationship_type_id = 2
      AND is_active = 1
      AND (end_date IS NULL OR end_date > CURDATE())
      AND (contact_id_a IN ($contacts) OR contact_id_b IN ($contacts))
    ");
    while ($dao->fetch()) {
      if (!in_array($dao->contact_id_a, $contacts_and_spouses)) {
        $contacts_and_spouses[] = $dao->contact_id_a;
      }
      if (!in_array($dao->contact_id_b, $contacts_and_spouses)) {
        $contacts_and_spouses[] = $dao->contact_id_b;
      }
      if (in_array($dao->contact_id_a, $cids)) {
        $spouses[$dao->contact_id_b] = $dao->contact_id_a;
      }
      if (in_array($dao->contact_id_b, $cids)) {
        $spouses[$dao->contact_id_a] = $dao->contact_id_b;
      }
    }
    $contacts_and_spouses = implode(',', $contacts_and_spouses);
    // Clear today's thank-yous (a kind of crude UNDO)
    if (in_array('clear_thank_you', $tokens['donor'])) {
      CRM_Core_DAO::executeQuery("
        UPDATE civicrm_contribution SET thankyou_date = NULL
        WHERE is_test = 0 AND financial_type_id = 1 AND contribution_status_id = 1
        AND DATE(thankyou_date) = CURDATE()"
      );
    }
    if (in_array('unthanked', $tokens['donor'])) {
      $dao = &CRM_Core_DAO::executeQuery("
        SELECT cc.contact_id, cc.total_amount, cc.receive_date, cc.check_number, con.display_name, hon.display_name as honoree, pi.label AS payment_instrument, ht.label AS honor_type
        FROM civicrm_contribution cc
        INNER JOIN civicrm_contact con ON con.id = cc.contact_id
        LEFT JOIN civicrm_contact hon ON hon.id = cc.honor_contact_id
        LEFT JOIN civicrm_option_value pi ON cc.payment_instrument_id = pi.value AND pi.option_group_id = (SELECT id FROM civicrm_option_group WHERE name = 'payment_instrument')
        LEFT JOIN civicrm_option_value ht ON cc.honor_type_id = ht.value AND ht.option_group_id = (SELECT id FROM civicrm_option_group WHERE name = 'honor_type')
        WHERE cc.is_test = 0 AND cc.financial_type_id = 1 AND cc.contribution_status_id = 1
        AND cc.contact_id IN ($contacts_and_spouses) AND cc.thankyou_date IS NULL
        ORDER BY cc.receive_date"
      );
      $header = '
        <table class="donations">
          <thead><tr>
            <th>Date</th>
            <th>Donor</th>
            <th>Amount</th>
            <th>Paid By</th>
            <th>Notes</th>
          </tr></thead>
          <tbody>';
      while ($dao->fetch()) {
        $cid = $dao->contact_id;
        $row = '
          <tr>
            <td>' . date('m/d/Y', strtotime($dao->receive_date)) . '</td>
            <td>' . $dao->display_name . '</td>
            <td>$' . $dao->total_amount . '</td>
            <td>' . ($dao->payment_instrument ? $dao->payment_instrument : 'In Kind') 
            . ($dao->check_number ? ' #' . $dao->check_number : '') . '</td>
            <td>' . ($dao->honoree ? "<br />{$dao->honor_type} {$dao->honoree}" : '') . '</td>
          </tr>';
        if (in_array($cid, $cids)) {
          $values[$cid]['donor.unthanked'] = (!empty($values[$cid]['donor.unthanked']) ? $values[$cid]['donor.unthanked'] : $header) . $row;
        }
        if (isset($spouses[$cid])) {
          $values[$spouses[$cid]]['donor.unthanked'] = (!empty($values[$spouses[$cid]]['donor.unthanked']) ? $values[$spouses[$cid]]['donor.unthanked'] : $header) . $row;
        }
      }
      foreach ($cids as $cid) {
        if (!empty($values[$cid]['donor.unthanked'])) {
          $values[$cid]['donor.unthanked'] .= '</tbody></table>';
        }
      }
    }
    if (in_array('set_thank_you', $tokens['donor'])) {
      CRM_Core_DAO::executeQuery("
        UPDATE civicrm_contribution SET thankyou_date = NOW()
        WHERE is_test = 0 AND financial_type_id = 1 AND contribution_status_id = 1
        AND contact_id IN ($contacts_and_spouses) AND thankyou_date IS NULL"
      );
    }
  }
}

Notice that the code actually takes it a bit farther and also looks up the person's spouse, if they have one, and their donations, so that we can send one thank-you letter to a couple instead of two. We have our postal greeting populated with both names (thanks to, you guessed it, another hook), which makes it easy to address a letter to a couple without having to mess with households.

Updating your database with a token is maybe not the best all-around practice, but I liked the simple elegance of having donations marked as thanked upon printing a thank-you letter. Also notice that I created a third token to clear all thank-you dates which were set to today, a kind of undo in case something goes wrong and the letters need to be reprinted.

The List Goes On

We're also now using these hooks to:

  • Create a "formatted address" token, also great for form letters
  • Print a student's transcript - generating a table with a row for each course - from a multi-valued custom fieldset
  • Create a token for "parents names" "parents addresses" and "emergency contact" pulling the relevant info from related contacts
  • Lookup and print the dates a student attended our school and their graduation status

You can do just about anything with custom tokens. Use your imagination!

Comments

Reallly nice examples,

 

If you have already custom token in 3.4 or 4.0, I would encourage of all you to modify them to benefit from that extra param  $tokens to skip fetching a lot of informations for a token that isn't use.

 

The $cids param might be an array or a single id, isn't it? Or has it changed since 4.0?

On line 1131 of CRM/Mailing/BAO/Mailing.php there is a call to ...

CRM_Utils_Hook::tokenValues($contact, $contactId, $job_id);

Not sure if that's a bug or a feature!

Ken Williams (not verified)
2012-01-25 - 20:01

Thanks much for this writeup, the stuff on donor thank-you letters is spot on for what I need. One question though, if we want to implement custom hooks & tokens, where do we stick the code? Some directory somewhere?

Depends on your CMS. See the instructions for implementing hooks for a how-to.

If using drupal, create a "mysite" module - IMO every site should have one of these. Then just add your hooks to it. You can copy-paste the code directly from this article, just replace the word hook with mysite or whatever you decided to name your module.

Anonymous (not verified)
2012-04-29 - 20:30

Colemanw,

Great examples, I'm putting them to use!  I want to make some changes to the donation token.

I want to be able to do a search for all contacts that have made a donation in the last year and the token cycle through those contacts instead of only the ones that don't have a thank you sent.

 

        if (in_array('unthanked', $tokens['donor']))
        {
            $dao = &CRM_Core_DAO::executeQuery("
        SELECT cc.contact_id, cc.total_amount, cc.receive_date, cc.check_number, con.display_name,
        hon.display_name as honoree, pi.label AS payment_instrument, ht.label AS honor_type
        FROM civicrm_contribution cc
        INNER JOIN civicrm_contact con ON con.id = cc.contact_id
        LEFT JOIN civicrm_contact hon ON hon.id = cc.honor_contact_id
        LEFT JOIN civicrm_option_value ac ON ca.account_18 = ac.value AND ac.option_group_id = 103
        LEFT JOIN civicrm_option_value pi ON cc.payment_instrument_id = pi.value AND pi.option_group_id =
        (SELECT id FROM civicrm_option_group WHERE name = 'payment_instrument')
        LEFT JOIN civicrm_option_value ht ON cc.honor_type_id = ht.value AND ht.option_group_id =
        (SELECT id FROM civicrm_option_group WHERE name = 'honor_type')
        WHERE cc.is_test = 0 AND cc.contribution_type_id = 1 OR cc.contribution_type_id = 2
        OR cc.contribution_type_id = 6 AND cc.contribution_status_id = 1
        AND cc.contact_id IN ($contacts_and_spouses) ORDER BY cc.receive_date");
            $header = '
        <table class="donations">
          <thead><tr>
            <th>Date</th>
            <th>Donor</th>
            <th>Amount</th>
            <th>Paid By</th>
            <th>Notes</th>
          </tr></thead>
          <tbody>';
            while ($dao->fetch())
            {
                $cid = $dao->contact_id;
                $row = '
          <tr>
            <td>' . date('m/d/Y', strtotime($dao->receive_date)) . '</td>
            <td>' . $dao->display_name . '</td>
            <td>$' . $dao->total_amount . '</td>
            <td>' . ($dao->payment_instrument ? $dao->payment_instrument :
                    'In Kind') . ($dao->check_number ? ' #' . $dao->check_number : '') . '</td>
            <td>' . ($dao->honoree ? "<br />{$dao->honor_type} {$dao->honoree}" :
                    '') . '</td>
          </tr>';
                if (in_array($cid, $cids))
                {
                    $values[$cid]['donor.unthanked'] = woolman_aval($values[$cid],
                        'donor.unthanked', $header) . $row;
                }
                if (isset($spouses[$cid]))
                {
                    $values[$spouses[$cid]]['donor.unthanked'] = woolman_aval($values[$spouses[$cid]],
                        'donor.unthanked', $header) . $row;
                }
            }
            foreach ($cids as $cid)
            {
                if (!empty($values[$cid]['donor.unthanked']))
                {
                    $values[$cid]['donor.unthanked'] .= '</tbody></table>';
                }
            }
        }

 

hat's a portion of the code. I just want to make sure I'm doing it correctly before I run it on my db.

Basically, I removed the bit in the SQL toward the end <code>AND cc.thankyou_date IS NULL</code>

I'm not sure what the line

LEFT JOIN civicrm_option_value ac ON ca.account_18 = ac.value AND ac.option_group_id = 103    is. Could you help me out on what it is doing?

Also, I'd like to get a token called donor.totalcontributions which would total all the contributions from the result.

Can you explaiin the woolman_aval function in your example?  Did I miss where it was defined?

BTW, thank you for a great explanation of the token functions.  It highly simplified a project that I was working on.

Anonymous (not verified)
2012-11-08 - 02:48

Can someone give me pointers on getting Coleman's sample code to run in Joomla. I tried it via teh plugin way as well teh civicrm.Hooks.php way. It looks like the code is totally ignored. Am I missing out a step in the implementation ? 

I, too, tried creating hooks in Joomla, but to no avail.  It doesn't look as though the hooks are getting loaded.  I am now running Joomla 3.5.9 and CiviCRM 4.7.3.  I plan to ask the question in StackExchange as that seems to be the right forum to use these days.

Anonymous (not verified)
2012-12-18 - 13:53

Thanks for the great tutorial and examples.  I need to add some tokens for events. I'm confused by the function.  It seems to use contact IDs ($cids) but it seems to me that I need to use event IDs.   Can you provide some basic tips about setting up Event tokens?

Thanks.

 

I have a similar request for membership tokens. In face, why aren't all available custom fields automatically available as tokens ?

Anonymous (not verified)
2013-02-23 - 02:11

Please note the code as used in the example given will not work. The in_array() test fails (at least in my code) since the values being searched for are array keys and not array values and in_array() only checks values.

The array in the example would have something like: $tokens['donor']['unthanked'] = 1 when you dump the tokens array.

This may account for others who had issues with nothing happening when a mailing was sent.

@ehlondon sorry you're having trouble getting the code to work, but I haven't had the problem you describe. That section of the $tokens array looks like $tokens['donor'] = array('unthanked', 'set_thank_you') - perhaps you're thinking of another variable?

Thanks (again!) Coleman, this was very helpful. I just modified your snippet to make the Drupal username available to CiviMail. I added it (and a link to this blog post) as a new page in the wiki.

Anonymous (not verified)
2014-01-27 - 10:54

This really is awesome stuff. Not sure how we can lobby to have this built-in, but I really think it should be.

In any case, I do believe that your example is outdated. I'm not sure when things changed, but your SQL queries will not work with the latest versions of CiviCRM. I'm using 4.4.3 on Drupal 7, and the contribution_type_id column in the civicrm_contribution table no longer exists. It appears to me to have been replaced with a column named financial_type_id.

When I changed that, the query began to work for me.

Cheers!

You're right, in 4.3 that column name changed. I'll update the example.

Note however that CiviCRM now has a native way to print thank-you letters for donations so this token is no longer needed unless you need heavy customization or the aggrigation feature is useful for you (natively Civi would print one letter per donation, not one letter per donor).

Anonymous (not verified)
2014-02-01 - 11:00

I do use the aggregation feature to print out year-end giving reports to our donors, and I think that's very typical for many organization. I have seen some non-profits do both: they send individual receipts near the date of the donation, and then they follow it up with an aggregated, year-end report.

I think it's a nice touch, and I'd love to see it supported natively.

Again, thanks for this great code. You sure saved my bacon this year! I used your code to create a custom module, and I created a Drupal sandbox in case anyone else might find it useful.

Anonymous (not verified)
2014-04-29 - 14:49

Hello,

I am trying to implement the custom token for "username" as decribed in the blog and in the link by Submitted by laryn on January 16, 2014 - 13:59.

I successfully created a Drupal Module and copy/pasted the code replacing the MYMODULE name with the name of the module I created.

I am wondering if I am missing any steps, but I cannot see the custom token "username" inside the "select a token" popup in either civimail or the create PDF letter areas.

 

  1. Am I supposed to see custom tokens in this popup?
  2. If not, how can I test if the token works? (I created an email and manually added a {contact.username} token in it and sent it to myself, but it did not work, it simply sent out "{contact.username}" rather than replacing it with the correct username.

Thanks for your help to a new programmer :)

Some things you should check:

  • Is your module loading at all? Put a print statement at the top of the module file to check.
  • Is your hook being called at all? Put a "die" statement in your hook code to check.
  • Does your token format match other tokens? Use print_r or dpm to see the exisitng params passed to your hook.

@fernieggg I apologize I didn't see this sooner. You should be able to see the token in the list, but the code produces it as {username.drupal} rather than {contact.username} -- you can adjust that in the code if you wish. I did find a typo in the wiki code I had pasted, so double check the names of the functions and you should be all set. I just duplicated this on another site to test.

Anonymous (not verified)
2015-08-17 - 06:40

Hey there,

thanks a lot for this nice article!

I'm struggling with custom tokens for use in greetings. I wrote aboud my problems in civi's stackexchange

http://civicrm.stackexchange.com/questions/4002/tokens-in-greetings-not-replaced

but there's just no answer for the last month.

Could you have a look?

Thankd in advance // nielo

How would one make a token that summarized the participant price set items for an event registration, for use in an email communication?