How to set a default value for a custom field
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, &amp;$objectRef) {

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

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

    if ($result['count'] == 0) {
      // Create the default entry.
      $result = civicrm_api3('CustomValue', 'create', [
        'sequential' =&gt; 1,
        'entity_id' =&gt; $contribution_id,
        'entity_type' =&gt; "Contribution",
        'custom_N' =&gt; '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:


User creates a contribution record.
Contribution record created.
Our hook runs, no custom value exists so it creates the default record.
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, &amp;$objectRef) {

  if ($objectId &amp;&amp; $op == 'create' &amp;&amp; $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(&amp;$wrappers, $apiRequest) {
  if ($apiRequest['entity'] === 'Contribution' &amp;&amp; $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-&gt;getParams();
      if (empty($params['id']) &amp;&amp; noneOfTheCustomFieldsAreDefinedApi4($apiRequest)) {
        $apiRequest-&gt;addValue('my_fieldset_name.my_field_name', '');
      }
    }
    if (empty($apiRequest['params']['id']) &amp;&amp; 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.