* Applies supplied filters to the where clause
*/
protected function applyFilters() {
+ // Allow all filters that are included in SELECT clause or are fields on the Afform.
+ $allowedFilters = array_merge($this->getSelectAliases(), $this->getAfformFilters());
+
// Ignore empty strings
$filters = array_filter($this->filters, [$this, 'hasValue']);
if (!$filters) {
return;
}
- // Process all filters that are included in SELECT clause or are allowed by the Afform.
- $allowedFilters = array_merge($this->getSelectAliases(), $this->getAfformFilters());
- foreach ($filters as $fieldName => $value) {
- if (in_array($fieldName, $allowedFilters, TRUE)) {
- $this->applyFilter($fieldName, $value);
+ foreach ($filters as $key => $value) {
+ $fieldNames = explode(',', $key);
+ if (in_array($key, $allowedFilters, TRUE) || !array_diff($fieldNames, $allowedFilters)) {
+ $this->applyFilter($fieldNames, $value);
}
}
}
}
/**
- * @param string $fieldName
+ * @param array $fieldNames
+ * If multiple field names are given they will be combined in an OR clause
* @param mixed $value
*/
- private function applyFilter(string $fieldName, $value) {
+ private function applyFilter(array $fieldNames, $value) {
// Global setting determines if % wildcard should be added to both sides (default) or only the end of a search string
$prefixWithWildcard = \Civi::settings()->get('includeWildCardInName');
+ // Based on the first field, decide which clause to add this condition to
+ $fieldName = $fieldNames[0];
$field = $this->getField($fieldName);
// If field is not found it must be an aggregated column & belongs in the HAVING clause.
if (!$field) {
$clause =& $this->savedSearch['api_params']['where'];
}
- $dataType = $field['data_type'] ?? NULL;
-
- // Array is either associative `OP => VAL` or sequential `IN (...)`
- if (is_array($value)) {
- $value = array_filter($value, [$this, 'hasValue']);
- // If array does not contain operators as keys, assume array of values
- if (array_diff_key($value, array_flip(CoreUtil::getOperators()))) {
- // Use IN for regular fields
- if (empty($field['serialize'])) {
- $clause[] = [$fieldName, 'IN', $value];
+ $filterClauses = [];
+
+ foreach ($fieldNames as $fieldName) {
+ $field = $this->getField($fieldName);
+ $dataType = $field['data_type'] ?? NULL;
+ // Array is either associative `OP => VAL` or sequential `IN (...)`
+ if (is_array($value)) {
+ $value = array_filter($value, [$this, 'hasValue']);
+ // If array does not contain operators as keys, assume array of values
+ if (array_diff_key($value, array_flip(CoreUtil::getOperators()))) {
+ // Use IN for regular fields
+ if (empty($field['serialize'])) {
+ $filterClauses[] = [$fieldName, 'IN', $value];
+ }
+ // Use an OR group of CONTAINS for array fields
+ else {
+ $orGroup = [];
+ foreach ($value as $val) {
+ $orGroup[] = [$fieldName, 'CONTAINS', $val];
+ }
+ $filterClauses[] = ['OR', $orGroup];
+ }
}
- // Use an OR group of CONTAINS for array fields
+ // Operator => Value array
else {
- $orGroup = [];
- foreach ($value as $val) {
- $orGroup[] = [$fieldName, 'CONTAINS', $val];
+ $andGroup = [];
+ foreach ($value as $operator => $val) {
+ $andGroup[] = [$fieldName, $operator, $val];
}
- $clause[] = ['OR', $orGroup];
+ $filterClauses[] = ['AND', $andGroup];
}
}
- // Operator => Value array
+ elseif (!empty($field['serialize'])) {
+ $filterClauses[] = [$fieldName, 'CONTAINS', $value];
+ }
+ elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) {
+ $filterClauses[] = [$fieldName, '=', $value];
+ }
+ elseif ($prefixWithWildcard) {
+ $filterClauses[] = [$fieldName, 'CONTAINS', $value];
+ }
else {
- foreach ($value as $operator => $val) {
- $clause[] = [$fieldName, $operator, $val];
- }
+ $filterClauses[] = [$fieldName, 'LIKE', $value . '%'];
}
}
- elseif (!empty($field['serialize'])) {
- $clause[] = [$fieldName, 'CONTAINS', $value];
- }
- elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) {
- $clause[] = [$fieldName, '=', $value];
- }
- elseif ($prefixWithWildcard) {
- $clause[] = [$fieldName, 'CONTAINS', $value];
+ // Single field
+ if (count($filterClauses) === 1) {
+ $clause[] = $filterClauses[0];
}
else {
- $clause[] = [$fieldName, 'LIKE', $value . '%'];
+ $clause[] = ['OR', $filterClauses];
}
}
$filterAttr = $afform['searchDisplay']['filters'] ?? NULL;
if ($filterAttr && is_string($filterAttr) && $filterAttr[0] === '{') {
foreach (\CRM_Utils_JS::decode($filterAttr) as $filterKey => $filterVal) {
- $filterKeys[] = $filterKey;
// Automatically apply filters from the markup if they have a value
// (if it's a javascript variable it will have come back from decode() as NULL and we'll ignore it).
+ unset($this->filters[$filterKey]);
if ($this->hasValue($filterVal)) {
- $this->applyFilter($filterKey, $filterVal);
+ $this->applyFilter(explode(',', $filterKey), $filterVal);
}
}
}
['first_name' => 'One', 'last_name' => $lastName, 'contact_sub_type' => ['Tester', 'Bot']],
['first_name' => 'Two', 'last_name' => $lastName, 'contact_sub_type' => ['Tester']],
['first_name' => 'Three', 'last_name' => $lastName, 'contact_sub_type' => ['Bot']],
- ['first_name' => 'Four', 'last_name' => $lastName],
+ ['first_name' => 'Four', 'middle_name' => 'None', 'last_name' => $lastName],
];
Contact::save(FALSE)->setRecords($sampleData)->execute();
'api_entity' => 'Contact',
'api_params' => [
'version' => 4,
- 'select' => ['id', 'first_name', 'last_name', 'contact_sub_type:label', 'is_deceased'],
+ 'select' => ['id', 'first_name', 'middle_name', 'last_name', 'contact_sub_type:label', 'is_deceased'],
'where' => [],
],
],
$this->assertEquals(FALSE, $result[0]['is_deceased']['raw']);
$this->assertEquals(ts('No'), $result[0]['is_deceased']['view']);
- $params['filters'] = ['id' => ['>' => $result[0]['id']['raw'], '<=' => $result[1]['id']['raw'] + 1]];
+ $params['filters'] = ['last_name' => $lastName, 'id' => ['>' => $result[0]['id']['raw'], '<=' => $result[1]['id']['raw'] + 1]];
$params['sort'] = [['first_name', 'ASC']];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
$this->assertEquals('Three', $result[0]['first_name']['raw']);
$this->assertEquals('Two', $result[1]['first_name']['raw']);
- $params['filters'] = ['contact_sub_type:label' => ['Tester', 'Bot']];
+ $params['filters'] = ['last_name' => $lastName, 'contact_sub_type:label' => ['Tester', 'Bot']];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(3, $result);
- $params['filters'] = ['contact_sub_type' => ['Tester']];
+ // Comma indicates first_name OR last_name
+ $params['filters'] = ['first_name,last_name' => $lastName, 'contact_sub_type' => ['Tester']];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(2, $result);
+
+ // Comma indicates first_name OR middle_name, matches "One" or "None"
+ $params['filters'] = ['first_name,middle_name' => 'one', 'last_name' => $lastName];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
}