3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
12 namespace Civi\Api4\Query
;
14 use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable
;
15 use Civi\Api4\Utils\FormattingUtil
;
16 use Civi\Api4\Utils\CoreUtil
;
17 use Civi\Api4\Utils\SelectUtil
;
20 * A query `node` may be in one of three formats:
22 * * leaf: [$fieldName, $operator, $criteria]
23 * * negated: ['NOT', $node]
24 * * branch: ['OR|NOT', [$node, $node, ...]]
26 * Leaf operators are one of:
28 * * '=', '<=', '>=', '>', '<', 'LIKE', "<>", "!=",
29 * * 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN',
30 * * 'IS NOT NULL', or 'IS NULL', 'CONTAINS'.
32 class Api4SelectQuery
{
35 MAIN_TABLE_ALIAS
= 'a',
36 UNLIMITED
= '18446744073709551615';
39 * @var \CRM_Utils_SQL_Select
46 protected $joins = [];
51 protected $apiFieldSpec;
56 protected $entityFieldNames = [];
61 protected $aclFields = [];
64 * @var \Civi\Api4\Generic\DAOGetAction
72 protected $selectAliases = [];
77 public $forceSelectId = TRUE;
80 * @param \Civi\Api4\Generic\DAOGetAction $apiGet
82 public function __construct($apiGet) {
85 // Always select ID of main table unless grouping by something else
86 $this->forceSelectId
= !$this->getGroupBy() ||
$this->getGroupBy() === ['id'];
89 foreach ($this->api
->entityFields() as $field) {
90 $this->entityFieldNames
[] = $field['name'];
91 $field['sql_name'] = '`' . self
::MAIN_TABLE_ALIAS
. '`.`' . $field['column_name'] . '`';
92 $this->addSpecField($field['name'], $field);
95 $tableName = CoreUtil
::getTableName($this->getEntity());
96 $this->query
= \CRM_Utils_SQL_Select
::from($tableName . ' ' . self
::MAIN_TABLE_ALIAS
);
98 // Add ACLs first to avoid redundant subclauses
99 $baoName = CoreUtil
::getBAOFromApiName($this->getEntity());
100 $this->query
->where($this->getAclClause(self
::MAIN_TABLE_ALIAS
, $baoName));
104 * Builds main final sql statement after initialization.
107 * @throws \API_Exception
108 * @throws \CRM_Core_Exception
110 public function getSql() {
111 // Add explicit joins. Other joins implied by dot notation may be added later
112 $this->addExplicitJoins();
113 $this->buildSelectClause();
114 $this->buildWhereClause();
115 $this->buildOrderBy();
117 $this->buildGroupBy();
118 $this->buildHavingClause();
119 return $this->query
->toSQL();
123 * Why walk when you can
127 public function run() {
129 $sql = $this->getSql();
130 $this->debug('sql', $sql);
131 $query = \CRM_Core_DAO
::executeQuery($sql);
132 while ($query->fetch()) {
134 foreach ($this->selectAliases
as $alias => $expr) {
135 $returnName = $alias;
136 $alias = str_replace('.', '_', $alias);
137 $result[$returnName] = property_exists($query, $alias) ?
$query->$alias : NULL;
139 $results[] = $result;
141 FormattingUtil
::formatOutputValues($results, $this->apiFieldSpec
, $this->getEntity(), 'get', $this->selectAliases
);
147 * @throws \API_Exception
149 public function getCount() {
150 $this->addExplicitJoins();
151 $this->buildWhereClause();
152 // If no having or groupBy, we only need to select count
153 if (!$this->getHaving() && !$this->getGroupBy()) {
154 $this->query
->select('COUNT(*) AS `c`');
155 $sql = $this->query
->toSQL();
157 // Use a subquery to count groups from GROUP BY or results filtered by HAVING
159 // With no HAVING, just select the last field grouped by
160 if (!$this->getHaving()) {
161 $select = array_slice($this->getGroupBy(), -1);
163 $this->buildSelectClause($select ??
NULL);
164 $this->buildHavingClause();
165 $this->buildGroupBy();
166 $subquery = $this->query
->toSQL();
167 $sql = "SELECT count(*) AS `c` FROM ( $subquery ) AS `rows`";
169 $this->debug('sql', $sql);
170 return (int) \CRM_Core_DAO
::singleValueQuery($sql);
174 * @param array $select
175 * Array of select expressions; defaults to $this->getSelect
176 * @throws \API_Exception
178 protected function buildSelectClause($select = NULL) {
179 // Use default if select not provided, exclude row_count which is handled elsewhere
180 $select = array_diff($select ??
$this->getSelect(), ['row_count']);
181 // An empty select is the same as *
182 if (empty($select)) {
183 $select = $this->entityFieldNames
;
186 if ($this->forceSelectId
) {
187 $select = array_merge(['id'], $select);
190 // Expand the superstar 'custom.*' to select all fields in all custom groups
191 $customStar = array_search('custom.*', array_values($select), TRUE);
192 if ($customStar !== FALSE) {
193 $customGroups = civicrm_api4($this->getEntity(), 'getFields', [
194 'checkPermissions' => FALSE,
195 'where' => [['custom_group', 'IS NOT NULL']],
196 ], ['custom_group' => 'custom_group']);
198 foreach ($customGroups as $groupName) {
199 $customSelect[] = "$groupName.*";
201 array_splice($select, $customStar, 1, $customSelect);
204 // Expand wildcards in joins (the api wrapper already expanded non-joined wildcards)
205 $wildFields = array_filter($select, function($item) {
206 return strpos($item, '*') !== FALSE && strpos($item, '.') !== FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
209 foreach ($wildFields as $item) {
210 $pos = array_search($item, array_values($select));
211 $this->autoJoinFK($item);
212 $matches = SelectUtil
::getMatchingFields($item, array_keys($this->apiFieldSpec
));
213 array_splice($select, $pos, 1, $matches);
215 $select = array_unique($select);
217 foreach ($select as $item) {
218 $expr = SqlExpression
::convert($item, TRUE);
220 foreach ($expr->getFields() as $fieldName) {
221 $field = $this->getField($fieldName);
222 // Remove expressions with unknown fields without raising an error
224 $select = array_diff($select, [$item]);
225 $this->debug('undefined_fields', $fieldName);
230 $alias = $expr->getAlias();
231 if ($alias != $expr->getExpr() && isset($this->apiFieldSpec
[$alias])) {
232 throw new \
API_Exception('Cannot use existing field name as alias');
234 $this->selectAliases
[$alias] = $expr->getExpr();
235 $this->query
->select($expr->render($this->apiFieldSpec
) . " AS `$alias`");
241 * Add WHERE clause to query
243 protected function buildWhereClause() {
244 foreach ($this->getWhere() as $clause) {
245 $sql = $this->treeWalkClauses($clause, 'WHERE');
247 $this->query
->where($sql);
253 * Add HAVING clause to query
255 * Every expression referenced must also be in the SELECT clause.
257 protected function buildHavingClause() {
258 foreach ($this->getHaving() as $clause) {
259 $this->query
->having($this->treeWalkClauses($clause, 'HAVING'));
264 * Add ORDER BY to query
266 protected function buildOrderBy() {
267 foreach ($this->getOrderBy() as $item => $dir) {
268 if ($dir !== 'ASC' && $dir !== 'DESC') {
269 throw new \
API_Exception("Invalid sort direction. Cannot order by $item $dir");
271 $expr = $this->getExpression($item);
272 $column = $expr->render($this->apiFieldSpec
);
274 // Use FIELD() function to sort on pseudoconstant values
275 $suffix = strstr($item, ':');
276 if ($suffix && $expr->getType() === 'SqlField') {
277 $field = $this->getField($item);
278 $options = FormattingUtil
::getPseudoconstantList($field, substr($suffix, 1));
281 $column = "FIELD($column,'" . implode("','", array_keys($options)) . "')";
284 $this->query
->orderBy("$column $dir");
291 * @throws \CRM_Core_Exception
293 protected function buildLimit() {
294 if ($this->getLimit() ||
$this->getOffset()) {
295 // If limit is 0, mysql will actually return 0 results. Instead set to maximum possible.
296 $this->query
->limit($this->getLimit() ?
: self
::UNLIMITED
, $this->getOffset());
301 * Add GROUP BY clause to query
303 protected function buildGroupBy() {
304 foreach ($this->getGroupBy() as $item) {
305 $this->query
->groupBy($this->getExpression($item)->render($this->apiFieldSpec
));
310 * Recursively validate and transform a branch or leaf clause array to SQL.
312 * @param array $clause
313 * @param string $type
315 * @return string SQL where clause
317 * @throws \API_Exception
318 * @uses composeClause() to generate the SQL etc.
320 protected function treeWalkClauses($clause, $type) {
322 if (in_array($clause[0], ['AND', 'OR', 'NOT']) && empty($clause[1])) {
325 switch ($clause[0]) {
329 if (count($clause[1]) === 1) {
330 // a single set so AND|OR is immaterial
331 return $this->treeWalkClauses($clause[1][0], $type);
334 $sql_subclauses = [];
335 foreach ($clause[1] as $subclause) {
336 $sql_subclauses[] = $this->treeWalkClauses($subclause, $type);
338 return '(' . implode("\n" . $clause[0], $sql_subclauses) . ')';
342 // If we get a group of clauses with no operator, assume AND
343 if (!is_string($clause[1][0])) {
344 $clause[1] = ['AND', $clause[1]];
346 return 'NOT (' . $this->treeWalkClauses($clause[1], $type) . ')';
349 return $this->composeClause($clause, $type);
354 * Validate and transform a leaf clause array to SQL.
355 * @param array $clause [$fieldName, $operator, $criteria]
356 * @param string $type
359 * @throws \API_Exception
362 protected function composeClause(array $clause, string $type) {
363 // Pad array for unary operators
364 list($expr, $operator, $value) = array_pad($clause, 3, NULL);
365 if (!in_array($operator, CoreUtil
::getOperators(), TRUE)) {
366 throw new \
API_Exception('Illegal operator');
369 // For WHERE clause, expr must be the name of a field.
370 if ($type === 'WHERE') {
371 $field = $this->getField($expr, TRUE);
372 FormattingUtil
::formatInputValue($value, $expr, $field, $operator);
373 $fieldAlias = $field['sql_name'];
375 // For HAVING, expr must be an item in the SELECT clause
376 elseif ($type === 'HAVING') {
377 // Expr references a fieldName or alias
378 if (isset($this->selectAliases
[$expr])) {
380 // Attempt to format if this is a real field
381 if (isset($this->apiFieldSpec
[$expr])) {
382 $field = $this->getField($expr);
383 FormattingUtil
::formatInputValue($value, $expr, $field, $operator);
386 // Expr references a non-field expression like a function; convert to alias
387 elseif (in_array($expr, $this->selectAliases
)) {
388 $fieldAlias = array_search($expr, $this->selectAliases
);
390 // If either the having or select field contains a pseudoconstant suffix, match and perform substitution
392 list($fieldName) = explode(':', $expr);
393 foreach ($this->selectAliases
as $selectAlias => $selectExpr) {
394 list($selectField) = explode(':', $selectAlias);
395 if ($selectAlias === $selectExpr && $fieldName === $selectField && isset($this->apiFieldSpec
[$fieldName])) {
396 $field = $this->getField($fieldName);
397 FormattingUtil
::formatInputValue($value, $expr, $field, $operator);
398 $fieldAlias = $selectAlias;
403 if (!isset($fieldAlias)) {
404 throw new \
API_Exception("Invalid expression in HAVING clause: '$expr'. Must use a value from SELECT clause.");
406 $fieldAlias = '`' . $fieldAlias . '`';
408 elseif ($type === 'ON') {
409 $expr = $this->getExpression($expr);
410 $fieldName = count($expr->getFields()) === 1 ?
$expr->getFields()[0] : NULL;
411 $fieldAlias = $expr->render($this->apiFieldSpec
);
412 if (is_string($value)) {
413 $valExpr = $this->getExpression($value);
414 if ($fieldName && $valExpr->getType() === 'SqlString') {
415 $value = $valExpr->getExpr();
416 FormattingUtil
::formatInputValue($value, $fieldName, $this->apiFieldSpec
[$fieldName], $operator);
417 return \CRM_Core_DAO
::createSQLFilter($fieldAlias, [$operator => $value]);
420 $value = $valExpr->render($this->apiFieldSpec
);
421 return sprintf('%s %s %s', $fieldAlias, $operator, $value);
424 elseif ($fieldName) {
425 $field = $this->getField($fieldName);
426 FormattingUtil
::formatInputValue($value, $fieldName, $field, $operator);
430 if ($operator === 'CONTAINS') {
431 switch ($field['serialize'] ??
NULL) {
432 case \CRM_Core_DAO
::SERIALIZE_JSON
:
434 $value = '%"' . $value . '"%';
435 // FIXME: Use this instead of the above hack once MIN_INSTALL_MYSQL_VER is bumped to 5.7.
436 // return sprintf('JSON_SEARCH(%s, "one", "%s") IS NOT NULL', $fieldAlias, \CRM_Core_DAO::escapeString($value));
439 case \CRM_Core_DAO
::SERIALIZE_SEPARATOR_BOOKEND
:
441 $value = '%' . \CRM_Core_DAO
::VALUE_SEPARATOR
. $value . \CRM_Core_DAO
::VALUE_SEPARATOR
. '%';
446 $value = '%' . $value . '%';
451 $sql_clause = \CRM_Core_DAO
::createSQLFilter($fieldAlias, [$operator => $value]);
452 if ($sql_clause === NULL) {
453 throw new \
API_Exception("Invalid value in $type clause for '$expr'");
459 * @param string $expr
460 * @return SqlExpression
461 * @throws \API_Exception
463 protected function getExpression(string $expr) {
464 $sqlExpr = SqlExpression
::convert($expr);
465 foreach ($sqlExpr->getFields() as $fieldName) {
466 $this->getField($fieldName, TRUE);
472 * Get acl clause for an entity
474 * @param string $tableAlias
475 * @param \CRM_Core_DAO|string $baoName
476 * @param array $stack
479 public function getAclClause($tableAlias, $baoName, $stack = []) {
480 if (!$this->getCheckPermissions()) {
483 // Prevent (most) redundant acl sub clauses if they have already been applied to the main entity.
484 // FIXME: Currently this only works 1 level deep, but tracking through multiple joins would increase complexity
485 // and just doing it for the first join takes care of most acl clause deduping.
486 if (count($stack) === 1 && in_array($stack[0], $this->aclFields
, TRUE)) {
489 $clauses = $baoName::getSelectWhereClause($tableAlias);
491 // Track field clauses added to the main entity
492 $this->aclFields
= array_keys($clauses);
494 return array_filter($clauses);
498 * Fetch a field from the getFields list
500 * @param string $expr
501 * @param bool $strict
502 * In strict mode, this will throw an exception if the field doesn't exist
505 * @throws \API_Exception
507 public function getField($expr, $strict = FALSE) {
508 // If the expression contains a pseudoconstant filter like activity_type_id:label,
509 // strip it to look up the base field name, then add the field:filter key to apiFieldSpec
510 $col = strpos($expr, ':');
511 $fieldName = $col ?
substr($expr, 0, $col) : $expr;
512 // Perform join if field not yet available - this will add it to apiFieldSpec
513 if (!isset($this->apiFieldSpec
[$fieldName]) && strpos($fieldName, '.')) {
514 $this->autoJoinFK($fieldName);
516 $field = $this->apiFieldSpec
[$fieldName] ??
NULL;
517 if ($strict && !$field) {
518 throw new \
API_Exception("Invalid field '$fieldName'");
520 $this->apiFieldSpec
[$expr] = $field;
525 * Join onto other entities as specified by the api call.
527 * @throws \API_Exception
528 * @throws \Civi\API\Exception\NotImplementedException
530 private function addExplicitJoins() {
531 foreach ($this->getJoin() as $join) {
532 // First item in the array is the entity name
533 $entity = array_shift($join);
534 // Which might contain an alias. Split on the keyword "AS"
535 list($entity, $alias) = array_pad(explode(' AS ', $entity), 2, NULL);
536 // Ensure alias is a safe string, and supply default if not given
537 $alias = $alias ? \CRM_Utils_String
::munge($alias, '_', 256) : strtolower($entity);
538 // First item in the array is a boolean indicating if the join is required (aka INNER or LEFT).
539 // The rest are join conditions.
540 $side = array_shift($join) ?
'INNER' : 'LEFT';
541 // Add all fields from joined entity to spec
542 $joinEntityGet = \Civi\API\Request
::create($entity, 'get', ['version' => 4, 'checkPermissions' => $this->getCheckPermissions()]);
543 $joinEntityFields = $joinEntityGet->entityFields();
544 foreach ($joinEntityFields as $field) {
545 $field['sql_name'] = '`' . $alias . '`.`' . $field['column_name'] . '`';
546 $this->addSpecField($alias . '.' . $field['name'], $field);
548 if (!empty($join[0]) && is_string($join[0]) && \CRM_Utils_Rule
::alphanumeric($join[0])) {
549 $conditions = $this->getBridgeJoin($join, $entity, $alias);
552 $conditions = $this->getJoinConditions($join, $entity, $alias, $joinEntityFields);
554 foreach (array_filter($join) as $clause) {
555 $conditions[] = $this->treeWalkClauses($clause, 'ON');
557 $tableName = CoreUtil
::getTableName($entity);
558 $this->join($side, $tableName, $alias, $conditions);
563 * Supply conditions for an explicit join.
565 * @param array $joinTree
566 * @param string $joinEntity
567 * @param string $alias
568 * @param array $joinEntityFields
571 private function getJoinConditions($joinTree, $joinEntity, $alias, $joinEntityFields) {
573 // getAclClause() expects a stack of 1-to-1 join fields to help it dedupe, but this is more flexible,
574 // so unless this is a direct 1-to-1 join with the main entity, we'll just hack it
575 // with a padded empty stack to bypass its deduping.
576 $stack = [NULL, NULL];
577 // See if the ON clause already contains an FK reference to joinEntity
578 $explicitFK = array_filter($joinTree, function($clause) use ($alias, $joinEntityFields) {
579 list($sideA, $op, $sideB) = array_pad((array) $clause, 3, NULL);
580 if ($op !== '=' ||
!$sideB) {
583 foreach ([$sideA, $sideB] as $expr) {
584 if ($expr === "$alias.id" ||
!empty($joinEntityFields["$alias.$expr"]['fk_entity'])) {
590 // If we're not explicitly referencing the ID (or some other FK field) of the joinEntity, search for a default
592 foreach ($this->apiFieldSpec
as $name => $field) {
593 if ($field['entity'] !== $joinEntity && $field['fk_entity'] === $joinEntity) {
594 $conditions[] = $this->treeWalkClauses([$name, '=', "$alias.id"], 'ON');
596 elseif (strpos($name, "$alias.") === 0 && substr_count($name, '.') === 1 && $field['fk_entity'] === $this->getEntity()) {
597 $conditions[] = $this->treeWalkClauses([$name, '=', 'id'], 'ON');
601 // Hmm, if we came up with > 1 condition, then it's ambiguous how it should be joined so we won't return anything but the generic ACLs
602 if (count($conditions) > 1) {
603 $stack = [NULL, NULL];
607 $baoName = CoreUtil
::getBAOFromApiName($joinEntity);
608 $acls = array_values($this->getAclClause($alias, $baoName, $stack));
609 return array_merge($acls, $conditions);
613 * Join via a Bridge table
615 * This creates a double-join in sql that appears to the API user like a single join.
617 * @param array $joinTree
618 * @param string $joinEntity
619 * @param string $alias
621 * @throws \API_Exception
623 protected function getBridgeJoin(&$joinTree, $joinEntity, $alias) {
624 $bridgeEntity = array_shift($joinTree);
625 /* @var \Civi\Api4\Generic\DAOEntity $bridgeEntityClass */
626 $bridgeEntityClass = '\Civi\Api4\\' . $bridgeEntity;
627 $bridgeAlias = $alias . '_via_' . strtolower($bridgeEntity);
628 $bridgeInfo = $bridgeEntityClass::getInfo();
629 $bridgeFields = $bridgeInfo['bridge'] ??
[];
630 // Sanity check - bridge entity should declare exactly 2 FK fields
631 if (count($bridgeFields) !== 2) {
632 throw new \
API_Exception("Illegal bridge entity specified: $bridgeEntity. Expected 2 bridge fields, found " . count($bridgeFields));
634 /* @var \CRM_Core_DAO $bridgeDAO */
635 $bridgeDAO = $bridgeInfo['dao'];
636 $bridgeTable = $bridgeDAO::getTableName();
638 $joinTable = CoreUtil
::getTableName($joinEntity);
639 $bridgeEntityGet = $bridgeEntityClass::get($this->getCheckPermissions());
640 // Get the 2 bridge reference columns as CRM_Core_Reference_* objects
641 $joinRef = $baseRef = NULL;
642 foreach ($bridgeDAO::getReferenceColumns() as $ref) {
643 if (in_array($ref->getReferenceKey(), $bridgeFields)) {
644 if (!$joinRef && in_array($joinEntity, $ref->getTargetEntities())) {
652 if (!$joinRef ||
!$baseRef) {
653 throw new \
API_Exception("Unable to join $bridgeEntity to $joinEntity");
655 // Create link between bridge entity and join entity
657 "`$bridgeAlias`.`{$joinRef->getReferenceKey()}` = `$alias`.`{$joinRef->getTargetKey()}`",
659 // For dynamic references, also add the type column (e.g. `entity_table`)
660 if ($joinRef->getTypeColumn()) {
661 $joinConditions[] = "`$bridgeAlias`.`{$joinRef->getTypeColumn()}` = '$joinTable'";
663 // Register fields (other than bridge FK fields) from the bridge entity as if they belong to the join entity
665 foreach ($bridgeEntityGet->entityFields() as $name => $field) {
666 if ($name === 'id' ||
$name === $joinRef->getReferenceKey() ||
$name === $joinRef->getTypeColumn() ||
$name === $baseRef->getReferenceKey() ||
$name === $baseRef->getTypeColumn()) {
669 // Note these fields get a sql alias pointing to the bridge entity, but an api alias pretending they belong to the join entity
670 $field['sql_name'] = '`' . $bridgeAlias . '`.`' . $field['column_name'] . '`';
671 $this->addSpecField($alias . '.' . $field['name'], $field);
672 $fakeFields[] = $alias . '.' . $field['name'];
674 // Move conditions for the bridge join out of the joinTree
675 $bridgeConditions = [];
677 $joinTree = array_filter($joinTree, function($clause) use ($baseRef, $alias, $bridgeAlias, $fakeFields, &$bridgeConditions, &$isExplicit) {
678 list($sideA, $op, $sideB) = array_pad((array) $clause, 3, NULL);
679 // Skip AND/OR/NOT branches
683 // If this condition makes an explicit link between the bridge and another entity
684 if ($op === '=' && $sideB && ($sideA === "$alias.{$baseRef->getReferenceKey()}" ||
$sideB === "$alias.{$baseRef->getReferenceKey()}")) {
685 $expr = $sideA === "$alias.{$baseRef->getReferenceKey()}" ?
$sideB : $sideA;
686 $bridgeConditions[] = "`$bridgeAlias`.`{$baseRef->getReferenceKey()}` = " . $this->getExpression($expr)->render($this->apiFieldSpec
);
690 // Explicit link with dynamic "entity_table" column
691 elseif ($op === '=' && $baseRef->getTypeColumn() && ($sideA === "$alias.{$baseRef->getTypeColumn()}" ||
$sideB === "$alias.{$baseRef->getTypeColumn()}")) {
692 $expr = $sideA === "$alias.{$baseRef->getTypeColumn()}" ?
$sideB : $sideA;
693 $bridgeConditions[] = "`$bridgeAlias`.`{$baseRef->getTypeColumn()}` = " . $this->getExpression($expr)->render($this->apiFieldSpec
);
697 // Other conditions that apply only to the bridge table should be
698 foreach ([$sideA, $sideB] as $expr) {
699 if (is_string($expr) && in_array(explode(':', $expr)[0], $fakeFields)) {
700 $bridgeConditions[] = $this->composeClause($clause, 'ON');
706 // If no bridge conditions were specified, link it to the base entity
708 if (!in_array($this->getEntity(), $baseRef->getTargetEntities())) {
709 throw new \
API_Exception("Unable to join $bridgeEntity to " . $this->getEntity());
711 $bridgeConditions[] = "`$bridgeAlias`.`{$baseRef->getReferenceKey()}` = a.`{$baseRef->getTargetKey()}`";
712 if ($baseRef->getTypeColumn()) {
713 $bridgeConditions[] = "`$bridgeAlias`.`{$baseRef->getTypeColumn()}` = '" . $this->getFrom() . "'";
717 $this->join('LEFT', $bridgeTable, $bridgeAlias, $bridgeConditions);
719 $baoName = CoreUtil
::getBAOFromApiName($joinEntity);
720 $acls = array_values($this->getAclClause($alias, $baoName, [NULL, NULL]));
721 return array_merge($acls, $joinConditions);
725 * Joins a path and adds all fields in the joined entity to apiFieldSpec
728 * @throws \API_Exception
731 protected function autoJoinFK($key) {
732 if (isset($this->apiFieldSpec
[$key])) {
736 $pathArray = explode('.', $key);
738 /** @var \Civi\Api4\Service\Schema\Joiner $joiner */
739 $joiner = \Civi
::container()->get('joiner');
740 // The last item in the path is the field name. We don't care about that; we'll add all fields from the joined entity.
741 array_pop($pathArray);
742 $pathString = implode('.', $pathArray);
744 if (!$joiner->canAutoJoin($this->getFrom(), $pathString)) {
748 $joinPath = $joiner->join($this, $pathString);
750 $lastLink = array_pop($joinPath);
752 // Custom field names are already prefixed
753 $isCustom = $lastLink instanceof CustomGroupJoinable
;
755 array_pop($pathArray);
757 $prefix = $pathArray ?
implode('.', $pathArray) . '.' : '';
758 // Cache field info for retrieval by $this->getField()
759 foreach ($lastLink->getEntityFields() as $fieldObject) {
760 $fieldArray = $fieldObject->toArray();
761 $fieldArray['sql_name'] = '`' . $lastLink->getAlias() . '`.`' . $fieldArray['column_name'] . '`';
762 $this->addSpecField($prefix . $fieldArray['name'], $fieldArray);
767 * @param string $side
768 * @param string $tableName
769 * @param string $tableAlias
770 * @param array $conditions
772 public function join($side, $tableName, $tableAlias, $conditions) {
773 // INNER JOINs take precedence over LEFT JOINs
774 if ($side != 'LEFT' ||
!isset($this->joins
[$tableAlias])) {
775 $this->joins
[$tableAlias] = $side;
776 $this->query
->join($tableAlias, "$side JOIN `$tableName` `$tableAlias` ON " . implode(' AND ', $conditions));
781 * @return FALSE|string
783 public function getFrom() {
784 return CoreUtil
::getTableName($this->getEntity());
790 public function getEntity() {
791 return $this->api
->getEntityName();
797 public function getSelect() {
798 return $this->api
->getSelect();
804 public function getWhere() {
805 return $this->api
->getWhere();
811 public function getHaving() {
812 return $this->api
->getHaving();
818 public function getJoin() {
819 return $this->api
->getJoin();
825 public function getGroupBy() {
826 return $this->api
->getGroupBy();
832 public function getOrderBy() {
833 return $this->api
->getOrderBy();
839 public function getLimit() {
840 return $this->api
->getLimit();
846 public function getOffset() {
847 return $this->api
->getOffset();
851 * @return \CRM_Utils_SQL_Select
853 public function getQuery() {
858 * @return bool|string
860 public function getCheckPermissions() {
861 return $this->api
->getCheckPermissions();
865 * @param string $path
866 * @param array $field
868 private function addSpecField($path, $field) {
869 // Only add field to spec if we have permission
870 if ($this->getCheckPermissions() && !empty($field['permission']) && !\CRM_Core_Permission
::check($field['permission'])) {
871 $this->apiFieldSpec
[$path] = FALSE;
874 $this->apiFieldSpec
[$path] = $field;
878 * Add something to the api's debug output if debugging is enabled
883 public function debug($key, $item) {
884 if ($this->api
->getDebug()) {
885 $this->api
->_debugOutput
[$key][] = $item;