diff --git a/Civi/Api4/Generic/Traits/CustomValueActionTrait.php b/Civi/Api4/Generic/Traits/CustomValueActionTrait.php
index b35c789d0183..236cb3fc4ddc 100644
--- a/Civi/Api4/Generic/Traits/CustomValueActionTrait.php
+++ b/Civi/Api4/Generic/Traits/CustomValueActionTrait.php
@@ -57,7 +57,7 @@ protected function writeObjects($items) {
$result = [];
$fields = $this->entityFields();
foreach ($items as $item) {
- FormattingUtil::formatWriteParams($item, $this->getEntityName(), $fields);
+ FormattingUtil::formatWriteParams($item, $fields);
// Convert field names to custom_xx format
foreach ($fields as $name => $field) {
diff --git a/Civi/Api4/Generic/Traits/DAOActionTrait.php b/Civi/Api4/Generic/Traits/DAOActionTrait.php
index f4ae2eda0aaf..45b572a2526e 100644
--- a/Civi/Api4/Generic/Traits/DAOActionTrait.php
+++ b/Civi/Api4/Generic/Traits/DAOActionTrait.php
@@ -125,7 +125,7 @@ protected function writeObjects($items) {
foreach ($items as $item) {
$entityId = $item['id'] ?? NULL;
- FormattingUtil::formatWriteParams($item, $this->getEntityName(), $this->entityFields());
+ FormattingUtil::formatWriteParams($item, $this->entityFields());
$this->formatCustomParams($item, $entityId);
$item['check_permissions'] = $this->getCheckPermissions();
diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php
index 7ee2985c521a..63cba4e2807b 100644
--- a/Civi/Api4/Query/Api4SelectQuery.php
+++ b/Civi/Api4/Query/Api4SelectQuery.php
@@ -49,6 +49,7 @@ class Api4SelectQuery extends SelectQuery {
/**
* @var array
+ * [alias => expr][]
*/
protected $selectAliases = [];
@@ -321,20 +322,39 @@ 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, $expr, $field, $this->getEntity());
+ FormattingUtil::formatInputValue($value, $expr, $field);
$fieldAlias = $field['sql_name'];
}
// For HAVING, expr must be an item in the SELECT clause
else {
+ // Expr references a fieldName or alias
if (isset($this->selectAliases[$expr])) {
$fieldAlias = $expr;
+ // Attempt to format if this is a real field
+ if (isset($this->apiFieldSpec[$expr])) {
+ FormattingUtil::formatInputValue($value, $expr, $this->apiFieldSpec[$expr]);
+ }
}
+ // Expr references a non-field expression like a function; convert to alias
elseif (in_array($expr, $this->selectAliases)) {
$fieldAlias = array_search($expr, $this->selectAliases);
}
+ // If either the having or select field contains a pseudoconstant suffix, match and perform substitution
else {
- throw new \API_Exception("Invalid expression in $type clause: '$expr'. Must use a value from SELECT clause.");
+ list($fieldName) = explode(':', $expr);
+ foreach ($this->selectAliases as $selectAlias => $selectExpr) {
+ list($selectField) = explode(':', $selectAlias);
+ if ($selectAlias === $selectExpr && $fieldName === $selectField && isset($this->apiFieldSpec[$fieldName])) {
+ FormattingUtil::formatInputValue($value, $expr, $this->apiFieldSpec[$fieldName]);
+ $fieldAlias = $selectAlias;
+ break;
+ }
+ }
+ }
+ if (!isset($fieldAlias)) {
+ throw new \API_Exception("Invalid expression in HAVING clause: '$expr'. Must use a value from SELECT clause.");
}
+ $fieldAlias = '`' . $fieldAlias . '`';
}
$sql_clause = \CRM_Core_DAO::createSQLFilter($fieldAlias, [$operator => $value]);
diff --git a/Civi/Api4/Utils/FormattingUtil.php b/Civi/Api4/Utils/FormattingUtil.php
index e8f27bb5944c..be91ce41e199 100644
--- a/Civi/Api4/Utils/FormattingUtil.php
+++ b/Civi/Api4/Utils/FormattingUtil.php
@@ -34,12 +34,11 @@ class FormattingUtil {
/**
* Massage values into the format the BAO expects for a write operation
*
- * @param $params
- * @param $entity
- * @param $fields
+ * @param array $params
+ * @param array $fields
* @throws \API_Exception
*/
- public static function formatWriteParams(&$params, $entity, $fields) {
+ public static function formatWriteParams(&$params, $fields) {
foreach ($fields as $name => $field) {
if (!empty($params[$name])) {
$value =& $params[$name];
@@ -47,7 +46,7 @@ public static function formatWriteParams(&$params, $entity, $fields) {
if ($value === 'null') {
$value = 'Null';
}
- self::formatInputValue($value, $name, $field, $entity);
+ self::formatInputValue($value, $name, $field);
// Ensure we have an array for serialized fields
if (!empty($field['serialize'] && !is_array($value))) {
$value = (array) $value;
@@ -81,11 +80,9 @@ public static function formatWriteParams(&$params, $entity, $fields) {
* @param $value
* @param string $fieldName
* @param array $fieldSpec
- * @param string $entity
- * Ex: 'Contact', 'Domain'
* @throws \API_Exception
*/
- public static function formatInputValue(&$value, $fieldName, $fieldSpec, $entity) {
+ public static function formatInputValue(&$value, $fieldName, $fieldSpec) {
// Evaluate pseudoconstant suffix
$suffix = strpos($fieldName, ':');
if ($suffix) {
@@ -94,11 +91,11 @@ public static function formatInputValue(&$value, $fieldName, $fieldSpec, $entity
}
elseif (is_array($value)) {
foreach ($value as &$val) {
- self::formatInputValue($val, $fieldName, $fieldSpec, $entity);
+ self::formatInputValue($val, $fieldName, $fieldSpec);
}
return;
}
- $fk = $fieldSpec['name'] == 'id' ? $entity : $fieldSpec['fk_entity'] ?? NULL;
+ $fk = $fieldSpec['name'] == 'id' ? $fieldSpec['entity'] : $fieldSpec['fk_entity'] ?? NULL;
if ($fk === 'Domain' && $value === 'current_domain') {
$value = \CRM_Core_Config::domainID();
diff --git a/ang/api4Explorer/Explorer.html b/ang/api4Explorer/Explorer.html
index af771f914532..56d6e727963d 100644
--- a/ang/api4Explorer/Explorer.html
+++ b/ang/api4Explorer/Explorer.html
@@ -87,11 +87,11 @@