Allow more structured metadata for APIv4 actions
Overview
Expand the range of metadata that can be reported via APIv4, enabling more dev-tools and site-builder-tools. Specifically, allow for structured-data-types which are not entities. This is useful for action-parameters and value-types.
Example use-case
- For a specific workflow-message, provide a detailed list of fields that should provided in the message's evaluation context.
- For
Afform.prefill
, provide a detailed list of fields that are expected for thearray $args
. - For
Afform.submit
, provide a detailed list of fields that are expected for thearray $values
andarray $args
.
(The motivation here is more about providing metadata for message-workflow parameters, but the same reasoning applies to Afform
APIs which already exist, so I'll use those for the examples.)
Current behaviour
The type
is declared as array
. This does not truly indicate what is accepted/expected/required. Ex:
use Civi\Afform\Event\AfformSubmitEvent;
class Submit extends AbstractProcessor {
/**
* Submitted values
* @var array
*/
protected $values;
}
// Afform.getActions returns:
"name": "submit",
"params": {
"values": {
"description": "Submitted values",
"type": ["array"]
},
Proposed behaviour
Any API parameter of type @var array
may also declare a @schema
. This is a symbolic name of the data-type -- and using this name, you can get more detailed metadata.
use Civi\Afform\Event\AfformSubmitEvent;
class Submit extends AbstractProcessor {
/**
* Submitted values
* @var array
* @schema /Afform/FormFields?$name
*/
protected $values;
}
// Afform.getActions - give that `$name==newContact` would return:
"name": "submit",
"params": {
"values": {
"description": "Submitted values",
"type": ["array"],
"schema": "/Afform/FormFields/newContact"
},
And then a bit more detailed usage:
// Looking up the metadata for an API's action-parameter.
$actions = civicrm_api4('Afform', 'getActions', [
'values' => ['name' => 'newContact'],
])->indexBy('name');
$this->assertEquals('array', $actions['submit']['params']['args']['type']); // Status quo
$this->assertEquals('array', $actions['submit']['params']['values']['type']); // Status quo
$this->assertEquals('/Afform/FormArgs?newContact', $actions['submit']['params']['args']['schema']); // New
$this->assertEquals('/Afform/FormFields?newContact', $actions['submit']['params']['values']['schema']); // New
$actions = civicrm_api4('MessageWorkflow', 'getActions', [
'values' => ['name' => 'pcp_notify'],
])->indexBy('name');
$this->assertEquals('array', $actions['render']['params']['rows']['type']); // Status quo
$this->assertEquals('/MessageWorkflow/TplParams[]?pcp_notify', $actions['render']['params']['rows']['schema']); // New
// Resolve the 'schema' via internal/optimized API
$fields = Civi::schema()->get('/Afform/FormFields?newContact');
$this-=>assertEquals('String', $fields['Contact1.first_name']['type']);
// Resolve 'schema' via external/remote API
civicrm_api4('Schema', 'get', ['where' => [['name', '=', '/Afform/FormFields/newContact']], 'format' => 'json-schema-7']);
civicrm_api4('Schema', 'get', ['where' => [['name', '=', '/MessageWorkflow/TplParams/pcp_notify']], 'format' => 'civi-schema-1'])
// Supply a schema definition
Civi::dispatcher()->addListener('civi.schema.resolve::/Afform/FormFields', function($e) {
$afform = Civi::service('afform_scanner')->getLayout($e->getUrlParam());
$e->setSchema(...extractFields($afform)...);
});
Comments
(1) Note that the @schema
can be parameterized (e.g. $name
or $entity
). This means that (e.g.) the newContact
form and the newActivity
form would have different schemas.
(2) The @schema
identifier is a relative URL. This is inspired by JSON Schema.
(3) There is a part of me which thinks it would be really nice to use an existing standard (like JSON Schema). However, this would also create internal-inconsistencies. It seems more realistic to re-use the Civi Schema (akin to '$dao::fields()`) and then define an export-mapping (Civi Schema => JSON Schema) if needed.
(4) It might also make sense to extend this to, say, {$entity}::create()
and {$entity}::update()
e.g.
class AbstractCreateAction ... {
/**
* List of values to set on the new entity.
* @var array
* @schema /Entity/Fields?$entity
* - This is just an alias for '\Civi\Api4\$entity::getFields()`
*/
protected $values = [];
}
(5) I think I can get through my immediate work without this - hence type:backlog
. However, I wanted to document the idea.
Related rambling: https://chat.civicrm.org/civicrm/pl/o5bjakwjufdgxgrw6zbumh8zjy