case 'NOT IN':
return !in_array($value, $expected);
+ case 'CONTAINS':
+ if (is_array($value)) {
+ return in_array($expected, $value);
+ }
+ elseif (is_string($value) || is_numeric($value)) {
+ return strpos((string) $value, (string) $expected) !== FALSE;
+ }
+ return $value == $expected;
+
default:
throw new NotImplementedException("Unsupported operator: '$operator' cannot be used with array data");
}
* Leaf operators are one of:
*
* * '=', '<=', '>=', '>', '<', 'LIKE', "<>", "!=",
- * * "NOT LIKE", 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN',
- * * 'IS NOT NULL', or 'IS NULL'.
+ * * 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN',
+ * * 'IS NOT NULL', or 'IS NULL', 'CONTAINS'.
*/
class Api4SelectQuery {
$fieldAlias = $expr;
// Attempt to format if this is a real field
if (isset($this->apiFieldSpec[$expr])) {
- FormattingUtil::formatInputValue($value, $expr, $this->apiFieldSpec[$expr]);
+ $field = $this->getField($expr);
+ FormattingUtil::formatInputValue($value, $expr, $field);
}
}
// Expr references a non-field expression like a function; convert to alias
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]);
+ $field = $this->getField($fieldName);
+ FormattingUtil::formatInputValue($value, $expr, $field);
$fieldAlias = $selectAlias;
break;
}
return sprintf('%s %s %s', $fieldAlias, $operator, $valExpr->render($this->apiFieldSpec));
}
elseif ($fieldName) {
- FormattingUtil::formatInputValue($value, $fieldName, $this->apiFieldSpec[$fieldName]);
+ $field = $this->getField($fieldName);
+ FormattingUtil::formatInputValue($value, $fieldName, $field);
+ }
+ }
+
+ if ($operator === 'CONTAINS') {
+ switch ($field['serialize'] ?? NULL) {
+ case \CRM_Core_DAO::SERIALIZE_JSON:
+ $operator = 'LIKE';
+ $value = '%"' . $value . '"%';
+ // FIXME: Use this instead of the above hack once MIN_INSTALL_MYSQL_VER is bumped to 5.7.
+ // return sprintf('JSON_SEARCH(%s, "one", "%s") IS NOT NULL', $fieldAlias, \CRM_Core_DAO::escapeString($value));
+ break;
+
+ case \CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND:
+ $operator = 'LIKE';
+ $value = '%' . \CRM_Core_DAO::VALUE_SEPARATOR . $value . \CRM_Core_DAO::VALUE_SEPARATOR . '%';
+ break;
+
+ default:
+ $operator = 'LIKE';
+ $value = '%' . $value . '%';
+ break;
}
}
* @return string[]
*/
public static function getOperators() {
- return \CRM_Core_DAO::acceptedSQLOperators();
+ $operators = \CRM_Core_DAO::acceptedSQLOperators();
+ $operators[] = 'CONTAINS';
+ return $operators;
}
}
else if (type === 'Money') {
return CRM.formatMoney(value);
}
+ if (_.isArray(value)) {
+ return value.join(', ');
+ }
return value;
}
if (_.includes(['=', '!=', '<>', '>', '>=', '<', '<='], op)) {
$el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false});
}
- } else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op) && (field.fk_entity || field.options || dataType === 'Boolean')) {
+ } else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN', 'CONTAINS'], op) && (field.fk_entity || field.options || dataType === 'Boolean')) {
if (field.options) {
if (field.options === true) {
$el.addClass('loading');
$this->assertEquals(['shape', 'size', 'weight'], array_keys($result));
}
+ public function testContainsOperator() {
+ MockBasicEntity::delete()->addWhere('id', '>', 0)->execute();
+
+ $records = [
+ ['group' => 'one', 'fruit:name' => ['apple', 'pear'], 'weight' => 11],
+ ['group' => 'two', 'fruit:name' => ['pear', 'banana'], 'weight' => 12],
+ ];
+ MockBasicEntity::save()->setRecords($records)->execute();
+
+ $result = MockBasicEntity::get()
+ ->addWhere('fruit:name', 'CONTAINS', 'apple')
+ ->execute();
+ $this->assertCount(1, $result);
+ $this->assertEquals('one', $result->first()['group']);
+
+ $result = MockBasicEntity::get()
+ ->addWhere('fruit:name', 'CONTAINS', 'pear')
+ ->execute();
+ $this->assertCount(2, $result);
+
+ $result = MockBasicEntity::get()
+ ->addWhere('group', 'CONTAINS', 'o')
+ ->execute();
+ $this->assertCount(2, $result);
+
+ $result = MockBasicEntity::get()
+ ->addWhere('weight', 'CONTAINS', 1)
+ ->execute();
+ $this->assertCount(2, $result);
+
+ $result = MockBasicEntity::get()
+ ->addWhere('fruit:label', 'CONTAINS', 'Banana')
+ ->execute();
+ $this->assertCount(1, $result);
+ $this->assertEquals('two', $result->first()['group']);
+
+ $result = MockBasicEntity::get()
+ ->addWhere('weight', 'CONTAINS', 2)
+ ->execute();
+ $this->assertCount(1, $result);
+ $this->assertEquals('two', $result->first()['group']);
+ }
+
public function testPseudoconstantMatch() {
MockBasicEntity::delete()->addWhere('id', '>', 0)->execute();
use Civi\Api4\Email;
use Civi\Api4\EntityTag;
use Civi\Api4\OptionValue;
+use Civi\Api4\Participant;
use Civi\Api4\Tag;
/**
$this->assertEquals($tag, $options[$tag]['label']);
}
+ public function testParticipantRole() {
+ $event = $this->createEntity(['type' => 'Event']);
+ $contact = $this->createEntity(['type' => 'Individual']);
+ $participant = Participant::create()
+ ->addValue('contact_id', $contact['id'])
+ ->addValue('event_id', $event['id'])
+ ->addValue('role_id:label', ['Attendee', 'Volunteer'])
+ ->execute()->first();
+
+ $search1 = Participant::get()
+ ->addSelect('role_id', 'role_id:label')
+ ->addWhere('role_id:label', 'CONTAINS', 'Volunteer')
+ ->addOrderBy('id')
+ ->execute()->last();
+
+ $this->assertEquals(['Attendee', 'Volunteer'], $search1['role_id:label']);
+ $this->assertEquals(['1', '2'], $search1['role_id']);
+
+ $search2 = Participant::get()
+ ->addWhere('role_id:label', 'CONTAINS', 'Host')
+ ->execute()->indexBy('id');
+
+ $this->assertArrayNotHasKey($participant['id'], (array) $search2);
+ }
+
}