This allows any fieldSpec provider to change the callback used to build field options,
or to add an ad-hoc field not in the schema with its own option getter.
namespace Civi\Api4\Action\CustomValue;
-use Civi\Api4\Service\Spec\SpecFormatter;
-
/**
* Get fields for a custom group.
*/
/** @var \Civi\Api4\Service\Spec\SpecGatherer $gatherer */
$gatherer = \Civi::container()->get('spec_gatherer');
$spec = $gatherer->getSpec('Custom_' . $this->getCustomGroup(), $this->getAction(), $this->includeCustom, $this->values);
- return SpecFormatter::specToArray($spec->getFields($fields), $this->loadOptions);
+ return $this->specToArray($spec->getFields($fields));
}
/**
namespace Civi\Api4\Generic;
-use Civi\Api4\Service\Spec\SpecFormatter;
-
/**
* @inheritDoc
* @method bool getIncludeCustom()
$this->includeCustom = strpos(implode('', $fieldsToGet), '.') !== FALSE;
}
$spec = $gatherer->getSpec($this->getEntityName(), $this->getAction(), $this->includeCustom, $this->values);
- $fields = SpecFormatter::specToArray($spec->getFields($fieldsToGet), $this->loadOptions, $this->values);
+ $fields = $this->specToArray($spec->getFields($fieldsToGet));
foreach ($fieldsToGet ?? [] as $fieldName) {
if (empty($fields[$fieldName]) && strpos($fieldName, '.') !== FALSE) {
$fkField = $this->getFkFieldSpec($fieldName, $fields);
return $fields;
}
+ /**
+ * @param \Civi\Api4\Service\Spec\FieldSpec[] $fields
+ *
+ * @return array
+ */
+ protected function specToArray($fields) {
+ $fieldArray = [];
+
+ foreach ($fields as $field) {
+ if ($this->loadOptions) {
+ $field->getOptions($this->values, $this->loadOptions, $this->checkPermissions);
+ }
+ $fieldArray[$field->getName()] = $field->toArray();
+ }
+
+ return $fieldArray;
+ }
+
/**
* @param string $fieldName
* @param array $fields
/**
* @var int
*/
- protected $customFieldId;
+ public $customFieldId;
/**
* @var int
*/
- protected $customGroup;
+ public $customGroup;
/**
* @var string
*/
- protected $tableName;
+ public $tableName;
/**
* @inheritDoc
namespace Civi\Api4\Service\Spec;
-use Civi\Api4\Utils\CoreUtil;
-
class FieldSpec {
/**
* @var mixed
*/
- protected $defaultValue;
+ public $defaultValue;
/**
* @var string
*/
- protected $name;
+ public $name;
/**
* @var string
*/
- protected $label;
+ public $label;
/**
* @var string
*/
- protected $title;
+ public $title;
/**
* @var string
*/
- protected $entity;
+ public $entity;
/**
* @var string
*/
- protected $description;
+ public $description;
/**
* @var bool
*/
- protected $required = FALSE;
+ public $required = FALSE;
/**
* @var bool
*/
- protected $requiredIf;
+ public $requiredIf;
/**
* @var array|bool
*/
- protected $options;
+ public $options;
+
+ /**
+ * @var callable
+ */
+ private $optionsCallback;
/**
* @var string
*/
- protected $dataType;
+ public $dataType;
/**
* @var string
*/
- protected $inputType;
+ public $inputType;
/**
* @var array
*/
- protected $inputAttrs = [];
+ public $inputAttrs = [];
/**
* @var string
*/
- protected $fkEntity;
+ public $fkEntity;
/**
* @var int
*/
- protected $serialize;
+ public $serialize;
/**
* @var string
*/
- protected $helpPre;
+ public $helpPre;
/**
* @var string
*/
- protected $helpPost;
+ public $helpPost;
/**
* @var array
*/
- protected $permission;
+ public $permission;
/**
* @var string
*/
- protected $columnName;
+ public $columnName;
/**
* @var bool
*/
- protected $readonly = FALSE;
+ public $readonly = FALSE;
/**
* @var callable[]
*/
- protected $outputFormatters = [];
+ public $outputFormatters = [];
/**
* Aliases for the valid data types
/**
* @param array $values
* @param array|bool $return
+ * @param bool $checkPermissions
* @return array
*/
- public function getOptions($values = [], $return = TRUE) {
- if (!isset($this->options) || $this->options === TRUE) {
- $fieldName = $this->getName();
-
- if ($this instanceof CustomFieldSpec) {
- // buildOptions relies on the custom_* type of field names
- $fieldName = sprintf('custom_%d', $this->getCustomFieldId());
- }
-
- // BAO::buildOptions returns a single-dimensional list, we call that first because of the hook contract,
- // @see CRM_Utils_Hook::fieldOptions
- // We then supplement the data with additional properties if requested.
- $bao = CoreUtil::getBAOFromApiName($this->getEntity());
- $optionLabels = $bao::buildOptions($fieldName, NULL, $values);
-
- if (!is_array($optionLabels) || !$optionLabels) {
- $this->options = FALSE;
+ public function getOptions($values = [], $return = TRUE, $checkPermissions = TRUE) {
+ if (!isset($this->options)) {
+ if ($this->optionsCallback) {
+ $this->options = ($this->optionsCallback)($this, $values, $return, $checkPermissions);
}
else {
- $this->options = \CRM_Utils_Array::makeNonAssociative($optionLabels, 'id', 'label');
- if (is_array($return)) {
- self::addOptionProps($bao, $fieldName, $values, $return);
- }
+ $this->options = FALSE;
}
}
return $this->options;
}
/**
- * Augment the 2 values returned by BAO::buildOptions (id, label) with extra properties (name, description, color, icon, etc).
- *
- * We start with BAO::buildOptions in order to respect hooks which may be adding/removing items, then we add the extra data.
+ * @param array|bool $options
*
- * @param \CRM_Core_DAO $baoName
- * @param string $fieldName
- * @param array $values
- * @param array $return
- */
- private function addOptionProps($baoName, $fieldName, $values, $return) {
- // FIXME: For now, call the buildOptions function again and then combine the arrays. Not an ideal approach.
- // TODO: Teach CRM_Core_Pseudoconstant to always load multidimensional option lists so we can get more properties like 'color' and 'icon',
- // however that might require a change to the hook_civicrm_fieldOptions signature so that's a bit tricky.
- if (in_array('name', $return)) {
- $props['name'] = $baoName::buildOptions($fieldName, 'validate', $values);
- }
- $return = array_diff($return, ['id', 'name', 'label']);
- // CRM_Core_Pseudoconstant doesn't know how to fetch extra stuff like icon, description, color, etc., so we have to invent that wheel here...
- if ($return) {
- $optionIds = implode(',', array_column($this->options, 'id'));
- $optionIndex = array_flip(array_column($this->options, 'id'));
- if ($this instanceof CustomFieldSpec) {
- $optionGroupId = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', $this->getCustomFieldId(), 'option_group_id');
- }
- else {
- $dao = new $baoName();
- $fieldSpec = $dao->getFieldSpec($fieldName);
- $pseudoconstant = $fieldSpec['pseudoconstant'] ?? NULL;
- $optionGroupName = $pseudoconstant['optionGroupName'] ?? NULL;
- $optionGroupId = $optionGroupName ? \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroupName, 'id', 'name') : NULL;
- }
- if (!empty($optionGroupId)) {
- $extraStuff = \CRM_Core_BAO_OptionValue::getOptionValuesArray($optionGroupId);
- $keyColumn = $pseudoconstant['keyColumn'] ?? 'value';
- foreach ($extraStuff as $item) {
- if (isset($optionIndex[$item[$keyColumn]])) {
- foreach ($return as $ret) {
- // Note: our schema is inconsistent about whether `description` fields allow html,
- // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
- $this->options[$optionIndex[$item[$keyColumn]]][$ret] = ($ret === 'description' && isset($item[$ret])) ? strip_tags($item[$ret]) : $item[$ret] ?? NULL;
- }
- }
- }
- }
- else {
- // Fetch the abbr if requested using context: abbreviate
- if (in_array('abbr', $return)) {
- $props['abbr'] = $baoName::buildOptions($fieldName, 'abbreviate', $values);
- $return = array_diff($return, ['abbr']);
- }
- // Fetch anything else (color, icon, description)
- if ($return && !empty($pseudoconstant['table']) && \CRM_Utils_Rule::commaSeparatedIntegers($optionIds)) {
- $sql = "SELECT * FROM {$pseudoconstant['table']} WHERE id IN (%1)";
- $query = \CRM_Core_DAO::executeQuery($sql, [1 => [$optionIds, 'CommaSeparatedIntegers']]);
- while ($query->fetch()) {
- foreach ($return as $ret) {
- if (property_exists($query, $ret)) {
- // Note: our schema is inconsistent about whether `description` fields allow html,
- // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
- $this->options[$optionIndex[$query->id]][$ret] = $ret === 'description' ? strip_tags($query->$ret) : $query->$ret;
- }
- }
- }
- }
- }
- }
- if (isset($props)) {
- foreach ($this->options as &$option) {
- foreach ($props as $name => $prop) {
- $option[$name] = $prop[$option['id']] ?? NULL;
- }
- }
- }
+ * @return $this
+ */
+ public function setOptions($options) {
+ $this->options = $options;
+ return $this;
}
/**
- * @param array|bool $options
+ * @param callable $callback
*
* @return $this
*/
- public function setOptions($options) {
- $this->options = $options;
+ public function setOptionsCallback($callback) {
+ $this->optionsCallback = $callback;
return $this;
}
+ /**
+ * @return callable
+ */
+ public function getOptionsCallback() {
+ return $this->optionsCallback;
+ }
+
/**
* @return string
*/
}
/**
- * @param array $values
+ * Gets all public variables, converted to snake_case
+ *
* @return array
*/
- public function toArray($values = []) {
+ public function toArray() {
+ // Anonymous class will only have access to public vars
+ $getter = new class {
+
+ function getPublicVars($object) {
+ return get_object_vars($object);
+ }
+
+ };
+
+ // If getOptions was never called, make options a boolean
+ if (!isset($this->options)) {
+ $this->options = isset($this->optionsCallback);
+ }
+
$ret = [];
- foreach (get_object_vars($this) as $key => $val) {
+ foreach ($getter->getPublicVars($this) as $key => $val) {
$key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key));
- if (!$values || in_array($key, $values)) {
- $ret[$key] = $val;
- }
+ $ret[$key] = $val;
}
return $ret;
}
namespace Civi\Api4\Service\Spec;
+use Civi\Api4\Utils\CoreUtil;
use CRM_Core_DAO_AllCoreTables as AllCoreTables;
class SpecFormatter {
- /**
- * @param FieldSpec[] $fields
- * @param bool $includeFieldOptions
- * @param array $values
- *
- * @return array
- */
- public static function specToArray($fields, $includeFieldOptions = FALSE, $values = []) {
- $fieldArray = [];
-
- foreach ($fields as $field) {
- if ($includeFieldOptions) {
- $field->getOptions($values, $includeFieldOptions);
- }
- $fieldArray[$field->getName()] = $field->toArray();
- }
-
- return $fieldArray;
- }
-
/**
* @param array $data
* @param string $entity
$field->setLabel($data['custom_group.title'] . ': ' . $data['label']);
$field->setHelpPre($data['help_pre'] ?? NULL);
$field->setHelpPost($data['help_post'] ?? NULL);
- $field->setOptions(self::customFieldHasOptions($data));
+ if (self::customFieldHasOptions($data)) {
+ $field->setOptionsCallback([__CLASS__, 'getOptions']);
+ }
$field->setreadonly($data['is_view']);
}
else {
$field->setRequired(!empty($data['required']));
$field->setTitle($data['title'] ?? NULL);
$field->setLabel($data['html']['label'] ?? NULL);
- $field->setOptions(!empty($data['pseudoconstant']));
+ if (!empty($data['pseudoconstant'])) {
+ $field->setOptionsCallback([__CLASS__, 'getOptions']);
+ }
$field->setreadonly(!empty($data['readonly']));
}
$field->setSerialize($data['serialize'] ?? NULL);
return $dataTypeName;
}
+ /**
+ * Callback function to build option lists for all DAO & custom fields.
+ *
+ * @param FieldSpec $spec
+ * @param array $values
+ * @param bool|array $returnFormat
+ * @param bool $checkPermissions
+ * @return array|false
+ */
+ public static function getOptions($spec, $values, $returnFormat, $checkPermissions) {
+ $fieldName = $spec->getName();
+
+ if ($spec instanceof CustomFieldSpec) {
+ // buildOptions relies on the custom_* type of field names
+ $fieldName = sprintf('custom_%d', $spec->getCustomFieldId());
+ }
+
+ // BAO::buildOptions returns a single-dimensional list, we call that first because of the hook contract,
+ // @see CRM_Utils_Hook::fieldOptions
+ // We then supplement the data with additional properties if requested.
+ $bao = CoreUtil::getBAOFromApiName($spec->getEntity());
+ $optionLabels = $bao::buildOptions($fieldName, NULL, $values);
+
+ if (!is_array($optionLabels) || !$optionLabels) {
+ $options = FALSE;
+ }
+ else {
+ $options = \CRM_Utils_Array::makeNonAssociative($optionLabels, 'id', 'label');
+ if (is_array($returnFormat)) {
+ self::addOptionProps($options, $spec, $bao, $fieldName, $values, $returnFormat);
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * Augment the 2 values returned by BAO::buildOptions (id, label) with extra properties (name, description, color, icon, etc).
+ *
+ * We start with BAO::buildOptions in order to respect hooks which may be adding/removing items, then we add the extra data.
+ *
+ * @param array $options
+ * @param FieldSpec $spec
+ * @param \CRM_Core_DAO $baoName
+ * @param string $fieldName
+ * @param array $values
+ * @param array $returnFormat
+ */
+ private static function addOptionProps(&$options, $spec, $baoName, $fieldName, $values, $returnFormat) {
+ // FIXME: For now, call the buildOptions function again and then combine the arrays. Not an ideal approach.
+ // TODO: Teach CRM_Core_Pseudoconstant to always load multidimensional option lists so we can get more properties like 'color' and 'icon',
+ // however that might require a change to the hook_civicrm_fieldOptions signature so that's a bit tricky.
+ if (in_array('name', $returnFormat)) {
+ $props['name'] = $baoName::buildOptions($fieldName, 'validate', $values);
+ }
+ $returnFormat = array_diff($returnFormat, ['id', 'name', 'label']);
+ // CRM_Core_Pseudoconstant doesn't know how to fetch extra stuff like icon, description, color, etc., so we have to invent that wheel here...
+ if ($returnFormat) {
+ $optionIds = implode(',', array_column($options, 'id'));
+ $optionIndex = array_flip(array_column($options, 'id'));
+ if ($spec instanceof CustomFieldSpec) {
+ $optionGroupId = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', $spec->getCustomFieldId(), 'option_group_id');
+ }
+ else {
+ $dao = new $baoName();
+ $fieldSpec = $dao->getFieldSpec($fieldName);
+ $pseudoconstant = $fieldSpec['pseudoconstant'] ?? NULL;
+ $optionGroupName = $pseudoconstant['optionGroupName'] ?? NULL;
+ $optionGroupId = $optionGroupName ? \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroupName, 'id', 'name') : NULL;
+ }
+ if (!empty($optionGroupId)) {
+ $extraStuff = \CRM_Core_BAO_OptionValue::getOptionValuesArray($optionGroupId);
+ $keyColumn = $pseudoconstant['keyColumn'] ?? 'value';
+ foreach ($extraStuff as $item) {
+ if (isset($optionIndex[$item[$keyColumn]])) {
+ foreach ($returnFormat as $ret) {
+ // Note: our schema is inconsistent about whether `description` fields allow html,
+ // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
+ $options[$optionIndex[$item[$keyColumn]]][$ret] = ($ret === 'description' && isset($item[$ret])) ? strip_tags($item[$ret]) : $item[$ret] ?? NULL;
+ }
+ }
+ }
+ }
+ else {
+ // Fetch the abbr if requested using context: abbreviate
+ if (in_array('abbr', $returnFormat)) {
+ $props['abbr'] = $baoName::buildOptions($fieldName, 'abbreviate', $values);
+ $returnFormat = array_diff($returnFormat, ['abbr']);
+ }
+ // Fetch anything else (color, icon, description)
+ if ($returnFormat && !empty($pseudoconstant['table']) && \CRM_Utils_Rule::commaSeparatedIntegers($optionIds)) {
+ $sql = "SELECT * FROM {$pseudoconstant['table']} WHERE id IN (%1)";
+ $query = \CRM_Core_DAO::executeQuery($sql, [1 => [$optionIds, 'CommaSeparatedIntegers']]);
+ while ($query->fetch()) {
+ foreach ($returnFormat as $ret) {
+ if (property_exists($query, $ret)) {
+ // Note: our schema is inconsistent about whether `description` fields allow html,
+ // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
+ $options[$optionIndex[$query->id]][$ret] = $ret === 'description' ? strip_tags($query->$ret) : $query->$ret;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (isset($props)) {
+ foreach ($options as &$option) {
+ foreach ($props as $name => $prop) {
+ $option[$name] = $prop[$option['id']] ?? NULL;
+ }
+ }
+ }
+ }
+
/**
* @param \Civi\Api4\Service\Spec\FieldSpec $fieldSpec
* @param array $data
namespace api\v4\Action;
use api\v4\UnitTestCase;
+use Civi\Api4\Contact;
/**
* @group headless
*/
class GetFieldsTest extends UnitTestCase {
+ public function testOptionsAreReturned() {
+ $fields = Contact::getFields(FALSE)
+ ->execute()
+ ->indexBy('name');
+ $this->assertTrue($fields['gender_id']['options']);
+ $this->assertFalse($fields['first_name']['options']);
+
+ $fields = Contact::getFields(FALSE)
+ ->setLoadOptions(TRUE)
+ ->execute()
+ ->indexBy('name');
+ $this->assertTrue(is_array($fields['gender_id']['options']));
+ $this->assertFalse($fields['first_name']['options']);
+ }
+
public function testComponentFields() {
\CRM_Core_BAO_ConfigSetting::disableComponent('CiviCampaign');
$fields = \Civi\Api4\Event::getFields()
namespace api\v4\Spec;
use Civi\Api4\Service\Spec\CustomFieldSpec;
-use Civi\Api4\Service\Spec\FieldSpec;
-use Civi\Api4\Service\Spec\RequestSpec;
use Civi\Api4\Service\Spec\SpecFormatter;
use api\v4\UnitTestCase;
*/
class SpecFormatterTest extends UnitTestCase {
- public function testSpecToArray() {
- $spec = new RequestSpec('Contact', 'get');
- $fieldName = 'last_name';
- $field = new FieldSpec($fieldName, 'Contact');
- $spec->addFieldSpec($field);
- $arraySpec = SpecFormatter::specToArray($spec->getFields());
-
- $this->assertEquals('String', $arraySpec[$fieldName]['data_type']);
- }
-
/**
* @dataProvider arrayFieldSpecProvider
*