From 961e974cb7faf9f3d43482747b32cb94661f21a9 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 20 Apr 2020 09:00:07 -0400 Subject: [PATCH] APIv4 - Support pseudoconstant lookups Appending a suffix to a field name e.g. location_type_id:name or gender_id:label will automatically lookup values from field option lists. --- Civi/Api4/Generic/AbstractAction.php | 37 ++++ Civi/Api4/Generic/AbstractGetAction.php | 4 +- Civi/Api4/Generic/BasicCreateAction.php | 1 + Civi/Api4/Generic/BasicGetAction.php | 29 +++ Civi/Api4/Generic/BasicSaveAction.php | 9 +- Civi/Api4/Generic/BasicUpdateAction.php | 1 + Civi/Api4/Generic/DAOCreateAction.php | 1 + Civi/Api4/Generic/DAOSaveAction.php | 1 + Civi/Api4/Generic/DAOUpdateAction.php | 1 + .../Generic/Traits/ArrayQueryActionTrait.php | 9 + Civi/Api4/Generic/Traits/DAOActionTrait.php | 6 + Civi/Api4/Query/Api4SelectQuery.php | 11 +- Civi/Api4/Utils/FormattingUtil.php | 123 ++++++++++-- .../api/v4/Action/BasicActionsTest.php | 23 ++- .../phpunit/api/v4/Action/CustomValueTest.php | 21 ++- .../api/v4/Action/PseudoconstantTest.php | 177 ++++++++++++++++++ .../api/v4/Mock/Api4/MockBasicEntity.php | 7 +- 17 files changed, 424 insertions(+), 37 deletions(-) create mode 100644 tests/phpunit/api/v4/Action/PseudoconstantTest.php diff --git a/Civi/Api4/Generic/AbstractAction.php b/Civi/Api4/Generic/AbstractAction.php index 0de9856f63..905c9a1911 100644 --- a/Civi/Api4/Generic/AbstractAction.php +++ b/Civi/Api4/Generic/AbstractAction.php @@ -20,6 +20,7 @@ namespace Civi\Api4\Generic; +use Civi\Api4\Utils\FormattingUtil; use Civi\Api4\Utils\ReflectionUtils; /** @@ -470,6 +471,42 @@ abstract class AbstractAction implements \ArrayAccess { return $unmatched; } + /** + * Replaces pseudoconstants in input values + * + * @param array $record + * @throws \API_Exception + */ + protected function formatWriteValues(&$record) { + $optionFields = []; + // Collect fieldnames with a :pseudoconstant suffix & remove them from $record array + foreach (array_keys($record) as $expr) { + $suffix = strrpos($expr, ':'); + if ($suffix) { + $fieldName = substr($expr, 0, $suffix); + $field = $this->entityFields()[$fieldName] ?? NULL; + if ($field) { + $optionFields[$fieldName] = [ + 'val' => $record[$expr], + 'name' => empty($field['custom_field_id']) ? $field['name'] : 'custom_' . $field['custom_field_id'], + 'suffix' => substr($expr, $suffix + 1), + 'depends' => $field['input_attrs']['controlField'] ?? NULL, + ]; + unset($record[$expr]); + } + } + } + // Sort option lookups by dependency, so e.g. country_id is processed first, then state_province_id, then county_id + uasort($optionFields, function ($a, $b) { + return $a['name'] === $b['depends'] ? -1 : 1; + }); + // Replace pseudoconstants. Note this is a reverse lookup as we are evaluating input not output. + foreach ($optionFields as $fieldName => $info) { + $options = FormattingUtil::getPseudoconstantList($this->_entityName, $info['name'], $info['suffix'], $record, 'create'); + $record[$fieldName] = FormattingUtil::replacePseudoconstant($options, $info['val'], TRUE); + } + } + /** * This function is used internally for evaluating field annotations. * diff --git a/Civi/Api4/Generic/AbstractGetAction.php b/Civi/Api4/Generic/AbstractGetAction.php index dc020f4af1..1e0d547702 100644 --- a/Civi/Api4/Generic/AbstractGetAction.php +++ b/Civi/Api4/Generic/AbstractGetAction.php @@ -120,7 +120,7 @@ abstract class AbstractGetAction extends AbstractQueryAction { * Checks the SELECT, WHERE and ORDER BY params to see what fields are needed. * * Note that if no SELECT clause has been set then all fields should be selected - * and this function will always return TRUE. + * and this function will return TRUE for field expressions that don't contain a :pseudoconstant suffix. * * @param string ...$fieldNames * One or more field names to check (uses OR if multiple) @@ -128,7 +128,7 @@ abstract class AbstractGetAction extends AbstractQueryAction { * Returns true if any given fields are in use. */ protected function _isFieldSelected(string ...$fieldNames) { - if (!$this->select || array_intersect($fieldNames, array_merge($this->select, array_keys($this->orderBy)))) { + if ((!$this->select && strpos($fieldNames[0], ':') === FALSE) || array_intersect($fieldNames, array_merge($this->select, array_keys($this->orderBy)))) { return TRUE; } return $this->_whereContains($fieldNames); diff --git a/Civi/Api4/Generic/BasicCreateAction.php b/Civi/Api4/Generic/BasicCreateAction.php index 66f4e1b50f..7fe0f301da 100644 --- a/Civi/Api4/Generic/BasicCreateAction.php +++ b/Civi/Api4/Generic/BasicCreateAction.php @@ -58,6 +58,7 @@ class BasicCreateAction extends AbstractCreateAction { * @param \Civi\Api4\Generic\Result $result */ public function _run(Result $result) { + $this->formatWriteValues($this->values); $this->validateValues(); $result->exchangeArray([$this->writeRecord($this->values)]); } diff --git a/Civi/Api4/Generic/BasicGetAction.php b/Civi/Api4/Generic/BasicGetAction.php index 22278c973e..22d37c2e2c 100644 --- a/Civi/Api4/Generic/BasicGetAction.php +++ b/Civi/Api4/Generic/BasicGetAction.php @@ -22,6 +22,7 @@ namespace Civi\Api4\Generic; use Civi\API\Exception\NotImplementedException; +use Civi\Api4\Utils\FormattingUtil; /** * Retrieve $ENTITIES based on criteria specified in the `where` parameter. @@ -59,6 +60,7 @@ class BasicGetAction extends AbstractGetAction { $this->setDefaultWhereClause(); $this->expandSelectClauseWildcards(); $values = $this->getRecords(); + $this->formatRawValues($values); $result->exchangeArray($this->queryArray($values)); } @@ -102,4 +104,31 @@ class BasicGetAction extends AbstractGetAction { throw new NotImplementedException('Getter function not found for api4 ' . $this->getEntityName() . '::' . $this->getActionName()); } + /** + * Evaluate :pseudoconstant suffix expressions and replace raw values with option values + * + * @param $records + * @throws \API_Exception + * @throws \CRM_Core_Exception + */ + protected function formatRawValues(&$records) { + // Pad $records and $fields with pseudofields + $fields = $this->entityFields(); + foreach ($records as &$values) { + foreach ($this->entityFields() as $field) { + if (!empty($field['options'])) { + foreach (array_keys(FormattingUtil::$pseudoConstantContexts) as $suffix) { + $pseudofield = $field['name'] . ':' . $suffix; + if (!isset($values[$pseudofield]) && isset($values[$field['name']]) && $this->_isFieldSelected($pseudofield)) { + $values[$pseudofield] = $values[$field['name']]; + $fields[$pseudofield] = $field; + } + } + } + } + } + // Swap raw values with pseudoconstants + FormattingUtil::formatOutputValues($records, $fields, $this->getEntityName(), $this->getActionName()); + } + } diff --git a/Civi/Api4/Generic/BasicSaveAction.php b/Civi/Api4/Generic/BasicSaveAction.php index a080fa6d82..855980cbdd 100644 --- a/Civi/Api4/Generic/BasicSaveAction.php +++ b/Civi/Api4/Generic/BasicSaveAction.php @@ -60,10 +60,13 @@ class BasicSaveAction extends AbstractSaveAction { * @param \Civi\Api4\Generic\Result $result */ public function _run(Result $result) { - $this->validateValues(); - foreach ($this->records as $record) { + foreach ($this->records as &$record) { $record += $this->defaults; - $result[] = $this->writeRecord($record); + $this->formatWriteValues($record); + } + $this->validateValues(); + foreach ($this->records as $item) { + $result[] = $this->writeRecord($item); } if ($this->reload) { /** @var BasicGetAction $get */ diff --git a/Civi/Api4/Generic/BasicUpdateAction.php b/Civi/Api4/Generic/BasicUpdateAction.php index a7c8865f8c..ade3f0da1b 100644 --- a/Civi/Api4/Generic/BasicUpdateAction.php +++ b/Civi/Api4/Generic/BasicUpdateAction.php @@ -61,6 +61,7 @@ class BasicUpdateAction extends AbstractUpdateAction { * @throws \Civi\API\Exception\NotImplementedException */ public function _run(Result $result) { + $this->formatWriteValues($this->values); foreach ($this->getBatchRecords() as $item) { $result[] = $this->writeRecord($this->values + $item); } diff --git a/Civi/Api4/Generic/DAOCreateAction.php b/Civi/Api4/Generic/DAOCreateAction.php index c90b81116d..c65a91f884 100644 --- a/Civi/Api4/Generic/DAOCreateAction.php +++ b/Civi/Api4/Generic/DAOCreateAction.php @@ -34,6 +34,7 @@ class DAOCreateAction extends AbstractCreateAction { * @inheritDoc */ public function _run(Result $result) { + $this->formatWriteValues($this->values); $this->validateValues(); $params = $this->values; $this->fillDefaults($params); diff --git a/Civi/Api4/Generic/DAOSaveAction.php b/Civi/Api4/Generic/DAOSaveAction.php index 954ecae5ba..1f0dead375 100644 --- a/Civi/Api4/Generic/DAOSaveAction.php +++ b/Civi/Api4/Generic/DAOSaveAction.php @@ -37,6 +37,7 @@ class DAOSaveAction extends AbstractSaveAction { public function _run(Result $result) { foreach ($this->records as &$record) { $record += $this->defaults; + $this->formatWriteValues($record); if (empty($record['id'])) { $this->fillDefaults($record); } diff --git a/Civi/Api4/Generic/DAOUpdateAction.php b/Civi/Api4/Generic/DAOUpdateAction.php index 5fe1cda280..d039b6e144 100644 --- a/Civi/Api4/Generic/DAOUpdateAction.php +++ b/Civi/Api4/Generic/DAOUpdateAction.php @@ -42,6 +42,7 @@ class DAOUpdateAction extends AbstractUpdateAction { * @inheritDoc */ public function _run(Result $result) { + $this->formatWriteValues($this->values); // Add ID from values to WHERE clause and check for mismatch if (!empty($this->values['id'])) { $wheres = array_column($this->where, NULL, 0); diff --git a/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php b/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php index baddfe1a09..fc7f21229b 100644 --- a/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php +++ b/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php @@ -199,10 +199,19 @@ trait ArrayQueryActionTrait { $values = [['row_count' => count($values)]]; } elseif ($this->getSelect()) { + // Return only fields specified by SELECT foreach ($values as &$value) { $value = array_intersect_key($value, array_flip($this->getSelect())); } } + else { + // With no SELECT specified, return all values that are keyed by plain field name; omit those with :pseudoconstant suffixes + foreach ($values as &$value) { + $value = array_filter($value, function($key) { + return strpos($key, ':') === FALSE; + }, ARRAY_FILTER_USE_KEY); + } + } return $values; } diff --git a/Civi/Api4/Generic/Traits/DAOActionTrait.php b/Civi/Api4/Generic/Traits/DAOActionTrait.php index b8cb994feb..f4ae2eda0a 100644 --- a/Civi/Api4/Generic/Traits/DAOActionTrait.php +++ b/Civi/Api4/Generic/Traits/DAOActionTrait.php @@ -175,6 +175,7 @@ trait DAOActionTrait { } list($customGroup, $customField) = explode('.', $name); + list($customField, $option) = array_pad(explode(':', $customField), 2, NULL); $customFieldId = \CRM_Core_BAO_CustomField::getFieldValue( \CRM_Core_DAO_CustomField::class, @@ -198,6 +199,11 @@ trait DAOActionTrait { // todo are we sure we don't want to allow setting to NULL? need to test if ($customFieldId && NULL !== $value) { + if ($option) { + $options = FormattingUtil::getPseudoconstantList($this->getEntityName(), 'custom_' . $customFieldId, $option, $params, $this->getActionName()); + $value = FormattingUtil::replacePseudoconstant($options, $value, TRUE); + } + if ($customFieldType == 'CheckBox') { // this function should be part of a class formatCheckBoxField($value, 'custom_' . $customFieldId, $this->getEntityName()); diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php index 5bc18313b5..7ee2985c52 100644 --- a/Civi/Api4/Query/Api4SelectQuery.php +++ b/Civi/Api4/Query/Api4SelectQuery.php @@ -321,7 +321,7 @@ class Api4SelectQuery extends SelectQuery { // For WHERE clause, expr must be the name of a field. if ($type === 'WHERE') { $field = $this->getField($expr, TRUE); - FormattingUtil::formatInputValue($value, $field, $this->getEntity()); + FormattingUtil::formatInputValue($value, $expr, $field, $this->getEntity()); $fieldAlias = $field['sql_name']; } // For HAVING, expr must be an item in the SELECT clause @@ -354,14 +354,18 @@ class Api4SelectQuery extends SelectQuery { /** * Fetch a field from the getFields list * - * @param string $fieldName + * @param string $expr * @param bool $strict * In strict mode, this will throw an exception if the field doesn't exist * * @return string|null * @throws \API_Exception */ - public function getField($fieldName, $strict = FALSE) { + public function getField($expr, $strict = FALSE) { + // If the expression contains a pseudoconstant filter like activity_type_id:label, + // strip it to look up the base field name, then add the field:filter key to apiFieldSpec + $col = strpos($expr, ':'); + $fieldName = $col ? substr($expr, 0, $col) : $expr; // Perform join if field not yet available - this will add it to apiFieldSpec if (!isset($this->apiFieldSpec[$fieldName]) && strpos($fieldName, '.')) { $this->joinFK($fieldName); @@ -370,6 +374,7 @@ class Api4SelectQuery extends SelectQuery { if ($strict && !$field) { throw new \API_Exception("Invalid field '$fieldName'"); } + $this->apiFieldSpec[$expr] = $field; return $field; } diff --git a/Civi/Api4/Utils/FormattingUtil.php b/Civi/Api4/Utils/FormattingUtil.php index b4a55d493c..89f4c7c54b 100644 --- a/Civi/Api4/Utils/FormattingUtil.php +++ b/Civi/Api4/Utils/FormattingUtil.php @@ -25,6 +25,12 @@ require_once 'api/v3/utils.php'; class FormattingUtil { + public static $pseudoConstantContexts = [ + 'name' => 'validate', + 'abbr' => 'abbreviate', + 'label' => 'get', + ]; + /** * Massage values into the format the BAO expects for a write operation * @@ -41,7 +47,7 @@ class FormattingUtil { if ($value === 'null') { $value = 'Null'; } - self::formatInputValue($value, $field, $entity); + self::formatInputValue($value, $name, $field, $entity); // Ensure we have an array for serialized fields if (!empty($field['serialize'] && !is_array($value))) { $value = (array) $value; @@ -73,15 +79,22 @@ class FormattingUtil { * This is used by read AND write actions (Get, Create, Update, Replace) * * @param $value - * @param $fieldSpec + * @param string $fieldName + * @param array $fieldSpec * @param string $entity * Ex: 'Contact', 'Domain' * @throws \API_Exception */ - public static function formatInputValue(&$value, $fieldSpec, $entity) { - if (is_array($value)) { + public static function formatInputValue(&$value, $fieldName, $fieldSpec, $entity) { + // Evaluate pseudoconstant suffix + $suffix = strpos($fieldName, ':'); + if ($suffix) { + $options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1)); + $value = self::replacePseudoconstant($options, $value, TRUE); + } + elseif (is_array($value)) { foreach ($value as &$val) { - self::formatInputValue($val, $fieldSpec, $entity); + self::formatInputValue($val, $fieldName, $fieldSpec, $entity); } return; } @@ -120,29 +133,98 @@ class FormattingUtil { * @param array $results * @param array $fields * @param string $entity + * @param string $action + * @throws \API_Exception * @throws \CRM_Core_Exception */ - public static function formatOutputValues(&$results, $fields, $entity) { + public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') { + $fieldOptions = []; foreach ($results as &$result) { // Remove inapplicable contact fields if ($entity === 'Contact' && !empty($result['contact_type'])) { \CRM_Utils_Array::remove($result, self::contactFieldsToRemove($result['contact_type'])); } - foreach ($result as $field => $value) { - $dataType = $fields[$field]['data_type'] ?? ($field == 'id' ? 'Integer' : NULL); - if (!empty($fields[$field]['serialize'])) { - if (is_string($value)) { - $result[$field] = $value = \CRM_Core_DAO::unSerializeField($value, $fields[$field]['serialize']); - foreach ($value as $key => $val) { - $result[$field][$key] = self::convertDataType($val, $dataType); + foreach ($result as $fieldExpr => $value) { + $field = $fields[$fieldExpr] ?? NULL; + $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL); + if ($field) { + // Evaluate pseudoconstant suffixes + $suffix = strrpos($fieldExpr, ':'); + if ($suffix) { + $fieldName = empty($field['custom_field_id']) ? $field['name'] : 'custom_' . $field['custom_field_id']; + $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field['entity'], $fieldName, substr($fieldExpr, $suffix + 1), $result, $action); + $dataType = NULL; + } + if (!empty($field['serialize'])) { + if (is_string($value)) { + $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']); } } + if (isset($fieldOptions[$fieldExpr])) { + $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value); + } } - else { - $result[$field] = self::convertDataType($value, $dataType); - } + $result[$fieldExpr] = self::convertDataType($value, $dataType); + } + } + } + + /** + * Retrieves pseudoconstant option list for a field. + * + * @param string $entity + * Name of api entity + * @param string $fieldName + * @param string $optionValue + * @param array $params + * Other values for this object + * @param string $action + * @return array + * @throws \API_Exception + */ + public static function getPseudoconstantList($entity, $fieldName, $optionValue, $params = [], $action = 'get') { + $context = self::$pseudoConstantContexts[$optionValue] ?? NULL; + if (!$context) { + throw new \API_Exception('Illegal expression'); + } + $baoName = CoreUtil::getBAOFromApiName($entity); + // Use BAO::buildOptions if possible + if ($baoName) { + $options = $baoName::buildOptions($fieldName, $context, $params); + } + // Fallback for non-bao based entities + if (!isset($options)) { + $options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => TRUE, 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL; + } + if (is_array($options)) { + return $options; + } + throw new \API_Exception("No option list found for '$fieldName'"); + } + + /** + * Replaces value (or an array of values) with options from a pseudoconstant list. + * + * The direction of lookup defaults to transforming ids to option values for api output; + * for api input, set $reverse = TRUE to transform option values to ids. + * + * @param array $options + * @param string|string[] $value + * @param bool $reverse + * Is this a reverse lookup (for transforming input instead of output) + * @return array|mixed|null + */ + public static function replacePseudoconstant($options, $value, $reverse = FALSE) { + $matches = []; + foreach ((array) $value as $val) { + if (!$reverse && isset($options[$val])) { + $matches[] = $options[$val]; + } + elseif ($reverse && array_search($val, $options) !== FALSE) { + $matches[] = array_search($val, $options); } } + return is_array($value) ? $matches : $matches[0] ?? NULL; } /** @@ -151,7 +233,14 @@ class FormattingUtil { * @return mixed */ public static function convertDataType($value, $dataType) { - if (isset($value)) { + if (isset($value) && $dataType) { + if (is_array($value)) { + foreach ($value as $key => $val) { + $value[$key] = self::convertDataType($val, $dataType); + } + return $value; + } + switch ($dataType) { case 'Boolean': return (bool) $value; diff --git a/tests/phpunit/api/v4/Action/BasicActionsTest.php b/tests/phpunit/api/v4/Action/BasicActionsTest.php index 21b181f7d4..27ba63953a 100644 --- a/tests/phpunit/api/v4/Action/BasicActionsTest.php +++ b/tests/phpunit/api/v4/Action/BasicActionsTest.php @@ -49,22 +49,39 @@ class BasicActionsTest extends UnitTestCase { $this->assertEquals('new', $result->first()['foo']); $result = MockBasicEntity::save() - ->addRecord(['id' => $id1, 'foo' => 'one updated']) - ->addRecord(['id' => $id2]) + ->addRecord(['id' => $id1, 'foo' => 'one updated', 'weight' => '5']) + ->addRecord(['id' => $id2, 'group:label' => 'Second']) ->addRecord(['foo' => 'three']) ->addDefault('color', 'pink') ->setReload(TRUE) ->execute() ->indexBy('id'); + $this->assertTrue(5 === $result[$id1]['weight']); $this->assertEquals('new', $result[$id2]['foo']); + $this->assertEquals('two', $result[$id2]['group']); $this->assertEquals('three', $result->last()['foo']); $this->assertCount(3, $result); foreach ($result as $item) { $this->assertEquals('pink', $item['color']); } - $this->assertEquals('one updated', MockBasicEntity::get()->addWhere('id', '=', $id1)->execute()->first()['foo']); + $ent1 = MockBasicEntity::get()->addWhere('id', '=', $id1)->execute()->first(); + $this->assertEquals('one updated', $ent1['foo']); + $this->assertFalse(isset($ent1['group:label'])); + + $ent2 = MockBasicEntity::get()->addWhere('group:label', '=', 'Second')->addSelect('group:label', 'group')->execute()->first(); + $this->assertEquals('two', $ent2['group']); + $this->assertEquals('Second', $ent2['group:label']); + // We didn't select this + $this->assertFalse(isset($ent2['group:name'])); + + // With no SELECT, all fields should be returned but not suffixy stuff like group:name + $ent2 = MockBasicEntity::get()->addWhere('group:label', '=', 'Second')->execute()->first(); + $this->assertEquals('two', $ent2['group']); + $this->assertFalse(isset($ent2['group:name'])); + // This one wasn't selected but did get used by the WHERE clause; ensure it isn't returned + $this->assertFalse(isset($ent2['group:label'])); MockBasicEntity::delete()->addWhere('id', '=', $id2); $result = MockBasicEntity::get()->execute(); diff --git a/tests/phpunit/api/v4/Action/CustomValueTest.php b/tests/phpunit/api/v4/Action/CustomValueTest.php index 4286f618dd..2c7e85b6f4 100644 --- a/tests/phpunit/api/v4/Action/CustomValueTest.php +++ b/tests/phpunit/api/v4/Action/CustomValueTest.php @@ -55,7 +55,7 @@ class CustomValueTest extends BaseCustomValueTest { CustomField::create() ->setCheckPermissions(FALSE) ->addValue('label', $colorField) - ->addValue('options', $optionValues) + ->addValue('option_values', $optionValues) ->addValue('custom_group_id', $customGroup['id']) ->addValue('html_type', 'Select') ->addValue('data_type', 'String') @@ -64,7 +64,7 @@ class CustomValueTest extends BaseCustomValueTest { CustomField::create() ->setCheckPermissions(FALSE) ->addValue('label', $multiField) - ->addValue('options', $optionValues) + ->addValue('option_values', $optionValues) ->addValue('custom_group_id', $customGroup['id']) ->addValue('html_type', 'CheckBox') ->addValue('data_type', 'String') @@ -87,7 +87,7 @@ class CustomValueTest extends BaseCustomValueTest { ->first()['id']; // Retrieve and check the fields of CustomValue = Custom_$group - $fields = CustomValue::getFields($group)->execute(); + $fields = CustomValue::getFields($group)->setLoadOptions(TRUE)->setCheckPermissions(FALSE)->execute(); $expectedResult = [ [ 'custom_group' => $group, @@ -97,6 +97,7 @@ class CustomValueTest extends BaseCustomValueTest { 'data_type' => 'String', 'fk_entity' => NULL, 'serialize' => NULL, + 'options' => $optionValues, ], [ 'custom_group' => $group, @@ -106,6 +107,7 @@ class CustomValueTest extends BaseCustomValueTest { 'data_type' => 'String', 'fk_entity' => NULL, 'serialize' => 1, + 'options' => $optionValues, ], [ 'custom_group' => $group, @@ -150,6 +152,7 @@ class CustomValueTest extends BaseCustomValueTest { ->execute(); // fetch custom values using API4 CustomValue::get $result = CustomValue::get($group) + ->addSelect('id', 'entity_id', $colorField, $colorField . ':label') ->addOrderBy($colorField, 'DESC') ->execute(); @@ -159,11 +162,13 @@ class CustomValueTest extends BaseCustomValueTest { [ 'id' => 2, $colorField => 'r', + $colorField . ':label' => 'Red', 'entity_id' => $this->contactID, ], [ 'id' => 1, $colorField => 'g', + $colorField . ':label' => 'Green', 'entity_id' => $this->contactID, ], ]; @@ -178,7 +183,7 @@ class CustomValueTest extends BaseCustomValueTest { // Update a records whose id is 1 and change the custom field (name = Color) value to 'Blue' from 'Green' CustomValue::update($group) ->addWhere("id", "=", 1) - ->addValue($colorField, 'b') + ->addValue($colorField . ':label', 'Blue') ->execute(); // ensure that the value is changed for id = 1 @@ -200,19 +205,23 @@ class CustomValueTest extends BaseCustomValueTest { // Replace all the records which was created earlier with entity_id = first contact // with custom record [$colorField => 'g', 'entity_id' => $secondContactID] CustomValue::replace($group) - ->setRecords([[$colorField => 'g', $multiField => ['r', 'g'], 'entity_id' => $secondContactID]]) + ->setRecords([[$colorField => 'g', $multiField . ':label' => ['Red', 'Green'], 'entity_id' => $secondContactID]]) ->addWhere('entity_id', '=', $this->contactID) ->execute(); // Check the two records created earlier is replaced by new contact - $result = CustomValue::get($group)->execute(); + $result = CustomValue::get($group) + ->addSelect('id', 'entity_id', $colorField, $colorField . ':label', $multiField, $multiField . ':label') + ->execute(); $this->assertEquals(1, count($result)); $expectedResult = [ [ 'id' => 3, $colorField => 'g', + $colorField . ':label' => 'Green', $multiField => ['r', 'g'], + $multiField . ':label' => ['Red', 'Green'], 'entity_id' => $secondContactID, ], ]; diff --git a/tests/phpunit/api/v4/Action/PseudoconstantTest.php b/tests/phpunit/api/v4/Action/PseudoconstantTest.php new file mode 100644 index 0000000000..e6cab2de67 --- /dev/null +++ b/tests/phpunit/api/v4/Action/PseudoconstantTest.php @@ -0,0 +1,177 @@ +setCheckPermissions(FALSE)->addValue('first_name', 'bill')->execute()->first()['id']; + $subject = uniqid('subject'); + OptionValue::create() + ->addValue('option_group_id:name', 'activity_type') + ->addValue('label', 'Fake') + ->execute(); + + Activity::create() + ->addValue('activity_type_id:name', 'Fake') + ->addValue('source_contact_id', $cid) + ->addValue('subject', $subject) + ->execute(); + + $act = Activity::get() + ->addWhere('activity_type_id:name', '=', 'Fake') + ->addWhere('subject', '=', $subject) + ->addSelect('activity_type_id:label') + ->addSelect('activity_type_id') + ->execute(); + + $this->assertCount(1, $act); + $this->assertEquals('Fake', $act[0]['activity_type_id:label']); + $this->assertTrue(is_numeric($act[0]['activity_type_id'])); + } + + public function testAddressOptions() { + $cid = Contact::create()->setCheckPermissions(FALSE)->addValue('first_name', 'addr')->execute()->first()['id']; + Address::save() + ->addRecord([ + 'contact_id' => $cid, + 'state_province_id:abbr' => 'CA', + 'country_id:label' => 'United States', + 'street_address' => '1', + ]) + ->addRecord([ + 'contact_id' => $cid, + 'state_province_id:abbr' => 'CA', + 'country_id:label' => 'Uruguay', + 'street_address' => '2', + ]) + ->addRecord([ + 'contact_id' => $cid, + 'state_province_id:abbr' => 'CA', + 'country_id:abbr' => 'ES', + 'street_address' => '3', + ]) + ->execute(); + + $addr = Address::get() + ->addWhere('contact_id', '=', $cid) + ->addSelect('state_province_id:abbr', 'state_province_id:name', 'country_id:label', 'country_id:name') + ->addOrderBy('street_address') + ->execute(); + + $this->assertCount(3, $addr); + + // US - California + $this->assertEquals('CA', $addr[0]['state_province_id:abbr']); + $this->assertEquals('California', $addr[0]['state_province_id:name']); + $this->assertEquals('US', $addr[0]['country_id:name']); + $this->assertEquals('United States', $addr[0]['country_id:label']); + // Uruguay - Canelones + $this->assertEquals('CA', $addr[1]['state_province_id:abbr']); + $this->assertEquals('Canelones', $addr[1]['state_province_id:name']); + $this->assertEquals('UY', $addr[1]['country_id:name']); + $this->assertEquals('Uruguay', $addr[1]['country_id:label']); + // Spain - Cádiz + $this->assertEquals('CA', $addr[2]['state_province_id:abbr']); + $this->assertEquals('Cádiz', $addr[2]['state_province_id:name']); + $this->assertEquals('ES', $addr[2]['country_id:name']); + $this->assertEquals('Spain', $addr[2]['country_id:label']); + } + + public function testCustomOptions() { + CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'myPseudoconstantTest') + ->addValue('extends', 'Individual') + ->addChain('field', CustomField::create() + ->addValue('custom_group_id', '$id') + ->addValue('option_values', ['r' => 'red', 'g' => 'green', 'b' => 'blue']) + ->addValue('label', 'Color') + ->addValue('html_type', 'Select') + )->execute(); + + $cid = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'col') + ->addValue('myPseudoconstantTest.Color:label', 'blue') + ->execute()->first()['id']; + + $result = Contact::get() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $cid) + ->addSelect('myPseudoconstantTest.Color:label', 'myPseudoconstantTest.Color') + ->execute()->first(); + + $this->assertEquals('blue', $result['myPseudoconstantTest.Color:label']); + $this->assertEquals('b', $result['myPseudoconstantTest.Color']); + } + + public function testJoinOptions() { + $cid1 = Contact::create()->setCheckPermissions(FALSE) + ->addValue('first_name', 'Tom') + ->addValue('gender_id:label', 'Male') + ->addChain('email', Email::create()->setValues(['contact_id' => '$id', 'email' => 'tom@example.com', 'location_type_id:name' => 'Work'])) + ->execute()->first()['id']; + $cid2 = Contact::create()->setCheckPermissions(FALSE) + ->addValue('first_name', 'Sue') + ->addValue('gender_id:name', 'Female') + ->addChain('email', Email::create()->setValues(['contact_id' => '$id', 'email' => 'sue@example.com', 'location_type_id:name' => 'Home'])) + ->execute()->first()['id']; + $cid3 = Contact::create()->setCheckPermissions(FALSE) + ->addValue('first_name', 'Pat') + ->addChain('email', Email::create()->setValues(['contact_id' => '$id', 'email' => 'pat@example.com', 'location_type_id:name' => 'Home'])) + ->execute()->first()['id']; + + $emails = Email::get() + ->addSelect('location_type_id:name', 'contact.gender_id:label', 'email', 'contact_id') + ->addWhere('contact_id', 'IN', [$cid1, $cid2, $cid3]) + ->addWhere('contact.gender_id:label', 'IN', ['Male', 'Female']) + ->execute()->indexBy('contact_id'); + $this->assertCount(2, $emails); + $this->assertEquals('Work', $emails[$cid1]['location_type_id:name']); + $this->assertEquals('Home', $emails[$cid2]['location_type_id:name']); + $this->assertEquals('Male', $emails[$cid1]['contact.gender_id:label']); + $this->assertEquals('Female', $emails[$cid2]['contact.gender_id:label']); + + $emails = Email::get() + ->addSelect('location_type_id:name', 'contact.gender_id:label', 'email', 'contact_id') + ->addWhere('contact_id', 'IN', [$cid1, $cid2, $cid3]) + ->addWhere('location_type_id:name', 'IN', ['Home']) + ->execute()->indexBy('contact_id'); + $this->assertCount(2, $emails); + $this->assertEquals('Home', $emails[$cid2]['location_type_id:name']); + $this->assertEquals('Home', $emails[$cid3]['location_type_id:name']); + $this->assertEquals('Female', $emails[$cid2]['contact.gender_id:label']); + $this->assertNull($emails[$cid3]['contact.gender_id:label']); + } + +} diff --git a/tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php b/tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php index 143ac4b330..f6fd57f9bf 100644 --- a/tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php +++ b/tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php @@ -38,13 +38,13 @@ class MockBasicEntity extends Generic\AbstractEntity { return [ [ 'name' => 'id', - 'type' => 'Integer', + 'data_type' => 'Integer', ], [ 'name' => 'group', 'options' => [ - 'one' => 'One', - 'two' => 'Two', + 'one' => 'First', + 'two' => 'Second', ], ], [ @@ -58,6 +58,7 @@ class MockBasicEntity extends Generic\AbstractEntity { ], [ 'name' => 'weight', + 'data_type' => 'Integer', ], ]; }); -- 2.25.1