*/
public $forceSelectId = TRUE;
+ /**
+ * @var array
+ */
+ private $explicitJoins = [];
+
/**
* @param \Civi\Api4\Generic\DAOGetAction $apiGet
*/
$field['sql_name'] = '`' . $alias . '`.`' . $field['column_name'] . '`';
$this->addSpecField($alias . '.' . $field['name'], $field);
}
+ $tableName = CoreUtil::getTableName($entity);
+ // Save join info to be retrieved by $this->getExplicitJoin()
+ $this->explicitJoins[$alias] = [
+ 'entity' => $entity,
+ 'table' => $tableName,
+ 'bridge' => NULL,
+ ];
+ // 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->explicitJoins[$alias]['bridge'] = $join[0];
$conditions = $this->getBridgeJoin($join, $entity, $alias);
}
else {
foreach (array_filter($join) as $clause) {
$conditions[] = $this->treeWalkClauses($clause, 'ON');
}
- $tableName = CoreUtil::getTableName($entity);
$this->join($side, $tableName, $alias, $conditions);
}
}
$joiner = \Civi::container()->get('joiner');
// 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.
array_pop($pathArray);
- $pathString = implode('.', $pathArray);
- if (!$joiner->canAutoJoin($this->getFrom(), $pathString)) {
+ try {
+ $joinPath = $joiner->autoJoin($this, $pathArray);
+ }
+ catch (\Exception $e) {
return;
}
-
- $joinPath = $joiner->join($this, $pathString);
-
$lastLink = array_pop($joinPath);
// Custom field names are already prefixed
return $this->api->getCheckPermissions();
}
+ /**
+ * @param string $alias
+ * @return array|NULL
+ */
+ public function getExplicitJoin($alias) {
+ return $this->explicitJoins[$alias] ?? NULL;
+ }
+
/**
* @param string $path
* @param array $field
/**
* @param \Civi\Api4\Query\Api4SelectQuery $query
* The query object to do the joins on
- * @param string $joinPath
- * A path of aliases in dot notation, e.g. contact.phone
+ * @param array $joinPath
+ * A list of aliases, e.g. [contact, phone]
* @param string $side
* Can be LEFT or INNER
*
* @return \Civi\Api4\Service\Schema\Joinable\Joinable[]
* The path used to make the join
*/
- public function join(Api4SelectQuery $query, $joinPath, $side = 'LEFT') {
- $fullPath = $this->getPath($query->getFrom(), $joinPath);
- $baseTable = $query::MAIN_TABLE_ALIAS;
+ public function autoJoin(Api4SelectQuery $query, array $joinPath, $side = 'LEFT') {
+ $explicitJoin = $query->getExplicitJoin($joinPath[0]);
+
+ // If the first item is the name of an explicit join, use it as the base & shift it off the path
+ if ($explicitJoin) {
+ $from = $explicitJoin['table'];
+ $baseTableAlias = array_shift($joinPath);
+ }
+ // Otherwise use the api entity as the base
+ else {
+ $from = $query->getFrom();
+ $baseTableAlias = $query::MAIN_TABLE_ALIAS;
+ }
+
+ $fullPath = $this->getPath($from, $joinPath);
foreach ($fullPath as $link) {
$target = $link->getTargetTable();
$alias = $link->getAlias();
$bao = \CRM_Core_DAO_AllCoreTables::getBAOClassName(\CRM_Core_DAO_AllCoreTables::getClassForTable($target));
- $conditions = $link->getConditionsForJoin($baseTable);
+ $conditions = $link->getConditionsForJoin($baseTableAlias);
// Custom fields do not have a bao, and currently do not have field-specific ACLs
if ($bao) {
- $conditions = array_merge($conditions, $query->getAclClause($alias, $bao, explode('.', $joinPath)));
+ $conditions = array_merge($conditions, $query->getAclClause($alias, $bao, $joinPath));
}
$query->join($side, $target, $alias, $conditions);
- $baseTable = $link->getAlias();
+ $baseTableAlias = $link->getAlias();
}
return $fullPath;
}
- /**
- * Determines if path string points to a simple n-1 join that can be automatically added
- *
- * @param string $baseTable
- * @param $joinPath
- *
- * @return bool
- */
- public function canAutoJoin($baseTable, $joinPath) {
- try {
- $path = $this->getPath($baseTable, $joinPath);
- foreach ($path as $joinable) {
- if ($joinable->getJoinType() === $joinable::JOIN_TYPE_ONE_TO_MANY) {
- return FALSE;
- }
- }
- return TRUE;
- }
- catch (\Exception $e) {
- return FALSE;
- }
- }
-
/**
* @param string $baseTable
- * @param string $joinPath
+ * @param array $joinPath
*
* @return \Civi\Api4\Service\Schema\Joinable\Joinable[]
* @throws \Exception
*/
- protected function getPath($baseTable, $joinPath) {
- $cacheKey = sprintf('%s.%s', $baseTable, $joinPath);
+ protected function getPath(string $baseTable, array $joinPath) {
+ $cacheKey = sprintf('%s.%s', $baseTable, implode('.', $joinPath));
if (!isset($this->cache[$cacheKey])) {
- $stack = explode('.', $joinPath);
$fullPath = [];
- foreach ($stack as $key => $targetAlias) {
+ foreach ($joinPath as $key => $targetAlias) {
$links = $this->schemaMap->getPath($baseTable, $targetAlias);
if (empty($links)) {
$this->assertEquals('1', $contacts[0]['phone.location_type_id']);
}
+ public function testImplicitJoinOnExplicitJoin() {
+ $contacts = Contact::get(FALSE)
+ ->addWhere('id', '=', $this->getReference('test_contact_1')['id'])
+ ->addJoin('Address AS address', TRUE, ['id', '=', 'address.contact_id'], ['address.location_type_id', '=', 1])
+ ->addSelect('id', 'address.country.iso_code')
+ ->execute();
+ $this->assertCount(1, $contacts);
+ $this->assertEquals('US', $contacts[0]['address.country.iso_code']);
+ }
+
public function testJoinToTheSameTableTwice() {
$cid1 = Contact::create(FALSE)
->addValue('first_name', 'Aaa')