*/
private $entityAccess = [];
+ /**
+ * Explicit join currently being processed
+ * @var array
+ */
+ private $openJoin;
+
/**
* @param \Civi\Api4\Generic\DAOGetAction $api
*/
$this->addSpecField($alias . '.' . $field['name'], $field);
}
$tableName = CoreUtil::getTableName($entity);
+ $this->startNewJoin($tableName, $alias);
// Save join info to be retrieved by $this->getExplicitJoin()
$joinOn = array_filter(array_filter($join, 'is_array'));
$this->explicitJoins[$alias] = [
];
// If the first condition is a string, it's the name of a bridge entity
if (!empty($join[0]) && is_string($join[0]) && \CRM_Utils_Rule::alphanumeric($join[0])) {
- $this->addBridgeJoin($join, $entity, $alias, $side);
+ $conditions = $this->addBridgeJoin($join, $entity, $alias);
}
else {
$conditions = $this->getJoinConditions($join, $entity, $alias, $joinEntityFields);
foreach ($joinOn as $clause) {
$conditions[] = $this->treeWalkClauses($clause, 'ON');
}
- $this->join($side, $tableName, $alias, $conditions);
}
+ $this->finishJoin($side, $conditions);
}
}
* @param array $joinTree
* @param string $joinEntity
* @param string $alias
- * @param string $side
* @throws \CRM_Core_Exception
*/
- protected function addBridgeJoin($joinTree, $joinEntity, $alias, $side) {
+ protected function addBridgeJoin($joinTree, $joinEntity, $alias) {
$bridgeEntity = array_shift($joinTree);
$this->explicitJoins[$alias]['bridge'] = $bridgeEntity;
$linkConditions = $this->getBridgeLinkConditions($bridgeAlias, $alias, $joinTable, $joinRef);
- $bridgeConditions = $this->getBridgeJoinConditions($joinTree, $baseRef, $alias, $bridgeAlias, $bridgeEntity, $side);
+ $bridgeConditions = $this->getBridgeJoinConditions($joinTree, $baseRef, $alias, $bridgeAlias, $bridgeEntity);
$acls = array_values($this->getAclClause($alias, CoreUtil::getBAOFromApiName($joinEntity), [NULL, NULL]));
- $joinConditions = [];
+ $outerConditions = [];
foreach (array_filter($joinTree) as $clause) {
- $joinConditions[] = $this->treeWalkClauses($clause, 'ON');
+ $outerConditions[] = $this->treeWalkClauses($clause, 'ON');
}
// Info needed for joining custom fields extending the bridge entity
$this->explicitJoins[$alias]['bridge_table_alias'] = $bridgeAlias;
+ // Invert the join
+ $this->openJoin['table'] = $bridgeTable;
+ $this->openJoin['alias'] = $bridgeAlias;
- $outerConditions = array_merge($joinConditions, $bridgeConditions);
+ // Add main table as inner join
$innerConditions = array_merge($linkConditions, $acls);
- $this->query->join($alias, "$side JOIN (`$bridgeTable` AS `$bridgeAlias` INNER JOIN `$joinTable` AS `$alias` ON (" . implode(' AND ', $innerConditions) . ")) ON " . implode(' AND ', $outerConditions));
+ $this->addJoin('INNER', $joinTable, $alias, $bridgeAlias, $innerConditions);
+ return array_merge($outerConditions, $bridgeConditions);
}
/**
if ($bao) {
$conditions = array_merge($conditions, $this->getAclClause($tableAlias, $bao, $joinPath));
}
- $this->join('LEFT', $target, $tableAlias, $conditions);
+ $this->addJoin('LEFT', $target, $tableAlias, $baseTableAlias, $conditions);
}
}
}
}
+ /**
+ * Begins a new join; as long as it's "open" then additional joins will nest inside it.
+ */
+ private function startNewJoin(string $tableName, string $joinAlias): void {
+ $this->openJoin = [
+ 'table' => $tableName,
+ 'alias' => $joinAlias,
+ 'subjoins' => [],
+ ];
+ }
+
+ private function finishJoin(string $side, $conditions): void {
+ $tableAlias = $this->openJoin['alias'];
+ $tableName = $this->openJoin['table'];
+ $subjoinClause = '';
+ foreach ($this->openJoin['subjoins'] as $subjoin) {
+ $subjoinClause .= " INNER JOIN `{$subjoin['table']}` `{$subjoin['alias']}` ON (" . implode(' AND ', $subjoin['conditions']) . ")";
+ }
+ $this->query->join($tableAlias, "$side JOIN (`$tableName` `$tableAlias`$subjoinClause) ON " . implode(' AND ', $conditions));
+ $this->openJoin = NULL;
+ }
+
/**
* @param string $side
* @param string $tableName
* @param string $tableAlias
+ * @param string $baseTableAlias
* @param array $conditions
*/
- private function join(string $side, string $tableName, string $tableAlias, array $conditions): void {
- $this->query->join($tableAlias, "$side JOIN `$tableName` `$tableAlias` ON " . implode(' AND ', $conditions));
+ private function addJoin(string $side, string $tableName, string $tableAlias, string $baseTableAlias, array $conditions): void {
+ // If this join is based off the current open join, incorporate it
+ if ($baseTableAlias === ($this->openJoin['alias'] ?? NULL)) {
+ $this->openJoin['subjoins'][] = [
+ 'table' => $tableName,
+ 'alias' => $tableAlias,
+ 'conditions' => $conditions,
+ ];
+ }
+ else {
+ $this->query->join($tableAlias, "$side JOIN `$tableName` `$tableAlias` ON " . implode(' AND ', $conditions));
+ }
}
/**
use Civi\Api4\Contact;
use Civi\Api4\CustomGroup;
use Civi\Api4\CustomField;
+use Civi\Api4\Participant;
/**
* @group headless
Contact::get()->addSelect('*')->addSelect('custom.*')->execute();
}
+ /**
+ * Ensures we can join two entities with a custom field in the ON clause
+ */
+ public function testJoinWithCustomFieldInOnClause(): void {
+ CustomGroup::create(FALSE)
+ ->addValue('extends', 'Participant')
+ ->addValue('title', 'p_set')
+ ->addChain('field', CustomField::create()
+ ->addValue('custom_group_id', '$id')
+ ->addValue('label', 'p_field')
+ ->addValue('html_type', 'Text')
+ )
+ ->execute();
+ $cid = $this->saveTestRecords('Contact', ['records' => 3])->column('id');
+ $this->saveTestRecords('Participant', [
+ 'records' => [
+ ['contact_id' => $cid[0], 'p_set.p_field' => 'Value A'],
+ ['contact_id' => $cid[1], 'p_set.p_field' => 'Value B'],
+ ['contact_id' => $cid[2], 'p_set.p_field' => 'Value A'],
+ ],
+ ]);
+ $results = Participant::get(FALSE)
+ ->addSelect('id')
+ ->addWhere('p_set.p_field', '=', 'Value A')
+ ->execute();
+ $this->assertCount(2, $results);
+ $results = Contact::get(FALSE)
+ ->addSelect('id')
+ ->addJoin('Participant AS participant', 'INNER',
+ ['id', '=', 'participant.contact_id'],
+ ['participant.p_set.p_field', '=', '"Value A"'],
+ )
+ ->execute();
+ $this->assertCount(2, $results);
+ }
+
}