Skip to content

APIv4 Fields

In most cases the values refer to the database fields by the database field name.

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.age is calculated from the Contact.birth_date field and the current date.
  • multiple fields from the same record: Contribution.tax_exclusive_amount is the difference of Contribution.total_amount and Contribution.tax_amount.
  • a sub-query or join using other tables: Contribution.balance_amount is 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 FieldSpec object.
  • 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 to
  • Activity.source_contact_id, Activity.target_contact_id, Activity.assignee_contact_id - contacts related to the activity
  • Address.proximity - (Filter) determines whether an address is within a given distance of a location (see example)
  • Contact.age_years- age of a contact (based on birth_date) in years
  • Contact.groups - (Filter) determines whether a contact belongs to any of the specified groups
  • Contact.address_primary, address_billing, email_primary, email_billing, phone_primary, phone_billing, im_primary, im_billing - addresses associated with a contact
  • Contribution.balance_amount - balance
  • Contribution.paid_amount - amount paid
  • Contribution.tax_exclusive_amount - total amount less the tax amount
  • Domain.is_active - is this the current active domain
  • MessageTemplate.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.