Skip to content

Commit

Permalink
Merge pull request #17138 from colemanw/api4pseudoconstants
Browse files Browse the repository at this point in the history
dev/core#1705 APIv4 - Support pseudoconstant lookups
  • Loading branch information
eileenmcnaughton authored Apr 23, 2020
2 parents 6bcf9fe + d818aa7 commit e6590cf
Show file tree
Hide file tree
Showing 18 changed files with 489 additions and 45 deletions.
37 changes: 37 additions & 0 deletions Civi/Api4/Generic/AbstractAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

namespace Civi\Api4\Generic;

use Civi\Api4\Utils\FormattingUtil;
use Civi\Api4\Utils\ReflectionUtils;

/**
Expand Down Expand Up @@ -470,6 +471,42 @@ protected function checkRequiredFields($values) {
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.
*
Expand Down
4 changes: 2 additions & 2 deletions Civi/Api4/Generic/AbstractGetAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ protected function _itemsToGet($field) {
* 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)
* @return bool
* 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);
Expand Down
1 change: 1 addition & 0 deletions Civi/Api4/Generic/BasicCreateAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function __construct($entityName, $actionName, $setter = NULL) {
* @param \Civi\Api4\Generic\Result $result
*/
public function _run(Result $result) {
$this->formatWriteValues($this->values);
$this->validateValues();
$result->exchangeArray([$this->writeRecord($this->values)]);
}
Expand Down
29 changes: 29 additions & 0 deletions Civi/Api4/Generic/BasicGetAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -59,6 +60,7 @@ public function _run(Result $result) {
$this->setDefaultWhereClause();
$this->expandSelectClauseWildcards();
$values = $this->getRecords();
$this->formatRawValues($values);
$result->exchangeArray($this->queryArray($values));
}

Expand Down Expand Up @@ -102,4 +104,31 @@ protected function getRecords() {
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());
}

}
9 changes: 6 additions & 3 deletions Civi/Api4/Generic/BasicSaveAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ public function __construct($entityName, $actionName, $idField = 'id', $setter =
* @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 */
Expand Down
1 change: 1 addition & 0 deletions Civi/Api4/Generic/BasicUpdateAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public function __construct($entityName, $actionName, $select = 'id', $setter =
* @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);
}
Expand Down
1 change: 1 addition & 0 deletions Civi/Api4/Generic/DAOCreateAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions Civi/Api4/Generic/DAOSaveAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions Civi/Api4/Generic/DAOUpdateAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,19 @@ protected function selectArray($values) {
$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;
}

Expand Down
6 changes: 6 additions & 0 deletions Civi/Api4/Generic/Traits/DAOActionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ protected function formatCustomParams(&$params, $entityId) {
}

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,
Expand All @@ -198,6 +199,11 @@ protected function formatCustomParams(&$params, $entityId) {
// 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());
Expand Down
11 changes: 8 additions & 3 deletions Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ protected function composeClause(array $clause, string $type) {
// 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
Expand Down Expand Up @@ -354,14 +354,18 @@ protected function getFields() {
/**
* 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);
Expand All @@ -370,6 +374,7 @@ public function getField($fieldName, $strict = FALSE) {
if ($strict && !$field) {
throw new \API_Exception("Invalid field '$fieldName'");
}
$this->apiFieldSpec[$expr] = $field;
return $field;
}

Expand Down
Loading

0 comments on commit e6590cf

Please sign in to comment.