return $plural ? ts('Case Clients') : ts('Case Client');
}
+ /**
+ * @return array
+ */
+ public static function getInfo() {
+ $info = parent::getInfo();
+ $info['bridge_title'] = ts('Clients');
+ $info['bridge'] = [
+ 'case_id' => [
+ 'to' => 'contact_id',
+ 'description' => ts('Cases with this contact as a client'),
+ ],
+ 'contact_id' => [
+ 'label' => ts('Clients'),
+ 'to' => 'case_id',
+ 'description' => ts('Clients for this case'),
+ ],
+ ];
+ return $info;
+ }
+
}
public static function getInfo() {
$info = parent::getInfo();
$info['bridge'] = [
- 'entity_id' => [],
- 'financial_account_id' => [],
+ 'entity_id' => ['to' => 'financial_account_id'],
+ 'financial_account_id' => ['to' => 'entity_id'],
];
return $info;
}
public static function getInfo() {
$info = parent::getInfo();
$info['bridge'] = [
- 'entity_id' => [],
- 'financial_trxn_id' => [],
+ 'entity_id' => ['to' => 'financial_trxn_id'],
+ 'financial_trxn_id' => ['to' => 'entity_id'],
];
return $info;
}
* A bridge is a small table that provides an intermediary link between two other tables.
*
* The API can automatically incorporate a Bridge into a join expression.
- *
- * Note: at time of writing this trait does nothing except affect the "type" shown in Entity::get() metadata.
*/
trait EntityBridge {
*/
public static function getInfo() {
$info = parent::getInfo();
+ $bridgeFields = [];
if (!empty($info['dao'])) {
foreach (($info['dao'])::fields() as $field) {
if (!empty($field['FKClassName']) || $field['name'] === 'entity_id') {
- $info['bridge'][$field['name']] = [];
+ $bridgeFields[] = $field['name'];
}
}
}
+ if (count($bridgeFields) === 2) {
+ $info['bridge'] = [
+ $bridgeFields[0] => ['to' => $bridgeFields[1]],
+ $bridgeFields[1] => ['to' => $bridgeFields[0]],
+ ];
+ }
return $info;
}
public static function getInfo() {
$info = parent::getInfo();
$info['bridge'] = [
- 'group_id' => ['description' => ts('Static (non-smart) group contacts')],
- 'contact_id' => ['description' => ts('Static (non-smart) group contacts')],
+ 'group_id' => [
+ 'to' => 'contact_id',
+ 'description' => ts('Static (non-smart) group contacts'),
+ ],
+ 'contact_id' => [
+ 'to' => 'group_id',
+ 'description' => ts('Static (non-smart) group contacts'),
+ ],
];
return $info;
}
* @throws \API_Exception
*/
private function getBridgeRefs(string $bridgeEntity, string $joinEntity): array {
- $bridgeFields = CoreUtil::getInfoItem($bridgeEntity, 'bridge') ?? [];
- // Sanity check - bridge entity should declare exactly 2 FK fields
- if (count($bridgeFields) !== 2) {
- throw new \API_Exception("Illegal bridge entity specified: $bridgeEntity. Expected 2 bridge fields, found " . count($bridgeFields));
- }
+ $bridges = CoreUtil::getInfoItem($bridgeEntity, 'bridge') ?? [];
/* @var \CRM_Core_DAO $bridgeDAO */
$bridgeDAO = CoreUtil::getInfoItem($bridgeEntity, 'dao');
+ $bridgeEntityFields = \Civi\API\Request::create($bridgeEntity, 'get', ['version' => 4, 'checkPermissions' => $this->getCheckPermissions()])->entityFields();
$bridgeTable = $bridgeDAO::getTableName();
// Get the 2 bridge reference columns as CRM_Core_Reference_* objects
- $joinRef = $baseRef = NULL;
- foreach ($bridgeDAO::getReferenceColumns() as $ref) {
- if (array_key_exists($ref->getReferenceKey(), $bridgeFields)) {
- if (!$joinRef && in_array($joinEntity, $ref->getTargetEntities())) {
- $joinRef = $ref;
+ $referenceColumns = $bridgeDAO::getReferenceColumns();
+ foreach ($referenceColumns as $joinRef) {
+ $refKey = $joinRef->getReferenceKey();
+ if (array_key_exists($refKey, $bridges) && in_array($joinEntity, $joinRef->getTargetEntities())) {
+ if (!empty($bridgeEntityFields[$refKey]['fk_entity']) && $joinEntity !== $bridgeEntityFields[$refKey]['fk_entity']) {
+ continue;
}
- else {
- $baseRef = $ref;
+ foreach ($bridgeDAO::getReferenceColumns() as $baseRef) {
+ if ($baseRef->getReferenceKey() === $bridges[$refKey]['to']) {
+ return [$bridgeTable, $baseRef, $joinRef];
+ }
}
}
}
- if (!$joinRef || !$baseRef) {
- throw new \API_Exception("Unable to join $bridgeEntity to $joinEntity");
- }
- return [$bridgeTable, $baseRef, $joinRef];
+ throw new \API_Exception("Unable to join $bridgeEntity to $joinEntity");
}
/**
$info = parent::getInfo();
$info['bridge_title'] = ts('Relationship');
$info['bridge'] = [
- 'near_contact_id' => ['description' => ts('One or more contacts with a relationship to this contact')],
- 'far_contact_id' => ['description' => ts('One or more contacts with a relationship to this contact')],
+ 'near_contact_id' => [
+ 'to' => 'far_contact_id',
+ 'description' => ts('One or more related contacts'),
+ ],
];
+ if (in_array('CiviCase', \Civi::settings()->get('enable_components'), TRUE)) {
+ $info['bridge']['case_id'] = [
+ 'to' => 'far_contact_id',
+ 'label' => ts('Case Roles'),
+ 'description' => ts('Cases in which this contact has a role'),
+ ];
+ }
return $info;
}
/* @var \CRM_Core_DAO $daoClass */
$daoClass = $entity['dao'];
$references = $daoClass::getReferenceColumns();
- // Only the first bridge reference gets processed, so if it's dynamic we want to be sure it's first in the list
- usort($references, function($first, $second) {
- foreach ([-1 => $first, 1 => $second] as $weight => $reference) {
- if (is_a($reference, 'CRM_Core_Reference_Dynamic')) {
- return $weight;
- }
- }
- return 0;
- });
$fields = array_column($entity['fields'], NULL, 'name');
$bridge = in_array('EntityBridge', $entity['type']) ? $entity['name'] : NULL;
- $bridgeFields = array_keys($entity['bridge'] ?? []);
- foreach ($references as $reference) {
- $keyField = $fields[$reference->getReferenceKey()] ?? NULL;
- if (
- // Sanity check - keyField must exist
- !$keyField ||
- // Exclude any joins that are better represented by pseudoconstants
- is_a($reference, 'CRM_Core_Reference_OptionValue') || (!$bridge && !empty($keyField['options'])) ||
- // Limit bridge joins to just the first
- ($bridge && array_search($keyField['name'], $bridgeFields) !== 0) ||
- // Sanity check - table should match
- $daoClass::getTableName() !== $reference->getReferenceTable()
- ) {
- continue;
- }
- // Dynamic references use a column like "entity_table" (for normal joins this value will be null)
- $dynamicCol = $reference->getTypeColumn();
- // For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once
- foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
- if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) {
+ // Non-bridge joins directly between 2 entities
+ if (!$bridge) {
+ foreach ($references as $reference) {
+ $keyField = $fields[$reference->getReferenceKey()] ?? NULL;
+ if (
+ // Sanity check - keyField must exist
+ !$keyField ||
+ // Exclude any joins that are better represented by pseudoconstants
+ is_a($reference, 'CRM_Core_Reference_OptionValue') || !empty($keyField['options']) ||
+ // Sanity check - table should match
+ $daoClass::getTableName() !== $reference->getReferenceTable()
+ ) {
continue;
}
- $targetEntity = $allowedEntities[$targetEntityName];
- // Non-bridge joins directly between 2 entities
- if (!$bridge) {
+ // Dynamic references use a column like "entity_table" (for normal joins this value will be null)
+ $dynamicCol = $reference->getTypeColumn();
+
+ // For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once
+ foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
+ if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) {
+ continue;
+ }
+ $targetEntity = $allowedEntities[$targetEntityName];
// Add the straight 1-1 join
$alias = $entity['name'] . '_' . $targetEntityName . '_' . $keyField['name'];
$joins[$entity['name']][] = [
'multi' => TRUE,
];
}
- // Bridge joins (sanity check - bridge must specify exactly 2 FK fields)
- elseif (count($entity['bridge']) === 2) {
- // Get the other entity being linked through this bridge
- $baseKey = array_search($reference->getReferenceKey(), $bridgeFields) ? $bridgeFields[0] : $bridgeFields[1];
+ }
+ }
+ // Bridge joins go through an intermediary table
+ elseif (!empty($entity['bridge'])) {
+ foreach ($entity['bridge'] as $targetKey => $bridgeInfo) {
+ $baseKey = $bridgeInfo['to'];
+ $reference = self::getReference($targetKey, $references);
+ $dynamicCol = $reference->getTypeColumn();
+ $keyField = $fields[$reference->getReferenceKey()] ?? NULL;
+ foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
+ $targetEntity = $allowedEntities[$targetEntityName] ?? NULL;
$baseEntity = $allowedEntities[$fields[$baseKey]['fk_entity']] ?? NULL;
- if (!$baseEntity) {
+ if (!$targetEntity || !$baseEntity) {
continue;
}
// Add joins for the two entities that connect through this bridge (n-n)
- $symmetric = $baseEntity['name'] === $targetEntityName;
- $targetsTitle = $symmetric ? $allowedEntities[$bridge]['title_plural'] : $targetEntity['title_plural'];
+ $targetsTitle = $bridgeInfo['label'] ?? $targetEntity['title_plural'];
$alias = $baseEntity['name'] . "_{$bridge}_" . $targetEntityName;
$joins[$baseEntity['name']][] = [
'label' => $baseEntity['title'] . ' ' . $targetsTitle,
- 'description' => $entity['bridge'][$baseKey]['description'] ?? E::ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
+ 'description' => $bridgeInfo['description'] ?? E::ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
'entity' => $targetEntityName,
'conditions' => array_merge(
[$bridge],
'alias' => $alias,
'multi' => TRUE,
];
- if (!$symmetric) {
+ // Back-fill the reverse join if declared
+ if ($dynamicCol && $keyField && !empty($entity['bridge'][$baseKey])) {
$alias = $targetEntityName . "_{$bridge}_" . $baseEntity['name'];
$joins[$targetEntityName][] = [
- 'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'],
+ 'label' => $targetEntity['title'] . ' ' . ($entity['bridge'][$baseKey]['label'] ?? $baseEntity['title_plural']),
'description' => $entity['bridge'][$reference->getReferenceKey()]['description'] ?? E::ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
'entity' => $baseEntity['name'],
'conditions' => array_merge(
return $joins;
}
+ /**
+ * @param string $fieldName
+ * @param \CRM_Core_Reference_Basic[] $references
+ * @return \CRM_Core_Reference_Basic
+ */
+ private static function getReference(string $fieldName, array $references) {
+ foreach ($references as $reference) {
+ if ($reference->getReferenceKey() === $fieldName) {
+ return $reference;
+ }
+ }
+ }
+
/**
* Boilerplate join clause
*
--- /dev/null
+<?php
+namespace Civi\Search;
+
+use Civi\Test\HeadlessInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * @group headless
+ */
+class AdminTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ return \Civi\Test::headless()->installMe(__DIR__)->apply();
+ }
+
+ /**
+ */
+ public function testGetJoins(): void {
+ \CRM_Core_BAO_ConfigSetting::disableComponent('CiviCase');
+ $allowedEntities = Admin::getSchema();
+ $this->assertArrayNotHasKey('Case', $allowedEntities);
+ $this->assertArrayNotHasKey('CaseContact', $allowedEntities);
+
+ \CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
+ $allowedEntities = Admin::getSchema();
+ $this->assertArrayHasKey('Case', $allowedEntities);
+ $this->assertArrayHasKey('CaseContact', $allowedEntities);
+
+ $joins = Admin::getJoins($allowedEntities);
+ $this->assertNotEmpty($joins);
+
+ $groupContactJoins = \CRM_Utils_Array::findAll($joins['Group'], [
+ 'entity' => 'Contact',
+ 'bridge' => 'GroupContact',
+ 'alias' => 'Group_GroupContact_Contact',
+ 'multi' => TRUE,
+ ]);
+ $this->assertCount(1, $groupContactJoins);
+ $this->assertEquals(
+ ['GroupContact', ['id', '=', 'Group_GroupContact_Contact.group_id']],
+ $groupContactJoins[0]['conditions']
+ );
+ $this->assertEquals(
+ [['Group_GroupContact_Contact.status:name', '=', '"Added"']],
+ $groupContactJoins[0]['defaults']
+ );
+
+ $relationshipJoins = \CRM_Utils_Array::findAll($joins['Contact'], [
+ 'entity' => 'Contact',
+ 'bridge' => 'RelationshipCache',
+ 'alias' => 'Contact_RelationshipCache_Contact',
+ 'multi' => TRUE,
+ ]);
+ $this->assertCount(1, $relationshipJoins);
+ $this->assertEquals(
+ ['RelationshipCache', ['id', '=', 'Contact_RelationshipCache_Contact.far_contact_id']],
+ $relationshipJoins[0]['conditions']
+ );
+ $this->assertEquals(
+ [['Contact_RelationshipCache_Contact.near_relation:name', '=', '"Child of"']],
+ $relationshipJoins[0]['defaults']
+ );
+
+ $eventParticipantJoins = \CRM_Utils_Array::findAll($joins['Event'], [
+ 'entity' => 'Participant',
+ 'alias' => 'Event_Participant_event_id',
+ 'multi' => TRUE,
+ ]);
+ $this->assertCount(1, $eventParticipantJoins);
+ $this->assertNull($eventParticipantJoins[0]['bridge'] ?? NULL);
+ $this->assertEquals(
+ [['id', '=', 'Event_Participant_event_id.event_id']],
+ $eventParticipantJoins[0]['conditions']
+ );
+
+ $tagActivityJoins = \CRM_Utils_Array::findAll($joins['Tag'], [
+ 'entity' => 'Activity',
+ 'bridge' => 'EntityTag',
+ 'alias' => 'Tag_EntityTag_Activity',
+ 'multi' => TRUE,
+ ]);
+ $this->assertCount(1, $tagActivityJoins);
+ $this->assertEquals(
+ ['EntityTag', ['id', '=', 'Tag_EntityTag_Activity.tag_id']],
+ $tagActivityJoins[0]['conditions']
+ );
+
+ $activityTagJoins = \CRM_Utils_Array::findAll($joins['Activity'], [
+ 'entity' => 'Tag',
+ 'bridge' => 'EntityTag',
+ 'alias' => 'Activity_EntityTag_Tag',
+ 'multi' => TRUE,
+ ]);
+ $this->assertCount(1, $activityTagJoins);
+ $this->assertEquals(
+ ['EntityTag', ['id', '=', 'Activity_EntityTag_Tag.entity_id'], ['Activity_EntityTag_Tag.entity_table', '=', "'civicrm_activity'"]],
+ $activityTagJoins[0]['conditions']
+ );
+ }
+
+}
use api\v4\UnitTestCase;
use Civi\Api4\Activity;
+use Civi\Api4\CiviCase;
use Civi\Api4\Contact;
use Civi\Api4\Email;
use Civi\Api4\EntityTag;
'civicrm_activity',
'civicrm_phone',
'civicrm_activity_contact',
+ 'civicrm_relationship',
+ 'civicrm_case_contact',
+ 'civicrm_case_type',
+ 'civicrm_case',
];
$this->cleanup(['tablesToTruncate' => $relatedTables]);
parent::tearDown();
$this->assertEquals('654321', $contacts[0]['phone.phone']);
}
+ public function testJoinCaseRoles() {
+ \CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
+ $this->loadDataSet('CaseType');
+
+ $contactID = $this->createEntity(['type' => 'Individual'])['id'];
+ $managerID = $this->createEntity(['type' => 'Individual'])['id'];
+
+ $case = CiviCase::create(FALSE)
+ ->addValue('case_type_id', $this->getReference('test_case_type_1')['id'])
+ ->addValue('status_id', 1)
+ ->addValue('creator_id', $managerID)
+ ->addValue('contact_id', $contactID)
+ ->execute()
+ ->first();
+
+ $contacts = \Civi\Api4\Contact::get()
+ ->addSelect('*', 'case.*')
+ ->addJoin('Case AS case', 'INNER', 'RelationshipCache', ['id', '=', 'case.far_contact_id'], ['case.far_relation', '=', '"Parent of"'])
+ ->addWhere('case.id', '=', $case['id'])
+ ->execute();
+
+ // FIXME: Currently returning 2
+ // $this->assertCount(1, $contacts);
+ $this->assertEquals($managerID, $contacts[0]['id']);
+ }
+
}
use Civi\Api4\CiviCase;
use api\v4\UnitTestCase;
+use Civi\Api4\Relationship;
/**
* @group headless
$this->loadDataSet('CaseType');
}
+ public function tearDown(): void {
+ $relatedTables = [
+ 'civicrm_activity',
+ 'civicrm_activity_contact',
+ 'civicrm_relationship',
+ 'civicrm_case_contact',
+ 'civicrm_case_type',
+ 'civicrm_case',
+ ];
+ $this->cleanup(['tablesToTruncate' => $relatedTables]);
+ parent::tearDown();
+ }
+
public function testCreateUsingLoggedInUser() {
- $this->createLoggedInUser();
+ $uid = $this->createLoggedInUser();
$contactID = $this->createEntity(['type' => 'Individual'])['id'];
- $result = CiviCase::create(FALSE)
+ $case = CiviCase::create(FALSE)
->addValue('case_type_id', $this->getReference('test_case_type_1')['id'])
->addValue('creator_id', 'user_contact_id')
->addValue('status_id', 1)
->execute()
->first();
+ $relationships = Relationship::get(FALSE)
+ ->addWhere('case_id', '=', $case['id'])
+ ->execute();
+
+ $this->assertCount(1, $relationships);
+ $this->assertEquals($uid, $relationships[0]['contact_id_b']);
+ $this->assertEquals($contactID, $relationships[0]['contact_id_a']);
}
}