Behaviors
FormBuilder Behaviors¶
Overview¶
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.
Examples¶
Contact Dedupe Behavior¶
A good example to emulate for civi.afform.submit 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) {
return;
}
// 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)
->setValues($values)
->setDedupeRule($dedupeMode)
->execute()->first();
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).
Grant Prefill¶
An example of civi.afform.prefill is Grant Prefill
The class definition and the 5 interface functions documented above for Contact Dedupe Behavior are similar. It also defines getTemplate(), which injects a partial Angular form (selectGrantBehavior.html) into the FormBuilder editing screen. It also defines getAttributes(), which is required in order to save any non-core settings on the editing screen.
public static function getAttributes(): array {
return [
'select-grant-grant-type' => 'js',
'select-grant-grant-status' => 'js',
];
}
onAfformPrefill() is registered as a callback in getSubscribedEvents(), which does the pre-filling when viewing the form itself:
public static function onAfformPrefill(AfformPrefillEvent $event): void {
if ('Grant' === $event->getEntityType()) {
$entity = $event->getEntity();
$contactId = $event->getEntityIds('Organization1')[0];
if ($entity['select-grant'] === 'selectlatest') {
$grantQuery = \Civi\Api4\Grant::get(TRUE)
->addWhere('contact_id', '=', $contactId);
if (isset($entity['select-grant-grant-type'])) {
foreach ($entity['select-grant-grant-type'] as $type) {
if (is_numeric($type)) {
$types[] = $type;
}
}
if (isset($types)) {
$grantQuery->addWhere('grant_type_id', 'IN', $types);
}
}
if (isset($entity['select-grant-grant-status'])) {
foreach ($entity['select-grant-grant-status'] as $status) {
if (is_numeric($status)) {
$statuses[] = $status;
}
}
if (isset($statuses)) {
$grantQuery->addWhere('status_id', 'IN', $statuses);
}
}
$grant = $grantQuery->execute()->first();
if ($grant['id']) {
$event->getApiRequest()->loadEntity($entity, [['id' => $grant['id']]]);
}
}
}
}
This function uses the Grants API to determine whcih grant to prefill, then fills it using $event->getApiRequest()->loadEntity().