APIv4 Fields¶
In most cases the values refer to the database fields by the database field name.
- In some cases the field may be qualified with a
:denoting that data about the field is displayed - In some cases the field may be extended with a
.to join to another entity - In some cases calculated fields will be exposed
Calculated Fields¶
Calculated Fields are fields that are determined from other data. A few important things to remember:
- they are a feature of APIv4 and so are not present in APIv3 or via BAO's.
- they are not retrieved unless specifically requested as they can be expensive.
- they are available as tokens, for example when sending email, and anywhere else APIv4 is used, such as SearchKit.
Calculated fields can be based on:
- a single field:
Contact.ageis calculated from theContact.birth_datefield and the current date. - multiple fields from the same record:
Contribution.tax_exclusive_amountis the difference ofContribution.total_amountandContribution.tax_amount. - a sub-query or join using other tables:
Contribution.balance_amountis calculated by a query on other financial tables.
Calculated Fields are specified in GetSpecProvider files. Look at the existing ones for examples in <civi_root>/Civi/Api4/Service/Spec/Provider/*GetSpecProvider.php
The SpecProvider has 3 main parts:
- the field definition - creating a
FieldSpecobject. - the
applies()function - limiting which entities and actions this applies to. - the SQL string - defining how to calculate the new field (but see below for Entity Refs).
For a Calculated Field to be available through the API, the SpecProvider must be made discoverable. In an extension, one way to do this is to make your SpecProvider extend the AutoService interface, and enable the scan-classes mixin. When developing an extension, the mixin can be enabled using civix:
civix mixin --enable=scan-classes@1
Using scan-classes may not be desirable for more complex/heavy extensions. Another way to make the SpecProvider discoverable is to register it individually with the CiviCRM container, with the tag spec_provider. For example, inside your extension's implementation of hook_civicrm_container:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
function my_ext_civicrm_container(ContainerBuilder $container) {
$class = \Civi\Api4\Service\Spec\Provider\MyFieldGetSpecProvider::class;
$container->setDefinition($class, new Definition($class))
->setPublic(TRUE)
->addTag('spec_provider');
}
Hint
When developing these Calculated Fields, use the debug feature of API4 Explorer to see the generated SQL.
Calculated Fields in FormBuilder¶
To make your calculated field available as a filter on a FormBuilder search form, include an input type. For example:
$field->setInputType('Select')
->setOptions([1 => 'One', 2 => 'Two']);
Entity Refs¶
One powerful feature of Calculated Fields is the ability to use them as Entity References if the calculated value is a foreign key. All of the referenced entity's fields can be accessed without further definition of those fields.
So for example, the Calculated Field Contact.address_primary links to the Contact's primary address (determined by joining to the civicrm_address table) which then provides access to Contact.address_primary.city.
The SQL string defining the join for a reference like this moves to a file in <civi_root>/Civi/Api4/Event/Subscriber/*SchemaMapSubscriber.php
For an example, study the implementation of Contact.address_primary in ContactGetSpecProvider.php and ContactSchemaMapSubscriber.php
Reminder
Note that a cache clear is needed after making changes to the *SchemaMapSubscriber.php files.
Core Calculated Fields¶
This section contains a listing of Calculated Fields in core. Use them as inspiration!
Activity.case_id- provides the case that an activity is linked toActivity.source_contact_id,Activity.target_contact_id,Activity.assignee_contact_id- contacts related to the activityAddress.proximity- (Filter) determines whether an address is within a given distance of a location (see example)Contact.age_years- age of a contact (based onbirth_date) in yearsContact.groups- (Filter) determines whether a contact belongs to any of the specified groupsContact.address_primary, address_billing, email_primary, email_billing, phone_primary, phone_billing, im_primary, im_billing- addresses associated with a contactContribution.balance_amount- balanceContribution.paid_amount- amount paidContribution.tax_exclusive_amount- total amount less the tax amountDomain.is_active- is this the current active domainMessageTemplate.master_id- the original version of a MessageTemplate if it has been modified
Filters vs Fields
Most calculated fields can be used in any part of the query (SELECT, ORDER BY, HAVING, etc) but fields designaged as type '(Filter)' can only be used in the WHERE clause.