Skip to content

hook_civicrm_initiators

Summary

This hook lists the ways a user can obtain an API key for a record that connects to a remote service, such as starting an OAuth "Authorization Code" grant.

Description

Some records hold credentials for a remote service. Rather than asking the user to paste an API key, an extension can offer an initiator: a widget (typically a "Connect to ..." button) that fetches the key for them. The canonical initiator starts an OAuth2 authorization grant, but the hook is not tied to OAuth.

Core calls the hook through Civi\Connect\Initiators::create(), which sorts the results by title and fills in computed properties. Each initiator's render callback is then called to add its markup to a region on the form.

The $context['for'] value identifies the kind of record. Currently supported:

for Where Other $context keys
PaymentProcessor "Edit Payment Processor" form, once for the live and once for the test credentials payment_processor_type, payment_processor_id, is_test
MailSettings (6.20+) "Edit Mail Account" form, for an existing account mail_settings_id

The oauth-client extension implements this hook for:

  • payment processors whose OAuth provider is tagged PaymentProcessorType:{TYPE_NAME}. See Civi\OAuth\OAuthPaymentProcessorTag.
  • mail accounts, using any active client whose provider declares a mailSettingsTemplate. See Civi\OAuth\OAuthMailSettingsTag.

Describing an existing connection

From CiviCRM 6.20, an initiator can also report on a connection the record already has, so the form can show its status instead of only offering to start a new one. Set is_connected on the initiator that owns the connection, along with any of status_message, status_severity, manage_url and managed_fields (see Parameters).

The "Edit Mail Account" form uses all of these. When an initiator reports a connection, the form shows status_message in an alert coloured by status_severity (default success), links to manage_url, and renders the initiators' widgets under a collapsed "Re-connect" option. When nothing is connected, it renders them as ways to connect. Once connected, an implementation should usually offer only the initiator that owns the connection, as oauth-client does, so re-connecting cannot silently switch to a different account.

A form that supports this reads it back through Civi\Connect\Initiators:

  • getConnected(): array returns the first initiator with is_connected, or [] if there is none.
  • getManagedFields(): array returns that initiator's managed_fields. These are fields the connection supplies at runtime (e.g. a password replaced by an OAuth access token), so the form hides them and leaves their stored values untouched on save.

Availability

CiviCRM 6.17+. The connection-status properties were added in 6.20.

Definition

hook_civicrm_initiators(array $context, array &$available, ?string &$default)

This event supports a targeted alias, hook_civicrm_initiators::{$context['for']} (e.g. hook_civicrm_initiators::PaymentProcessor), so a Symfony listener can subscribe to one kind of record only.

Parameters

  • array $context: describes the record that needs an API key.
    • for: string, required. The kind of record, e.g. PaymentProcessor.
    • payment_processor_type: string. For PaymentProcessor, the processor type's name, e.g. Stripe.
    • payment_processor_id: int|null. For PaymentProcessor, the record's ID (NULL when adding a new processor).
    • is_test: bool. For PaymentProcessor, whether these are the test credentials.
    • mail_settings_id: int. For MailSettings, the record's ID.
  • array &$available: the list of initiators. Add an item keyed by a unique symbolic name, with these properties:
    • title: string. The name shown to the user.
    • render: callable. Adds the widget to the form. Signature: function(CRM_Core_Region $region, array $context, array $initiator): void.
    • is_connected: bool, optional (6.20+). Whether the record is currently connected through this initiator.
    • status_message: string, optional (6.20+). Describes the current connection, e.g. "Connected as foo@example.org".
    • status_severity: string, optional (6.20+). One of success, warning or danger.
    • manage_url: string, optional (6.20+). The screen that administers this connection.
    • managed_fields: string[], optional (6.20+). Fields the connection supplies at runtime, which the form should hide and not overwrite.
    • After the hook runs, core also sets name (the array key) and is_default.
  • string|null &$default: the key of the suggested initiator, if any.

Returns

  • null

Example

use CRM_Myext_ExtensionUtil as E;

function myext_civicrm_initiators(array $context, array &$available, ?string &$default) {
  if ($context['for'] !== 'PaymentProcessor' || $context['payment_processor_type'] !== 'MyProcessor') {
    return;
  }
  $available['myprocessor_connect'] = [
    'title' => E::ts('My Processor'),
    'render' => function (CRM_Core_Region $region, array $context, array $initiator) {
      $url = CRM_Utils_System::url('civicrm/myprocessor/connect', [
        'id' => $context['payment_processor_id'],
        'is_test' => (int) $context['is_test'],
      ]);
      $region->addMarkup(sprintf('<a class="btn btn-xs btn-primary" href="%s">%s</a>',
        htmlentities($url), E::ts('Connect to %1', [1 => $initiator['title']])));
    },
  ];
  $default ??= 'myprocessor_connect';
}

Or, as a targeted listener:

public static function getSubscribedEvents(): array {
  return [
    '&hook_civicrm_initiators::PaymentProcessor' => ['onPaymentInitiators', 0],
  ];
}

public function onPaymentInitiators(array $context, array &$available, &$default): void {
  // ...
}

Reporting an existing connection

This listener offers a connection for mail accounts and, once a credential exists, reports on it. password is a managed field because the extension supplies it when the mailbox is polled, so the form hides it and saving the form does not overwrite it.

public static function getSubscribedEvents(): array {
  return [
    '&hook_civicrm_initiators::MailSettings' => ['onMailInitiators', 0],
  ];
}

public function onMailInitiators(array $context, array &$available, &$default): void {
  $token = $this->findToken($context['mail_settings_id']);
  $available['mymail'] = [
    'title' => E::ts('My Mail Service'),
    'render' => [$this, 'renderConnectButton'],
  ];
  if ($token) {
    $expired = $token['expires'] < CRM_Utils_Time::time();
    $available['mymail'] += [
      'is_connected' => TRUE,
      'status_severity' => $expired ? 'warning' : 'success',
      'status_message' => $expired
        ? E::ts('The connection has expired. Mail will not be collected until you re-connect.')
        : E::ts('Connected as %1.', [1 => $token['owner']]),
      'manage_url' => (string) Civi::url('backend://civicrm/mymail/settings'),
      'managed_fields' => ['password'],
    ];
  }
}

Tip

The form calls the hook on every page view, so read stored state rather than contacting the remote service.