Skip to content


FormBuilder Behaviors


Simply put, a Behavior extends the functionality of an entity. Behaviors are PHP classes written by a developer, which can be enabled in the GUI to affect how entities behave on a form.

Each Behavior class declares some configuration metadata which automatically appears in the Afform GUI. The user can make selections to enable and configure the behavior.

To enact its functionality, a behavior class can listen to any Civi hook or event, notably civi.afform.prefill and civi.afform.submit. These events are called for every entity on a form, and will include information such as the entity name, type, and any behaviors that have been configured for that entity.


A good example to emulate is the Contact Dedupe Behavior.

class ContactDedupe extends AbstractBehavior implements EventSubscriberInterface {

It starts off by extending the AbstractBehavior class, which makes it discoverable to Afform, and also implementing EventSubscriberInterface, which is the recommended way to subscribe to events. That interface requires a getSubscribedEvents function:

   * @return array
  public static function getSubscribedEvents() {
    return [
      'civi.afform.submit' => ['onAfformSubmit', 101],

That registers the callback function named onAfformSubmit in the same class, to be called every time an Afform entity is about to be saved.

The next 4 functions are part of the AfformBehavior interface, and provide enough information for the AfformAdmin GUI to show the behavior to the user for configuration:

  public static function getEntities():array {
    return \CRM_Contact_BAO_ContactType::basicTypes();

  public static function getTitle():string {
    return E::ts('Duplicate Matching');

  public static function getDescription():string {
    return E::ts('Update existing contact instead of creating a new one based on a dedupe rule.');

  public static function getModes(string $entityName):array {

Note that getEntities does not simply return "Contact" because Afform considers "Individual", "Household" and "Organization" to all be their own entities.

The getModes function returns an array of operation modes (in this case dedupe rules) for a particular entity type. So if your Behavior can act on more than one entity type as this one can, pay attention to the $entityName parameter and only return modes relevant to that type of entity (in this case, there are different dedupe rules for "Individual" vs "Organization", etc).

Finally, the callback registered with getSubscribedEvents:

  public static function onAfformSubmit(AfformSubmitEvent $event) {
    $entity = $event->getEntity();
    $dedupeMode = $entity['contact-dedupe'] ?? NULL;
    if ($event->getEntityType() !== 'Contact' || !$dedupeMode) {
    // Apply dedupe rule if contact isn't already identified
    foreach ($event->records as $index => $record) {
      $supportedJoins = ['Address', 'Email', 'Phone', 'IM'];
      $values = $record['fields'] ?? [];
      foreach ($supportedJoins as $joinEntity) {
        if (!empty($record['joins'][$joinEntity][0])) {
          $values += \CRM_Utils_Array::prefixKeys($record['joins'][$joinEntity][0], strtolower($joinEntity) . '_primary.');
      $match = Contact::getDuplicates(FALSE)
      if (!empty($match['id'])) {
        $event->setEntityId($index, $match['id']);

This function checks the contact-dedupe mode set by the Admin (this is a kebab-case version of the class name) and takes action on every record being saved for that entity (normally one entity saves one record, but because of the AfRepeat feature, entities should always be treated as if they may be multivalued).