Now that I'm posting here again, a little content.
I'm implementing CiviContribute for a client with some special reporting requirements. They are a political organization subject to US campaign finance laws, and the way they prepare their reports requires certain information to get uploaded to their payment processor (i.e.,. the people that run the servers that CiviContribute talks to when someone makes a donation or payment.).
It turns out that the best way for me to do this is to implement a new CiviCRM hook. This is not that hard to do, but it isn't obvious either. Here's how I did it.
This isn't the easiest thing to do, since the payment processors have very different protocols for communicating with their servers. In this case, I need to talk to Authorized.net's servers, which has two very different protocols we need to support, one for regular payments (AIM), and one for recurring payments (ARB). AIM is pretty simple, and sends key-value pairs (as do most protocols). ARB requires generating a block of XML, which can be tricky to do right if the input isn't properly encoded -- and IMNSHO, the payment code currently does this wrong. But more on that later.
Implementing the hook itself is pretty simple: we add it to CRM/Utils/Hook.php, which defines the various hooks and dispatches them to the appropriate platform specific code to actually call the module-level code (as it's called in Drupal).
I make use of CiviCRM's custom code directory settings for most of this, although there's a minor caveat here: CRM/Utils/Hook.php is loaded very early in CiviCRM's bootstrapping process, so you have to modify the file in the CiviCRM source itself. But payment processor code will load from the custom directories just fine, and since it's best to avoid putting files in the standard directories -- it's too easy to lose your changes during an upgrade -- anything that CiviCRM will load from the custom code directory we will store there.
So here's what we need to add to the Hook.php file in order to set up our hook:
<?php
/**
* Hook definition for altering payment parameters before talking to a payment processor back end.
*
* Definition will look like this:
*
* function hook_civicrm_paymentProcessor($processor_name, $mode,
* &$raw_params, &$cooked_params,
* $op='params');
*
* @param string $processor_name
* class name of the payment processor invoked (e.g., 'CRM_Core_Payment_Dummy')
* @param string $mode
* 'test' or 'live'
* @param array &$raw_params
* array of params as passed to to the processor
* @params array &$cooked_params
* params after the processor code has translated them into its own key/value pairs
* @param string $op
* what operation we are doing on the back end (for now, only 'params' -- parameter translation)
* @return void
*/
static function paymentProcessor($processor_name, $mode,
&$raw_params, &$cooked_params,
$op='params') {
$config =& CRM_Core_Config::singleton( );
require_once( str_replace( '_', DIRECTORY_SEPARATOR, $config->userHookClass ) . '.php' );
$null =& CRM_Core_DAO::$_nullObject;
return
eval( 'return ' .
$config->userHookClass .
'::invoke( 5, $processor_name, $mode, $raw_params, $cooked_params, $op, \'civicrm_paymentProcessor\' );' );
}
?>The creation of a $null object is not strictly necessary, but if you need fewer than 5 parameters (I needed them all), any parameters you do not intend to pass to your hook via invoke() needs to get passed that null object, or invoke() will be unhappy :-)
That's all you need to do to core CiviCRM to define the hook. To get the hook called at run time, the part of CiviCRM that needs to use your hook needs to set up the call. When CiviCRM calls hook_civicrm_buildForm, for example, the code of CRM_Core_Form::buildForm() (in file CRM/Core/Form.php, around line 315) does this:
<?php
// call the form hook
// also call the hook function so any modules can set thier own custom defaults
// the user can do both the form and set default values with this hook
CRM_Utils_Hook::buildForm( get_class( $this ),
$this );
?>My code needs to get called by the payment processor, so any payment processor that needs to do this kind of munging needs to implement my hook. Here's what an implementation for the dummy processor looks like (in file CRM/Core/Payment/Dummy.php), in function CRM_Core_Payment_Dummy::doDirectPayment():
<?php
//Invoke hook_civicrm_paymentProcessor
//In Dummy's case, there is no translation of parameters into
//the back-end's canonical set of parameters. But if a processor
//does this, it needs to invoke this hook after it has done translation,
//but before it actually starts talking to its proprietary back-end.
$cooked_params = $params; //no translation
CRM_Utils_Hook::paymentProcessor( get_class( $this ),
$this->_mode,
$params,
$cooked_params
);
//end of hook invokation
?>My module can get called to make use of this hook like this:
<?php
//Implementation of my custom hook for the processor
function my_custom_module_civicrm_paymentProcessor($processor_name, $mode,
&$raw_params, &$cooked_params) {
if ($processor_name == 'CRM_Core_Payment_Dummy') {
$employer = empty($raw_params['custom_1']) ?
'' : $raw_params['custom_1'];
$occupation = empty($raw_params['custom_2']) ?
'' : $raw_params['custom_2'];
$cooked_params['custom'] = "$employer|$occupation";
}
}
?>Most of this is boiler-plate code, and your implementation will likely be similar.