courtly
courtly
courtly
courtly

Upcoming Events

London user and administrator training
February 23rd, 2012
A comprehensive two day hands on training course covering the configuration, (more...)

CiviCRM Seminar - London
February 23rd, 2012
NfP Services free seminar

NYC CiviCRM MeetUp Thursday February 23, 2012
February 23rd, 2012
Reminder NYC CiviCRM meetup 5:30-7:30 Thursday, February 23.

CiviCRM London sprint Feb 2012
February 27th, 2012
Following the CiviCRM training here in London, we will have a CiviCRM code (more...)

Austin CiviCRM Meetup March 8, 2012 6:30pm
March 8th, 2012

Philadelphia - CiviCRM Meetup for Q1 2012
March 13th, 2012

UK South West - CiviCRM Meetup
March 20th, 2012
Come meet others from the Area who are interested in, using or developing for (more...)

[Bristol, UK] user and administrator training
March 21st, 2012
A comprehensive hands on training course covering the configuration, (more...)

San Francisco user and administrator training
March 29th, 2012
A comprehensive two day hands on training course covering the configuration, (more...)

CiviCRM Usability, Test and Code Sprint - San Francisco (March 2012)
March 29th, 2012
This usability, code and test sprint is targeted at CiviCRM users and (more...)

CiviCon 2012 San Francisco Bay Area - April 2nd 2012
April 2nd, 2012
CiviCon is THE annual event bringing together the people who use, develop, (more...)

CiviCRM Documentation, Test and Code Sprint - after CiviCon San Francisco (April 2012)
April 4th, 2012
This sprint is targeted at CiviCRM users and developers who want to work on (more...)

CiviCRM Components

Tools for engaging your supporters...

CiviContribute


CiviEvent


CiviMail


CiviMember


CiviReport


Create Your Own Tokens for Fun and Profit

Not Just a Contact Database

These optional components give you more power to connect and engage your supporters.

  • civiCASE

  • Case management for clients and constituents.

  • civiEVENT

  • Online event registration and participant tracking.

  • civiMEMBER

  • Online signup and membership management.

  • civiMAIL

  • Personalized email blasts and newsletters.

  • civiREPORT

  • Report generation and template management.

January 16, 2012 - 12:14 — colemanw

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) {
  // 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 contribution_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 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 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'] = 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>';
        }
      }
    }
    if (in_array('set_thank_you', $tokens['donor'])) {
      CRM_Core_DAO::executeQuery("
        UPDATE civicrm_contribution SET thankyou_date = NOW()
        WHERE is_test = 0 AND contribution_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!

( categories: )

Comments

Where goes it?

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?

Adding custom code

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.

Thanks for sharing

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?

$cids is always an array

As of 4.1. No more writing two versions of the same thing. Hooray!