4

EDIT: this is NOT working, at least in CiviCRM 5.24.2
EDIT: this is done automatically by CiviCRM from 5.18+

I have a custom fieldset on Contributions that uses an option list. The option list has a default value set, but when contributions are created by non-UI processes (e.g. API) they do not have this default value; the field is empty/null/not there.

The fieldset is created by an extension. How could I ensure the default is set for all contributions?

Seems the options might be:

  • Use hook_civicrm_post then use the API to make a call.
  • Or could hook_civicrm_pre be used for this? Can you set custom fields in this part of the process?
  • or a horrible way would be to write an API job to somehow create default entries and run this by cron. Perhaps this might be necessary to set a default for existing contributions though?

Anyone done this? If so, how d'ya do it?

  • think you should update your answer too - perhaps insert at top of it – petednz - fuzion Nov 7 '19 at 21:56
  • @petednz-fuzion erm, I did already! – artfulrobot Nov 8 '19 at 9:48
  • odd. i must be tired. I see "EDIT: this is done automatically by CiviCRM from 5.18+" in your question but don't see equivalent in your answer – petednz - fuzion Nov 9 '19 at 0:42
4

Used to work, before CiviCRM v5.18

Here's how I've opted to do it. Please do submit alternative answers if there's a better way.

Use the civicrm_post hook like this:

Note that you need to replace N with your custom field's ID number

function myext_civicrm_post($op, $objectName, $objectId, &$objectRef) {

  if ($objectId && $op == 'create' && $objectName == 'Contribution') {

    // See if we have a field that belongs to the fieldset.
    $result = civicrm_api3('CustomValue', 'get', [
      'sequential' => 1,
      'entity_id' => $objectId,
      'entity_type' => "Contribution",
      'return.custom_N' => 1,
    ]);

    if ($result['count'] == 0) {
      // Create the default entry.
      $result = civicrm_api3('CustomValue', 'create', [
        'sequential' => 1,
        'entity_id' => $contribution_id,
        'entity_type' => "Contribution",
        'custom_N' => 'unknown',
        // custom_M ...
      ]);
    }

  }
}

Then when you create a Contribution, the defaults are properly set.

Interesting observations

My interest was in creating the default record when the contribution was created by the API. But I thought I'd look into how this works with using the UI too.

If you use the web UI to create a contribution then the code above gets in before the main process creates/sets the custom values. i.e. This is the order of things:

  1. User creates a contribution record.
  2. Contribution record created.
  3. Our hook runs, no custom value exists so it creates the default record.
  4. The user-entered values (inc. the defaults that were set up in the form) overwrite the default created by our hook.

The same process happens when you use the API to create a contribution but include values for the custom data also.

Why this no longer works

The above worked fine until sometime around CiviCRM version 5.18 at which point it stopped working. It stopped because during creation of a contribution this code would run, creating a custom data record, then the calling code would also try to create the same custom data record - causing a database error.

Works, if you don't need to use phpunit (so, doesn't really work!)

I found that I could acheive what I had been trying to do as follows:

function myext_civicrm_post($op, $objectName, $objectId, &$objectRef) {

  if ($objectId && $op == 'create' && $objectName == 'Contribution') {
     CRM_Core_Transaction::addCallback(
       CRM_Core_Transaction::PHASE_POST_COMMIT,
       'myfixercallback', [$objectId]);
  }
}

Then myfixercallback would do the same work. However, if you are using this then beware phpunit tests that use transaction interface: your code won't run until the test ends - which is too late!

However! it seems that CiviCRM - at least from 5.18, possibly earlier, now properly creates default custom data. So this hack is simply not needed any more. Sorry, certainly 5.24.2 does not do this.

API Wrapper solution (tested with 5.24.2)

You can implement an API wrapper, but you need to implement it for both API3 and API4.


function giftaid_civicrm_apiWrappers(&$wrappers, $apiRequest) {
  if ($apiRequest['entity'] === 'Contribution' && $apiRequest['action'] === 'create') {
    $wrappers[] = new CRM_MyApiWrapperContributionCreate();
  }
}

// ... Implement the class ...

class CRM_MyApiWrapperContributionCreate implements API_Wrapper {

  public function fromApiInput($apiRequest) {

    if ($apiRequest instanceof Civi\Api4\Generic\AbstractAction) {
      // APIv4
      $params = $apiRequest->getParams();
      if (empty($params['id']) && noneOfTheCustomFieldsAreDefinedApi4($apiRequest)) {
        $apiRequest->addValue('my_fieldset_name.my_field_name', '');
      }
    }
    if (empty($apiRequest['params']['id']) && noneOfTheCustomFieldsAreDefinedApi3($apiRequest)) {
        $apiRequest['params']['custom_1234'] = '';
      }
    }
    return $apiRequest;
  }

  public function toApiOutput($apiRequest, $result) {
    return $result;
  }
}

In this example

  • my_fieldset_name is the custom fieldset, in which my_field_name is a field that has '' as a default value, and an internal ID of 1234 which explains where custom_1234 comes from.

  • Rather than relying on setting a value to '' you could look up the default value for a field, but if you're doing this for a specific solution you can do it this way to be more efficient.

  • noneOfTheCustomFieldsAreDefinedApi3 needs to scan the params for fields named custom_N where N is the known ID value of each of your fields. And noneOfTheCustomFieldsAreDefinedApi4 needs to scan the params for keys beginning with my_fieldset_name.

| improve this answer | |
1

Based on the answer of @artfulrobot (thanks!), i have made a version of the code that use the definition of the custom field itself. If we have a default value, then force the creation of the field, otherwise, just ignore. Of course, it's probably a bit slower because we do some extra api calls, but it means we could change the defaults in the UI without changing the code.


function myext_civicrm_post($op, $objectName, $objectId, &$objectRef) {

  if ($objectName == 'Contribution') {

    $result = civicrm_api3('CustomField', 'get', [
      'sequential' => 1,
      'return' => ["default_value", "custom_group_id", "name"],
      'default_value' => ['IS NOT NULL' => 1],
      // use your own custom group id
      'custom_group_id.id' => 1,
    ]); 
    if ($result['count']) {
      // use the first one (any will do because we know that we are in one custom group)
      $check_field = 'custom_' . $result['values'][0]['id'];

      // See if we have a field that belongs to the fieldset.
      $existResult = civicrm_api3('CustomValue', 'get', [
        'sequential' => 1,
        'entity_id' => $objectId,
        'entity_type' => "Contribution",
        'return.' . $check_field => 1,
      ]); 

      if ($existResult['count'] == 0) {
        // insert a new entry with defaults values
        $p = [ 
          'sequential' => 1,
          'entity_id' => $objectId,
          'entity_type' => 'Contribution'
        ];  
        foreach ($result['values'] as $row) {
          $id = $row['id'];
          $p['custom_' . $id] = $row['default_value'];
        }   

        // Create the default entry.                                                                                                        
        $result = civicrm_api3('CustomValue', 'create', $p);
      }   
    }

  }

}

I guess with a bit of extra effort, one could make a generic extension that works for any custom fields that attach to any entity type.

| improve this answer | |
  • Nb. I think CiviCRM does this stuff now. – artfulrobot Nov 6 '19 at 14:00
  • Ok, good to know thanks! Do you have any reference of the issue / pull request ? – samuelsov Nov 15 '19 at 15:58
  • No, I just noticed it at 5.18. – artfulrobot Nov 15 '19 at 17:35
0

I've handled this using hook_civicrm_custom. This fires after the db write, which isn't ideal, but has a much simpler set of params than hook_civicrm_pre.

| improve this answer | |
  • Does this call when Contributions are created? My db is not showing any record being written to the custom data table, which I think would imply that this hook would not fire (because the docs say "This hook is called AFTER the db write on a custom table" and no write has happened)? – artfulrobot Jan 5 '17 at 17:49
  • Ah - I guess in my instance, I had other custom fields that were being written. I don't think my solution will apply to your use case. – Jon G - Megaphone Tech Jan 5 '17 at 17:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.