Skip to main content

GROWING AND SUSTAINING RELATIONSHIPS

GROWING AND SUSTAINING RELATIONSHIPS
Close
Adam Wight

Developer

Wikimedia Foundation

http://wikimediafoundation.org/

Civi is one of those pieces of software that makes you wonder how early humans could have survived without it. Every nonprofit seems to be using Civi for some aspect of their fundraising, and I'm always surprised at the creative ways different people find to make it work for their needs. Happy to be able to help out a bit. There's a lot of energy going into this project--definitely checkout the forums and the IRC channel if you're curious.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Jose Torres

Administrator, Implementor

Professional Exchange Service Corporation

http://pesc.com

PESC uses CiviCRM as pillar in maintaining many nonprofits throughout California and Nevada.

GROWING AND SUSTAINING RELATIONSHIPS
Close
David Barratt

Developer

Donor Depot

http://www.donordepot.com

They provide us a way to manage Non-Profit Donors

GROWING AND SUSTAINING RELATIONSHIPS
Close
Franck Sinimalé

Integrator

l'AtelierWeb.Org

http://www.atelierweb.org

I chose to learn to use CiviCRM to learn how to help NPOs :) And because it seems to be a meeting point and a continuity of my values, my skills, and what I think we should develop for the next step of our humanity.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Graham Mitchell

Implementor, Administrator, end-user, Trainer

MC3

http://mc3.coop

I've been working with CiviCRM since 2006 or thereabouts. The community is outstanding in providing support and sharing expertise, which combines with a strong product to enable me in turn to deliver better results for the organisations that I work with. I only hope that over time I will be able to repay the debt by supporting other newcomers to CiviCRM.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Joshua Lange

Administrator

Concordia Welfare & Education Foundation

http://cwef.org.hk

CWEF is deploying CiviCRM on Wordpress to build and manage a database of our staff, volunteer, recipient, donor and partner contacts.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Leena Nangia

Consultant

nfpservices

http://www.nfpservices.co.uk/

We use CiviCRM for our own business functions. Nfpservices participate in the development of CiviCRM and contribute enhanced functionality to the community.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Xavier Dutoit

Developer and Implementor

Tech to the People

http://techtothepeople.com

Over the past 15 years I've been involved in several open source communities.
CiviCRM is without any doubt the one that has the strongest focus in welcoming "newbies" and letting everyone feel at home here. Another impressive feature is the focus on shipping. No matter what you think of CiviCRM today, you are almost sure that there will be a newer and better version in a few months.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Samuel Vanhove

Developer, Implementor

Réseau Koumbit

http://koumbit.org

As non-profit consultants working for non-profit organizations, we found CiviCRM to be particularly well suited to answer the common needs of activist associations, charities and other medium-sized groups. Based in Montréal, we've helped local and international organizations migrate to CiviCRM to manage their memberships, events, communications and fundraising campaigns. We empower our clients and assist them when they need us.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Michael Daryabeygi

Implementor

Ginkgo Street Labs

http://ginkgostreet.com

CiviCRM enables me to empower my clients with a database that suits their unique needs.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Peter Petrik

Implementor, Developer

Skvare, LLC

http://skvare.com

Helping non-profits, membership organizations, and professional associations to make the most out of their resources with open-source tools.

GROWING AND SUSTAINING RELATIONSHIPS
Close
Peter McAndrew

Implementor, Developer

Third Sector Design

http://www.thirdsectordesign.org

Being part of the CiviCRM community is really something to shout about! Not only is CiviCRM an amazing software package, its designed for organisations that make a difference in the world. We help non-profits across the UK gain control of their data through the power of CiviCRM.

It is without a doubt the best piece of software I've ever worked with, and I'm constantly discovering cool new features. More recently I've been working on CiviMobile as part of a project for my course at University. I'm really looking forward to seeing this being used by organisations across the globe.

LOGIN | REGISTER
  • Create new account
  • Request new password

Search form

  • BLOG
  • DEMO
  • Find An Expert
  • NEED HELP
  • SUPPORT US
  • DEVELOPER RESOURCES
CiviCRM Community Site logo CiviCRM Community Site
  • WHAT IS CIVICRM
    • Community
    • Case Studies
    • Experts
    • Contributors
    • Core Team
    • Licensing
    • Contact Us
  • WILL CIVICRM MEET YOUR NEEDS?
    • Contacts
    • Contributions
    • Communications
    • Peer-To-Peer Fundraisers
    • Advocacy Campaigns
    • Events
    • Members
    • Reports
    • Case Management
  • GET STARTED
    • Evaluate Your CRM Needs
    • Evaluate CiviCRM Features
    • Read Books
    • Contact an Ambassador
    • Demo CiviCRM
    • Download CiviCRM
    • Download Extensions
    • Find An Expert
  • PARTICIPATE
    • Join the community
    • Make it happen
    • Support CiviCRM
    • Meet ups
    • Document CiviCRM
    • Translate CiviCRM
    • Developer resources

You are here

Home » Blogs » colemanw's blog

Blog

  • API
  • Architecture Series
  • CiviCampaign
  • CiviCase
  • CiviCon
  • CiviContribute
  • CiviCRM
  • CiviCRM v4.1
  • CiviEvent
  • CiviMail
  • CiviMember
  • CiviMobile
  • CiviPledge
  • CiviReport
  • Documentation
  • Drupal
  • Extensions
  • Finance and Accounting
  • Interface Design and Layout Standards
  • Internationalization and Localization
  • Joomla
  • Make it happen
  • Marketing and Promotion
  • Meetups
  • Older Versions
  • Release
  • Schools
  • Solutions (case studies and user stories)
  • Sprints
  • Teams
  • Training
  • v1.6
  • v1.7
  • v1.8
  • v1.9
  • v2.0
  • v2.1
  • v2.2
  • v2.3
  • v3.0
  • v3.1
  • v3.2
  • v3.3
  • v3.4 and v4.0
  • v4.2
  • v4.3
  • WordPress

Create Your Own Tokens for Fun and Profit

Submitted by colemanw on January 16, 2012 - 12:14

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 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'] = (!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 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!

  • colemanw's blog
  • Log in or register to post comments

Comments

Thanks for sharing

Permalink Submitted by xavier on January 16, 2012 - 17:03

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?

  • Log in or register to post comments

$cids is always an array

Permalink Submitted by colemanw on January 16, 2012 - 17:09

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

  • Log in or register to post comments

Where goes it?

Permalink Submitted by Ken Williams (not verified) on January 25, 2012 - 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?

  • Log in or register to post comments

Adding custom code

Permalink Submitted by colemanw on February 2, 2012 - 11:13

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.

  • Log in or register to post comments

Colemanw,

Permalink Submitted by FrTommy on April 29, 2012 - 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.

  • Log in or register to post comments

woolman_aval

Permalink Submitted by LoganBear on September 26, 2012 - 07:33

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.

  • Log in or register to post comments

Hooks in Joomla

Permalink Submitted by philip on November 8, 2012 - 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 ? 

  • Log in or register to post comments

How to add event tokens?

Permalink Submitted by kungcivi on December 18, 2012 - 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.

 

  • Log in or register to post comments

Error in Code Example

Permalink Submitted by ehlondon on February 23, 2013 - 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.

  • Log in or register to post comments

@ehlondon sorry you're having

Permalink Submitted by colemanw on February 27, 2013 - 16:28

@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?

  • Log in or register to post comments

CIVICRM


GROWING AND SUSTAINING RELATIONSHIPS

WHAT IS CIVICRM
  • Community
  • Case Studies
  • Experts
  • Contributors
  • Core Team
  • Licensing
  • Contact Us
WILL CIVICRM MEET YOUR NEEDS?
  • Contacts
  • Contributions
  • Communications
  • Peer-To-Peer Fundraisers
  • Advocacy Campaigns
  • Events
  • Members
  • Reports
  • Case Management
GET STARTED
  • Evaluate Your CRM Needs
  • Evaluate CiviCRM Features
  • Read Books
  • Documentation
  • Demo CiviCRM
  • Download CiviCRM
  • Download Extensions
  • Find An Expert
PARTICIPATE
  • Join the CiviCRM Community
  • Read Our Blog
  • Community Forum
  • Attend a Training or Meetup
  • Make It Happen
  • Become A CiviCRM Developer
  • Issue Tracker
  • Help with Documentation
  • Translate