Skip to content

The Payment Class

A payment processor object extends CRM_Core_Payment. This class provides CiviCRM with a standard interface/API bridge to the third party processor. It should be found at:

<myextension>/CRM/Core/Payment/MyExtension.php

The most important method the payment processor class implements is doPayment. This function is responsible for receiving information from CiviCRM (such as the amount, billing details and payment details) that need to be passed to the payment gateway, formatting this information for the gateway and submitting and returning information about its success or otherwise to CiviCRM.

Important

Try to avoid infringing on CiviCRM's logic. The methods in your extension should take inputs, communicate with the third party, and return output data that CiviCRM can use to perform its logic. If you find your extension is sending emails, duplicating logic, updating or creating records in CiviCRM, outputting user content (e.g. status messages) then stop, check and consider separating out your code into different methods. Remember that you might be processing a webform or other non-core payment request so don't assume a user context (e.g. use CRM_Core_Error::statusBounce).

Note

Most methods should throw a Civi\Payment\Exception\PaymentProcessorException when they are unable to fulfill the expectations of a method.

Constructor

When the class is constructed it is passed an array which is the payment processor record in the database.

  /**
   * Constructor
   *
   * @param string $mode
   *   (deprecated) The mode of operation: live or test.
   * @param array $paymentProcessor
   */
  public function __construct($mode, $paymentProcessor) {
    $this->_paymentProcessor = $paymentProcessor;
  }

The checkConfig method

This is called when the user configures a processor through the UI and when loading a processor for payment. If they have not entered required fields an error message should be returned.

  /**
   * This function reports any configuration errors.
   *
   * @return string the error message if any
   */
  public function checkConfig() {
    if (empty($this->_paymentProcessor['user_name'])) {
      return E::ts('The "Bill To ID" is not set in Administer > CiviContribute > Payment Processor.');
    }
  }

doPayment function

The doPayment function receives information about the payment and is expected to return an array with the payment outcome, including any trxn_id from the payment processor and any fee_amount, if any. e.g.

Example code
/**
 * Make a payment by interacting with an external payment processor.
 *
 * @param array|PropertyBag $params
 *   This may be passed in as an array or a \Civi\Payment\PropertyBag
 *   It holds all the values that have been collected to make the payment (eg. amount, address, currency, email).
 * 
 * These values are documented at https://docs.civicrm.org/dev/en/latest/extensions/payment-processors/create/#available-parameters
 * h
 *   You can explicitly cast to PropertyBag and then work with that to get standardised keys and helpers to interact with the values passed in.
 *   See 
 *   Also https://docs.civicrm.org/dev/en/latest/extensions/payment-processors/create/#introducing-propertybag-objects explains how to interact with params as a property bag.
 *   Passed by reference to comply with the parent function but **should not be altered**.
 * @param string $component
 *   Component is either 'contribution' or 'event' and is primarily used to determine the url
 *   to return the browser to. (Membership purchases come through as 'contribution'.)
 *
 * @return array
 *   Result array:
 *   - MUST contain payment_status (Completed|Pending)
 *   - MUST contain payment_status_id
 *   - MAY contain trxn_id
 *   - MAY contain fee_amount
 *   See: https://lab.civicrm.org/dev/financial/-/issues/141
 *
 * @throws \Civi\Payment\Exception\PaymentProcessorException
 */
public function doPayment(array &$params, string $component = 'contribute') {
  // Optionally cast to a PropertyBag. 
  // The PropertyBag was intended to help developers access properties
  // without having to know their names and force a greated degree of consistency.
  // In order to do the latter it is also very strict so refer to the contract in 
  // this reference for which properties you can safely use the `get` functions for.
  // @see https://docs.civicrm.org/dev/en/latest/extensions/payment-processors/create/#available-parameters
  // Alternatively the `$params` have been standardised over time to those in the same
  // reference section.
  /* @var \Civi\Payment\PropertyBag $propertyBag */
  $propertyBag = \Civi\Payment\PropertyBag::cast($params);

  if ($propertyBag->getAmount() == 0) {
    // The function needs to cope with the possibility of it being zero
    // this is because historically it was thought some processors
    // might want to do something with $0 amounts. It is unclear if this is the
    // case but it is baked in now.
    $result['payment_status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
    $result['payment_status'] = 'Completed';
    return $result;
  }

  // Prepare whatever data the 3rd party processor requires to take a payment.
  // The contents of the array below are just examples of typical things that
  // might be used.
  $processorFormattedParams = [
    'authentication_key' => $this->getPaymentProcessor()['user_name'],
    'amount' => $propertyBag->getAmount(),
    // Either use `getter` or `has` or use `$params['contributionID']` for any
    // values that might be null as the PropertyBag is very strict
    'order_id' => $propertyBag->getter('contributionID', TRUE, ''),
    // getNotifyUrl helps you construct the url to tell an off-site
    // processor where to send payment notifications (IPNs/webhooks) to.
    // Not all 3rd party processors need this.
    'notifyUrl' => $this->getNotifyUrl(),
    // etc. depending on the features and requirements of the 3rd party API.
  ];
  if ($propertyBag->has('description')) {
    $processorFormattedParams['description'] = $propertyBag->getDescription();
  }

  // Allow further manipulation of the arguments via custom hooks
  CRM_Utils_Hook::alterPaymentProcessorParams($this, $propertyBag, $processorFormattedParams);

  // At this point you need to interact with the payment processor.
  $result = callThe3rdPartyAPI($processorFormattedParams);

  // Some processors require that you send the user off-site to complete the payment.
  // This can be done with CRM_Utils_System::redirect(), but note that in this case
  // the script execution ends before we have returned anything. Therefore the payment
  // processes must be picked up asynchronously (e.g. webhook/IPN or some other return
  // process). You may need to store data on the session in some cases to accommodate.

  // If you are interacting with the processor server side & get a result then
  // you should either throw an exception or return a result array, depending on
  // the outcome.
  if ($result['failed']) {
    throw new \Civi\Payment\Exception\PaymentProcessorException($failureMessage);
  }

  return [
    'payment_status'    => 'Completed',
    'payment_status_id' => CRM_Core_PseudoConstant::getKey(
                             'CRM_Contribute_BAO_Contribution',
                             'contribution_status_id',
                             'Completed'),
    'trxn_id'           => $result['payment_id'],
    'fee_amount'        => $result['fee'],
    // Optional result code - stored in civicrm_financial_trxn.
    'trxn_result_code` => 'abc'
  ];
}

doRefund function

This is largely similar to doPayment - if you implement this you need to declare that your site supportsRefund (see supports functions).

This function should return an array like return ['refund_status' => 'Completed'];. It can optionaly include trxn_id and trxn_result_code.

doPreApproval function

This is called when there is an opportunity for the user to pre-approve their credentials early in the process (and you declare supportsPreApproval) - they might be redirected to a different site to enter their card or perhaps do it onsite in a js intercept.

If this doPreApproval returns an array like

  return [
    'pre_approval_parameters' => ['token' => $params['token']],
  ];

That array will later be passed to the function getPreApprovalDetails for parsing and that parsed value will reach doPayment. For example getPreApprovalDetails could return ['token' => 'x'] and 'token' will be in the $params that gets passed to doPayment for finalisation (sometimes called 'Capture').

doCancelRecurring function

This functions cancels a recurring contribution series. In some cases it will be desirable to notify the processor for example, when the processor is responsible for initiating new payments in the series. In others it might be fine to simply return TRUE (e.g. if the schedule is maintained via the civicrm_contribution_recur table and CiviCRM initiates each payment in the series by calling the payment processor with a card on file token.) From CiviCRM 5.27 onwards this function is supported and accepts a PropertyBag rather than an array. See the PropertyBag section for more.

The payment class should implement the following methods for this to be exposed - (see supports section below):

  • protected function supportsCancelRecurring(): bool { return TRUE; }
  • protected function supportsCancelRecurringNotifyOptional(): bool { return TRUE; }
  /**
   * @param \Civi\Payment\PropertyBag $propertyBag
   *
   * @return array|null[]
   * @throws \Civi\Payment\Exception\PaymentProcessorException
   */
  public function doCancelRecurring(PropertyBag $propertyBag) {
    // Call processor here.
    // In this case we always receive a property bag - it can be
    // used as an array 
    return ['message' => E::ts('Successfully cancelled the subscription.')];
  }

changeSubscriptionAmount

This function allows you to change the subscription amount (and other parameters if supported and specified by getEditableRecurringScheduleFields() function).

It expects an array of parameters in $params. The only required parameters should be:

  • contributionRecurID - which allows the payment processor to look up the contributionRecur that we want to update. The contributionRecur should contain relevant payment processor references in it's processor_id field.
  • amount / {other updateable fields} - ie. may contain fields specified by getEditableRecurringScheduleFields() function.

Most of the "legacy" functions have been replaced with equivalent doX methods but this one is still using an older style. This also means that some older payment processors expect the following legacy parameters: Legacy Parameters

  • subscriptionId - This is the value in the contributionRecur.processor_id field.
  • id - This is the contributionRecur ID.

You can optionally return a message by setting the $message parameter.

  /**
   * Change the amount of the recurring payment.
   *
   * @param string $message
   * @param array $params
   *
   * @return bool
   *   TRUE on success (PaymentProcessorException otherwise)
   *
   * @throws \Civi\Payment\Exception\PaymentProcessorException
   */
  public function changeSubscriptionAmount(&$message = '', $params = []) {
    // Validate parameters
    // Call Payment Processor API to make changes

    // On failure:
    throw new PaymentProcessorException('message explaining why we failed');

    // On success:
    return TRUE;
  }

The handlePaymentNotification method for IPNs and Webhooks

IPN means Instant Payment Notification, although they are usually asynchronous and not "instant". Many third parties talk instead about Webhooks.

It refers to the data sent from the third party (i.e. Payment Processor) on various events e.g.:

- completed/confirmed payment,
- cancellation of recurring payment,
- often many more situations - depends heavily on the third party, often configurable in their account administration facilities

CiviCRM provides a menu route at `civicrm/payment/ipn/<N>` where `<N>` is the payment processor ID.

In older processors notifications may be handled in a separate class that extends the legacy BaseIPN class but the currently recommended method is to implement handlePaymentNotification on the main payment class.

Note that it's good practice to compile the parameters in this function and pass them to a second function for processing, as this allows you to write unit tests.

  /**
   * Handle response from processor.
   *
   * We simply get the params from the REQUEST and pass them to a static function that
   * can also be called / tested outside the normal process
   */
  public function handlePaymentNotification(): void {
    $q = explode('/',$_GET['q']);
    $paymentProcessorID = array_pop($q);

    $params = array_merge($_GET, $_REQUEST);
    $this->_paymentProcessor = civicrm_api3('payment_processor', 'getsingle', ['id' => $paymentProcessorID]);
    $this->processPaymentNotification($params);
  }
  ```

With this payment notification you are likely to need to do one or more of the following things

- Complete a transction. The best way to do this is use v3 `Payment.create` api as in the example below.
- Update a recurring contribution record. Generally use the v4 `ContributionRecur::update()`
- Record a new incoming recurring contribution. We recommend you use the v3 `Contribution.repeatcontribution` with `contribution_status_id` = 'Pending', Followed by v3 `Payment.create` api
- Store a payment token - this is also covered in the example below.

<details>
<summary>Example code</summary>

  ``` 

   /**
   * Update CiviCRM based on outcome of the transaction processing.
   *
   * @param array $params
   *
   * @throws CRM_Core_Exception
   * @throws CiviCRM_API3_Exception
   */
  public function processPaymentNotification(array $params): void {
    // Obviously all the below variables need to be extracted from the params.
    if ($isSuccess) {
      civicrm_api3('Payment', 'create', [
        'contribution_id' => $contributionID,
        'total_amount' => $totalAmount,
        'payment_instrument_id' => $this->_paymentProcessor['payment_instrment_id'],
        'trxn_id' => $trxnID,
        'credit_card_pan' => $last4CardsOfCardIfReturnedHere,
      ]);
      // Perhaps you are saving a payment token for future use (a token
      // is a string provided by the processor to allow you to recharge the card)
      $paymentToken = civicrm_api3('PaymentToken', 'create', [
        'contact_id' => $params['contact_id'],
        'token' => $params['token'],
        'payment_processor_id' => $params['payment_processor_id'] ?? $this->_paymentProcessor['id'],
        'created_id' => CRM_Core_Session::getLoggedInContactID() ?? $params['contact_id'],
        'email' => $params['email'],
        'billing_first_name' => $params['billing_first_name'] ?? NULL,
        'billing_middle_name' => $params['billing_middle_name'] ?? NULL,
        'billing_last_name' => $params['billing_last_name'] ?? NULL,
        'expiry_date' => $this->getCreditCardExpiry($params),
        'masked_account_number' => $this->getMaskedCreditCardNumber($params),
        'ip_address' => CRM_Utils_System::ipAddress(),
    ]);
    }

    if ($thisIsABrowserIwantToRedirect) {
      // This url was stored in the doPayment example above.
      $redirectURL = CRM_Core_Session::singleton()->get("ipn_success_url_{$this->transaction_id}");
      CRM_Utils_System::redirect($redirectUrl);
    }
    // Or perhaps just exit out for a server call.
    CRM_Utils_System::civiExit();
  }

Methods to determine the form fields and insert javascript

Generally you will want to give the form information about how to build the form for your payment processor. These are the methods you should consider implementing to influence the form.

getPaymentTypeLabel | getPaymentTypeName | getTitle

By default the label is 'Credit card' or 'Debit card' and the name is 'credit_card' or 'debit_card' - the label may appear on the forms as a legend when displaying billing fields. The name may be used in css selectors. The title is the name of the processor and may be used when describing the processor ('redirect to Paypal?')

Override to avoid relying on deprecated 'billing_mode' concept.

getText

This allows you to override various bits of help text that might be presented to the user. The context specifies ... the context - eg.

  • contributionPageRecurringHelp
  • contributionPageContinueText
  • cancelRecurDetailText
  • cancelRecurNotSupportedText
  • agreementTitle (generally for direct debits)
  • agreementText (generally for direct debits)

Ideally you should implement this in your payment processor - eg.

``` public function getText(string $context, array $params): string { switch ($context) { case 'contributionPageRecurringHelp': return E::ts('say something helpful');

case 'cancelRecurNotSupportedText':
  return E::ts('good try buddy');

} // For any not specified above. return '';
}
```

getPaymentFormFields

It is recommended that you always override this method. The parent class will come up with a version based on defaults if you do not but it relies on the deprecated ' billing_mode' concept.

 /**
   * Get array of fields that should be displayed on the payment form.
   *
   * Common results are
   *   ['credit_card_type', 'credit_card_number', 'cvv2', 'credit_card_exp_date']
   *   or
   *   ['account_holder', 'bank_account_number', 'bank_identification_number', 'bank_name']
   *   or
   *   []
   *
   * @return array
   *   Array of payment fields appropriate to the payment processor.
   *
   * @throws CiviCRM_API3_Exception
   */
  public function getPaymentFormFields() {
    return ['credit_card_type', 'credit_card_number', 'cvv2', 'credit_card_exp_date'];
  }

getPaymentFormFieldsMetadata

This function allows you to provide information to the form about how to present your fields. If you wish to present fields that are not in the parent function you will need to override this. An example is not copied here as it makes more sense to look up the parent function.

getBillingAddressFields

This is similar to getPaymentFormFields but for billing fields. As with getPaymentFormFields it is recommended you override it so as not to rely on the deprecated 'billing_mode' concept.

getBillingAddressFieldsMetadata

Similar to getPaymentFormFieldsMetadata - override if necessary.

buildForm

This is where you might add javascript or smarty variables e.g.

  /**
   * Interact with the form construction.
   *
   * @param CRM_Core_Form $form
   *
   * @return bool
   *   Should form building stop at this point?
   * @throws \Civi\Payment\Exception\PaymentProcessorException
   */
   public function buildForm(&$form): void {
     // refer https://docs.civicrm.org/dev/en/stable/framework/region/
     CRM_Core_Region::instance('billing-block-post')->add([
       'template' => 'CRM/Financial/Form/MyTemplate.tpl',
       'name' => 'money_money_money',
     ]);
   }

"supportsX" methods

These methods declare what functionality your payment processor offers - they all return booleans. In some cases the parent classes will attempt to guess based on the deprecated 'billing_mode'.

supportsBackOffice

Can an administrative user use this processor? This would likely be false if the user would be redirected to enter card details. It is recommended you override this to avoid a default based on the deprecated 'billing_mode'.

supportsCancelRecurring

Can a recurring contribution be cancelled for this processor from within CiviCRM? CiviCRM will show cancellation links based on this.

supportsCancelRecurringNotifyOptional

Added in CiviCRM 5.27

This dictates whether the checkbox 'Send cancellation request' is presented when cancelling a recurring contribution. It would be true if the processor allows you to cancel a payment within CiviCRM, thus cancelling future payments but it is ALSO possible to cancel within CiviCRM without doing so. An example would be when a user is cancelling a subscription within CiviCRM that is already cancelled at the payment processor end so they don't want a message sent to the processor's server.

If set to FALSE the payment processor should implement doCancelRecurring() and set a default using $propertyBag->setIsNotifyProcessorOnCancelRecur();

supportsChangeSubscriptionAmount

Can the monetary amount of a recurring contribution be changed from within CiviCRM?

supportsEditRecurringContribution

Can fields on the recurring contribution be changed from within CiviCRM? You should also implement getEditableRecurringScheduleFields to declare which fields (amount by default)

supportsFutureRecurStartDate

Can a recurring contribution have a start_date in the future?

supportsLiveMode | supportsTestMode

This is not a function you would normally override and relates to the processor instance not the functionality (ie is the test instance loaded)

supportsMultipleConcurrentPayments

Can two payments be processed in the same web request? Generally this relates to the separate membership payment functionality on the contribution page. An example of a processor that does not support it is paypal standard - the browser needs to be redirected & hence can only do one payment.

supportsNoEmailProvided

Will the processor work with no user email address? If not, one will be added to the payment form even if it is not otherwise required.

supportsPreApproval

This would be true in a flow like Paypal express where the browser is redirected early in the process for authorisation and then the token is charged at the end.

supportsRecurContributionsForPledges

Unused - deprecated.

supportsRecurring

Generally you should not override this and the database setting for the payment processor instance will be used.

supportsRefund

Can a refund be processed from within CiviCRM?

supportsUpdateSubscriptionBillingInfo

Can a user update their billing details from within CiviCRM? For example they might be able to update their credit card details if it has expired. You may need to update the subscriptionUrl function to provide an off-site url.

Available parameters

Core parameters

key ways to access / helpers propertyBag access notes
amount (required) $processor->getAmount($params) propertyBag->getAmount() amount is the money to charge in machine friendly formatting (e.g $4000.56). The helper functions will ensure it is rounded to 2 digits. Alternates, such as total_amount, are long-deprecated.
currency (required) $params['currency'] propertyBag->getCurrency()
contributionID (required) $params['contributionID'] ?? NULL $propertyBag->has('contributionID') && $propertyBag->getContributionID() There are a couple of outstanding cases where this is not yet available. These are treated as bugs.
invoiceID (required) params['invoiceID'] $propertyBag->getInvoiceID() This helps compensate for the instances where contribtion id is not passed in as this is a value the core forms 'intend' to save and it should be 100% reliably passed in.
contactID (required) $params['contactID'] ?? NULL $propertyBag->has('contactID') && $propertyBag->getContactID() As far as we know this is always present but log a bug if you hit cases where it isn not.
email $params['email'] $propertyBag->getEmail()
payment_token $params['payment_token'] ?? $propertyBag->has('paymentToken') && $propertyBag->getPaymentToken() A token provided by a payment processor that allows use of a pre-authorisation.
qfKey (only used in core forms) $params['qfKey'] ?? '' Used to construct return urls for the browser.
description $processor->getDescription($params) $propertyBag->has('description') && $propertyBag->getDescription() $processor->getDescription($params) returns a calculated string that includes the description but also makes various ids available.
ip_address $params['ip_address'] Probably better to just call CRM_Utils_System::ipAddress()
contributionPageID $params['contributionPageID'] ?? '' Available from core contribution page submissions.
eventID $params['eventID'] ?? '' Available from core event form registrations.
financial_type_id $params['financial_type_id'] Rarely used by processors but probably quite useful in the alterPaymentProcessorParams hook.
campaign_id $params['financial_type_id'] Rarely used by processors but probably quite useful in the alterPaymentProcessorParams hook.
accountingCode $params['accountingCode' ?? ''] Accounting code (based on financial type) - passed to paypal as a custom field - it's probably better to call CRM_Financial_BAO_FinancialAccount::getAccountingCode($params['financial_type_id']) than rely on this.

Billing fields

The available billing fields are processor dependent. The processor declares them in the function getBillingAddressFields() which directs the form builder to add them to the form. The metadata is declared with the function getBillingAddressFieldsMetadata(). This function is overrideable but the parent class (CRM_Core_Payment) declares the most common and best supported fields. If you take no action to define the fields to show core will make some assumptions based on 'billing mode' and only present billing fields if the 'billing_mode' is 2 - which would indicate taking details on site.

Also a note about billing fields - these traditionally look like billing_street_address-5 where the 5 is the output of CRM_Core_BAO_LocationType::getBilling();. Core forms will also pass just street_address but we have not audited enough to confirm that is always available. The examples below used $billingLocationID, assume retrieval by the above function.

Note that in testing the parameters from a default contribution page the propertyBag variants did not all work - i.e. the values with billingLocationID appended were not retrieved. However, they are included for completeness.

key ways to access / helpers propertyBag access notes
billing_first_name $params['billing_first_name] $propertyBag->has('firstName') && $propertyBag->getFirstName() $params['first_name'] is mostly or always also available (audit required)
billing_middle_name $params['billing_middle_name] ?? '' $propertyBag->getter('middle_name, TRUE) $params['middle_name'] is mostly or always also available (audit required)
billing_last_name $params['billing_last_name] $propertyBag->has('lastName') && $propertyBag->getLastName() $params['last_name'] is mostly or always also available (audit required)
address_name-{$billingLocationID} $params["address_name-$billingLocationID}"] ?? ''
billing_street_address-{$billingLocationID} $params["billing_street_address-$billingLocationID}"] ?? '' $propertyBag->has('billingStreetAddress') && $propertyBag->getBillingStreetAddress()
billing_supplemental_address-{$billingLocationID} $params["billing_supplemental_address-$billingLocationID}"] ?? '' $propertyBag->has('billingSupplementalAddress1') && $propertyBag->getBillingSupplementalAddress1()
billing_supplemental_address2-{$billingLocationID} $params["billing_supplemental_address2-$billingLocationID}"] ?? '' $propertyBag->has('billingSupplementalAddress2') && $propertyBag->getBillingSupplementalAddress2()
billing_supplemental_address3-{$billingLocationID} $params["billing_supplemental_address3-$billingLocationID}"] ?? '' $propertyBag->has('billingSupplementalAddress3') && $propertyBag->getBillingSupplementalAddress3()
billing_city-{$billingLocationID} $params["billing_city-$billingLocationID}"] ?? '' $propertyBag->has('billingCity') && $propertyBag-getBillingCity()
billing_postal_code-{$billingLocationID} $params["billing_postal_code-$billingLocationID}"] ?? '' $propertyBag->has('billingPostalCode') && $propertyBag-getBillingPostalCode()
billing_country-{$billingLocationID} $params["billing_country-$billingLocationID}"] ?? '' $propertyBag->has('billingCountry') && $propertyBag-getBillingCountry()
billing_state_province_id-{$billingLocationID} $params["billing_state_province_id-{$billingLocationID}"] ?? ''
billing_county-{$billingLocationID} $params["billing_county-$billingLocationID}"] ?? '' $propertyBag->has('billingCounty') && $propertyBag-getBillingCounty()
phone $params['phone'] ?? '' $propertyBag->has('phone') && $propertyBag-getPhone() This is in the property bag but it's not clear which forms, if any, pass it in.

Payment fields

The available payment fields are processor dependent. The processor declares them in the function getPaymentFormFields() which directs the form builder to add them to the form. The metadata is declared with the function getPaymentFormFieldsMetadata(). This function is overrideable but the parent class (CRM_Core_Payment) declares the most common and best supported fields. If you take no action to define the fields to show core will make some assumptions based on 'billing mode' and only present billing fields if the billing_mode is not 4 (transfer offsite). It will choose between credit and debit card related fields based on the payment_instrument_id.

key ways to access / helpers propertyBag access notes
credit_card_number $params['credit_card_number]
cvv2 $params['cvv2']
credit_card_exp_date $params['credit_card_exp_date'] This is an array like ['M' => 5, 'Y' => 2020]
credit_card_type $params['credit_card_type']
bank_account_number $params['bank_account_number'] For debit payments.
bank_account_holder $params['bank_account_holder'] For debit payments.
bank_identification_number $params['bank_identification_number'] For debit payments.
bank_name $params['bank_name'] For debit payments.

Recurring contribution parameters

key ways to access / helpers propertyBag access notes
is_recur $propertyBag->getIsRecur() Is this a recurring payment
contributionRecurID $params['contributionRecurID] ?? NULL $propertyBag->has('contributionRecurID') &&$propertyBag->getContributionRecurID()` id for the recurring contribution record, if available
installments $params['installments'] ?? ''
frequency_unit $params['frequency_unit'] ?? NULL $propertyBag->has('frequencyUnit') && $propertyBag->recurFrequencyUnit()
frequency_interval $params['frequency_interval'] ?? NULL $propertyBag->has('frequencyInterval') && $propertyBag->recurFrequencyInterval()
subscriptionId $params['subscriptionId'] ?? NULL $propertyBag->has('recurProcessorID') && $propertyBag->recurProcessorID() The processor_id field in the civicrm_contribution_recur table - this is the value the external processor uses as its reference.

Introducing PropertyBag objects

As noted above the params array can be confusing to work with. In 5.24 the option was introduced to cast it to a more prescriptive, typed way to pass in parameters by using a Civi\Payment\PropertyBag object instead of an array. Your code will in most cases still receive an Array rather than a PropertyBag (the only place where core passes out a PropertyBag is doCancelRecurring(), which was an experiment). Regardless or whether you receive a PropertyBag or not you can either use it as an array or cast it to a PropertyBag

This object has getters and setters that enforce standardised property names and a certain level of validation and type casting. For example, the property contactID (note capitalisation) has getContactID() and setContactID(). However, be careful as the PropertyBag often throws errors if the parameter is NULL - so you need to use has before get in most cases.

For backwards compatibility, this class implements ArrayAccess which means if old code does $propertyBag['contact_id'] = '123' or $propertyBag['contactID'] = 123 it will translate this to the new contactID property and use that setter which will ensure that accessing the property returns the integer value 123. When this happens deprecation messages are emitted to the log file and displayed if your site is configured to show deprecation notices.

Checking for existence of a property

Calling a getter for a property that has not been set will throw a BadMethodCall exception.

Code can require certain properties by calling $propertyBag->require(['contactID', 'contributionID']) which will throw an InvalidArgumentException if any property is missing. These calls should go at the top of your methods so that it's clear to a developer.

You can check whether a property has been set using $propertyBag->has('contactID') which will return TRUE or FALSE.

Multiple values, e.g. changing amounts

All the getters and setters take an optional extra parameter called $label. This can be used to store two (or more) different versions of a property, e.g. 'old' and 'new'

<?php
use Civi\Payment\PropertyBag;
//...
$propertyBag = new PropertyBag();
$propertyBag->setAmount(1.23, 'old');
$propertyBag->setAmount(2.46, 'new');
//...
$propertyBag->getAmount('old'); // 1.23
$propertyBag->getAmount('new'); // 2.46
$propertyBag->getAmount(); // throws BadMethodCall

This means the value is still validated and type-cast as an amount (in this example).

Custom payment processor-specific data

Warning

This is currently holding back a fuller adoption of PropertyBag.

Sometimes a payment processor will require custom data. e.g. A company called Stripe offers payment processing gateway services with its own API which requires some extra parameters called paymentMethodID and paymentIntentID - these are what that particular 3rd party requires and separate to anything in CiviCRM (CiviCRM also uses the concept of "payment methods" and these have IDs, but here we're talking about something Stripe needs).

In order for us to be able to implement the doPayment() method for Stripe, we'll need data for these custom, bespoke-to-the-third-party parameters passing in, via the PropertyBag.

So that any custom, non-CiviCRM data is handled unambiguously, these property names should be prefixed, e.g. stripe_paymentMethodID and set using PropertyBag->setCustomProperty($prop, $value, $label = 'default').

The payment class is responsible for validating such data; anything is allowed by setCustomProperty, including NULL.

However, payment classes are rarely responsible for passing data in, this responsibility is for core and custom implementations. Core's contribution forms and other UIs need a way to take the data POSTed by the forms, and arrange it into a standard format for payment classes. They will also have to pass the rest of the data to the payment class so that the payment class can extract and validate anything that is bespoke to that payment processor; i.e. only Stripe is going to know to expect a paymentMethodID in this data because this does not exist for other processors. As of 5.24, this does not exist and data is still passed into a PropertyBag as an array, which means that unrecognised keys will be added as custom properties, but emit a deprecation warning in your logs.

Best practice if you wish to use PropertyBag right now would be to:

  • use PropertyBag getters for the data you want, whether that's a core field or use getCustomProperty for anything else.

  • where your processor requires adding in custom data to the form, prefix it with your extension's name to avoid ambiguity with core fields. e.g. your forms might use a field called myprocessor_weirdCustomToken and you would access this via $propertyBag->getCustomProperty('myprocessor_weirdCustomToken).