Skip to content

Create a custom Case token

This step-by-step guide is out of date - use are your own risk - the MOST out-of-date parts have been lightly edited but this is generally not the right place to read.

It was used to show how you can programmatically create your own tokens for CiviCase based on code that was deprecated in early to mid 2021.

We will create a token which displays the name of the case coordinator and the role the person has on the case.

But we will start with a bit of theory to explain how token works.

Theory

CiviCRM provides the hook_civicrm_tokenValues for replacing tokens with a personalized value. This hook is deprecated and you should not implment it.

When a message in CiviCRM is personalized the engine around tokens only knows about the Contact, not whether the message is used for a Contribution, a Case, an Event registration etc... So this means for us as developers the only information we have is the Contact ID, and an array with Values.

Nevertheless, there are some workarounds to get to know more about the context. But those workarounds depends on when the message is generated

Scheduled reminders

Scheduled reminders use the Token Processor for replacing token contents. The core code supplies Case Tokens. However, it is possible to add additional tokens. The key event to implment is the Symfony event civi.token.eval.

In the event civi.token.eval we get the \Civi\Token\Event\TokenValueEvent object which holds all the rows and with each row we can replace the tokens.

$row->context['contactId'] (for example) can be used to get the contact ID.

Search results

When doing a Print/Merge Document after a case search the Case ID should be in the $row->context['caseId']

When doing a Print/Merge Document after an activity search the tokens are replaced with the Token Processor and the activity id is stored in the context of the row in activityId.

When doing a Send E-mail after an activity search we need a hack to store the related activity ids.

CiviRules

When a CiviRule executes the data from the trigger is stored in $values['extra_data'].

The actions: Send PDF and Send E-mail uses the following to pass data about the context of civirules:

  • Participant: $values['extra_data']['participant']['id']
  • Event: $values['extra_data']['event']['id']
  • Case: $values['case_id'] or $values['extra_data']['case']['id']. The $values['case_id'] is used when the e-mail or pdf is filed on the case.
  • Contribution: $values['extra_data]['contribution]['id']
  • Activity: $values['extra_data']['activity']['id']

Action Provider (Form Processor)

The actions: Create PDF and Send E-mail uses the following to pass data about the context of the action:

  • Participant: $values['extra_data']['participant']['id']
  • Event: no information is passed
  • Case: $values['case_id']
  • Contribution: $values['contribution_id']
  • Activity: $values['activity_id']

Our approach

To get our token working in every context we are going to make use of the following hooks/symfony events:

Which hooks to use to inform CiviCRM about our token?

Which hooks to use for replacing token contents?

  • DO NOT USE ANYMORE - hook_civicrm_tokenValues: replace our token for when used from Search, CiviRules or Action Provider.
  • civi.token.eval Symfony event: to replace our token when used in the scheduled reminder settings. This will only work for activities and when the scheduled reminder is executed on an activity on a case.

Which hooks to use for search context hack?

  • hook_civicrm_buildForm/: Only needed when the form is a CRM_Activity_Form_Task_Email and we store the activity IDs in \Civi::$statics['mycasetokens']['activity_ids']

How do we know about the case involved?

With hook_civicrm_tokens we check the following:

  • $values['case_id']
  • $values['activity_id'] and then lookup the case ID of this activity
  • $values['extra_data']['activity']['id'] and then lookup the case ID of this activity
  • \Civi::$statics['mycasetokens']['activity_ids'] and then lookup the case ID of this activity. As you can see multiple activities could be selected.

Create an extension

Run the command civix generate:module mycasetokens to generate a new extension. You may enable the extension right away.

Change info.xml to your liking.

Get case manager

Implement a function to return the name of the manager role on the case and the name of the contact that has this role.

/**
 * Returns the case manager role name and the display name of the contact who is the manager.
 *
 * @param $case_id
 *
 * @return string
 * @throws \CiviCRM_API3_Exception
 */
function mycasetokens_retrieve_case_manager($case_id) {
  $case = civicrm_api3('Case', 'getsingle', ['id' => $case_id]);
  $caseType = civicrm_api3('CaseType', 'getsingle', ['id' => $case['case_type_id']]);
  $xmlProcessor = new CRM_Case_XMLProcessor_Process();
  $caseRoles = $xmlProcessor->get($caseType['name'], 'CaseRoles');
  list($managerRoleId, $dir) = explode("_", $xmlProcessor->getCaseManagerRoleId($caseType['name']), 2);
  $sql = "
    SELECT `display_name`
    FROM `civicrm_contact`
    INNER JOIN `civicrm_relationship` ON `civicrm_relationship`.`contact_id_b` = `civicrm_contact`.`id`
    WHERE `civicrm_relationship`.`case_id` = %1 AND `civicrm_relationship`.`relationship_type_id` = %2
    AND `is_active` = '1'";
  $sqlParams[1] = [$case_id, 'Integer'];
  $sqlParams[2] = [$managerRoleId, 'Integer'];
  $display_name = CRM_Core_DAO::singleValueQuery($sql, $sqlParams);

  return $caseRoles[$managerRoleId.'_'.$dir].': '.$display_name;
}

The function above loads the case and the case type. It then looks up the case role for the manager and who has that role.

Retrieve the case id

Implement a function to return the corresponding case id. This is a helper function which can be used in DO NOT USE ANYMORE - hook_civicrm_tokenValues.

/**
 * Return the case_id to be used for replacing the token content.
 *
 * @param $values
 * @param $contacts
 * @param $contact_id
 *
 * @return int|null
 */
function mycasetokens_retrieve_case_id($values, $contacts, $contact_id) {
  if (isset($contacts['case_id'])) {
    return  $contacts['case_id'];
  } elseif (isset($values['activity_id']) && $values['activity_id']) {
    return mycasetokens_get_case_id_by_activity_id($values['activity_id']);
  } elseif (isset($values['extra_data']['activity']['id']) && $values['extra_data']['activity']['id']) {
    return mycasetokens_get_case_id_by_activity_id($values['extra_data']['activity']['id']);
  } elseif (isset(\Civi::$statics['mycasetokens']['activity_ids'])) {
    return mycasetokens_get_case_id_by_contact_id_and_activity_ids($contact_id, \Civi::$statics['mycasetokens']['activity_ids']);
  }
  return null;
}

/**
 * Returns the Case ID retrieved by the activity ID.
 *
 * @param $activity_id
 *
 * @return int|null
 */
function mycasetokens_get_case_id_by_activity_id($activity_id) {
  $case_activity = \Civi\Api4\CaseActivity::get()
    ->addWhere('activity_id', '=', $activity_id)
    ->setLimit(1)->execute()->first();
  return $case_activity['case_id'] ?? NULL;
}

/**
 * Returns the Case ID retrieved by a contact_id and mulitple activity ids.
 * We use this function as we dont know which activity is linked to which contact so
 * we try to retrieve this link again.
 *
 * @param $activity_id
 *
 * @return int|null
 */
function mycasetokens_get_case_id_by_contact_id_and_activity_ids($contact_id, $activity_ids) {
  $case_activity = \Civi\Api4\Activity::get()
    ->addSelect('case.id')
    ->setJoin([
      ['ActivityContact AS activity_contact', 'INNER'],
      ['CaseActivity AS case_activity', 'INNER'],
      ['Case AS case', 'INNER'],
    ])
    ->addWhere('id', 'IN', $activity_ids)
    ->addWhere('activity_contact.contact_id', '=', $contact_id)
    ->addWhere('case.is_deleted', '=', FALSE)
    ->addOrderBy('case.id', 'ASC')
    ->setLimit(1)
    ->execute()
    ->first();
  return $case_activity['case.id'] ?? NULL;
}

The function for retrieving the Case ID is called mycasetokens_retrieve_case_id and we need this function as the case id could be stored in different ways and this function checks all those different ways. See the Theory.

Implement the hack for Search results.

Add the following to mycasetokens.php to make the hack for search results work:

/**
 * Implements hook_civicrm_buildForm().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_buildForm/
 */
function mycasetokens_civicrm_buildForm($formName, &$form) {
  if ($form instanceof CRM_Activity_Form_Task_Email) {
    \Civi::$statics['mycasetokens']['activity_ids'] = $form->getVar('_activityHolderIds');
  }
}

Implement hook_civicrm_tokens

DO NOT USE ANYMORE - Implement hook_civicrm_tokens so that CiviCRM knows about our token.

Add the following to mycasetokens.php:

/**
 * Implements hook_civicrm_tokens().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_tokens/
 */
function mycasetokens_civicrm_tokens(&$tokens) {
  $tokens['mycasetokens']['mycasetokens.case_manager'] = E::ts('Case Manager') . ' :: ' . E::ts('My Case Tokens');
}

The function above tells CiviCRM about the token {mycasetokens.case_manager}. In the drop down with available tokens our token will show up under My Case Tokens.

Implement hook_civicrm_tokenValues

Implement hook_civicrm_tokenValues in which we replace the {mycasetokens.case_manager} token with the actual contents.

Add the following to mycasetokens.php:

/**
 * Implements hook_civicrm_tokenValues().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_tokenValues/
 */
function mycasetokens_civicrm_tokenValues(&$values, $contactIDs, $jobID=null, $tokens=array(), $className=null) {
  if (mycasetokens_is_token_present($tokens)) {
    // Normalize $contactIds.
    // $contactIds is either an array with contact ids
    // or it is formatted according $contactIds['contact_id'] = 123, $contactIds['case_id'] = 67
    // if this is the case we wrap the array so it is easier to process.
    if (is_array($contactIDs) && isset($contactIDs['contact_id'])) {
      $contactIDs = [$contactIDs]; // Wrap the $contactIds into an array.
    }
    foreach ($contactIDs as $index => $contact) {
      $contact_id = $contact;
      if (is_array($contact_id) && isset($contact_id['contact_id'])) {
        $contact_id = $contact_id['contact_id'];
      }
      $case_id = mycasetokens_retrieve_case_id($values, $contact, $contact_id);
      if ($case_id) {
        $values[$contact_id]['mycasetokens.case_manager'] = mycasetokens_retrieve_case_manager($case_id);
      }
    }
  }
}

/**
 * Returns true when the {mycasetokens.case_manager} is set and present in the set and
 * needs to be replaced.
 *
 * @param $tokens
 * @return bool
 */
function mycasetokens_is_token_present($tokens) {
  if (in_array('case_manager', $tokens)) {
    return TRUE;
  } elseif (isset($tokens['case_manager'])) {
    return TRUE;
  } elseif (isset($tokens['mycasetokens']) && in_array('case_manager', $tokens['mycasetokens'])) {
    return TRUE;
  } elseif (isset($tokens['mycasetokens']['case_manager'])) {
    return TRUE;
  }
  return FALSE;
}

The code above contains a helper function mycasetokens_is_token_present which is to check whether the token {mycasetokens.case_manager} exists and in the text and needs to be replaced.

Implement Symfony Event 'civi.token.eval'

The last bit we need to implement is the Symfony event civi.token.eval this event is used in a scheduled reminder context and in the actions Print/Merge Document after an activity search.

Add the following code to mycasetokens.php:

/**
 * Implements hook_civicrm_container().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_container/
 */
function mycasetokens_civicrm_container($container) {
  $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
  $container->findDefinition('dispatcher')->addMethodCall('addListener',
    array('civi.token.eval', 'mycasetokens_evaluate_tokens')
  );
}

/**
 * Symfony event for civi.token.eval
 *
 * @param \Civi\Token\Event\TokenValueEvent $event
 */
function mycasetokens_evaluate_tokens(Civi\Token\Event\TokenValueEvent $event) {
  foreach ($event->getRows() as $row) {
    $case_id = null;
    if (isset($row->context['actionSearchResult']) && ($row->context['actionSearchResult']->activity_id)) {
      $case_id = mycasetokens_get_case_id_by_activity_id($row->context['actionSearchResult']->activity_id);
    } elseif (isset($row->context['actionSearchResult']) && ('civicrm_activity' == $row->context['actionSearchResult']->entity_table && $row->context['actionSearchResult']->entity_id)) {
      $case_id = mycasetokens_get_case_id_by_activity_id($row->context['actionSearchResult']->entity_id);
    } elseif (isset($row->context['activityId']) && $row->context['activityId']) {
      $case_id = mycasetokens_get_case_id_by_activity_id($row->context['activityId']);
    }
    if ($case_id) {
      $row->tokens('mycasetokens', 'case_manager', mycasetokens_retrieve_case_manager($case_id));
    }
  }
}
In the code above we use the hook_civicrm_container to add an Symfony event listener to the civi.token.eval and we declare it to be the function mycasetokens_evaluate_tokens.

See also