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\Search
;
16 * @package Civi\Search
23 public static function getAdminSettings():array {
24 $schema = self
::getSchema();
27 'joins' => self
::getJoins(array_column($schema, NULL, 'name')),
28 'operators' => \CRM_Utils_Array
::makeNonAssociative(self
::getOperators()),
29 'functions' => \CRM_Api4_Page_Api4Explorer
::getSqlFunctions(),
30 'displayTypes' => Display
::getDisplayTypes(['name', 'label', 'description', 'icon']),
37 public static function getOperators():array {
45 'CONTAINS' => ts('Contains'),
47 'NOT IN' => ts('Not In'),
48 'LIKE' => ts('Is Like'),
49 'NOT LIKE' => ts('Not Like'),
50 'BETWEEN' => ts('Is Between'),
51 'NOT BETWEEN' => ts('Not Between'),
52 'IS NULL' => ts('Is Null'),
53 'IS NOT NULL' => ts('Not Null'),
58 * Fetch all entities the current user has permission to `get`
61 public static function getSchema() {
63 $entities = \Civi\Api4\Entity
::get()
64 ->addSelect('name', 'title', 'type', 'title_plural', 'description', 'icon', 'paths', 'dao', 'bridge')
65 ->addWhere('searchable', '=', TRUE)
66 ->addOrderBy('title_plural')
68 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']],
70 $getFields = ['name', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity'];
71 foreach ($entities as $entity) {
72 // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get
74 // Add paths (but only RUD actions) with translated titles
75 foreach ($entity['paths'] as $action => $path) {
76 unset($entity['paths'][$action]);
79 $title = ts('View %1', [1 => $entity['title']]);
83 $title = ts('Edit %1', [1 => $entity['title']]);
87 $title = ts('Delete %1', [1 => $entity['title']]);
93 $entity['paths'][] = [
99 $entity['fields'] = (array) civicrm_api4($entity['name'], 'getFields', [
100 'select' => $getFields,
101 'where' => [['name', 'NOT IN', ['api_key', 'hash']]],
102 'orderBy' => ['label'],
104 $params = $entity['get'][0];
105 // Entity must support at least these params or it is too weird for search kit
106 if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) {
107 \CRM_Utils_Array
::remove($params, 'checkPermissions', 'debug', 'chain', 'language', 'select', 'where', 'orderBy', 'limit', 'offset');
108 unset($entity['get']);
109 $schema[] = ['params' => array_keys($params)] +
array_filter($entity);
117 * @param array $allowedEntities
120 public static function getJoins(array $allowedEntities) {
122 foreach ($allowedEntities as $entity) {
123 if (!empty($entity['dao'])) {
124 /* @var \CRM_Core_DAO $daoClass */
125 $daoClass = $entity['dao'];
126 $references = $daoClass::getReferenceColumns();
127 // Only the first bridge reference gets processed, so if it's dynamic we want to be sure it's first in the list
128 usort($references, function($reference) {
129 return is_a($reference, 'CRM_Core_Reference_Dynamic') ?
-1 : 1;
131 $fields = array_column($entity['fields'], NULL, 'name');
132 $bridge = in_array('EntityBridge', $entity['type']) ?
$entity['name'] : NULL;
133 $baseEntity = $bridge && isset($entity['bridge'][1]) ?
$allowedEntities[$fields[$entity['bridge'][1]]['fk_entity']] ??
NULL : NULL;
134 if ($bridge && !$baseEntity) {
137 foreach ($references as $reference) {
138 $keyField = $fields[$reference->getReferenceKey()] ??
NULL;
139 // Exclude any joins that are better represented by pseudoconstants
140 if (is_a($reference, 'CRM_Core_Reference_OptionValue')
141 ||
!$keyField ||
!empty($keyField['options'])
142 // Limit bridge joins to just the first
143 ||
$bridge && array_search($keyField['name'], $entity['bridge']) !== 0
144 // Sanity check - table should match
145 ||
$daoClass::getTableName() !== $reference->getReferenceTable()
149 // Dynamic references use a column like "entity_table"
150 $dynamicCol = $reference->getTypeColumn();
152 $targetTables = $daoClass::buildOptions($dynamicCol);
153 if (!$targetTables) {
156 $targetTables = array_keys($targetTables);
159 $targetTables = [$reference->getTargetTable()];
161 foreach ($targetTables as $targetTable) {
162 $targetDao = \CRM_Core_DAO_AllCoreTables
::getClassForTable($targetTable);
163 $targetEntityName = \CRM_Core_DAO_AllCoreTables
::getBriefName($targetDao);
164 if (!isset($allowedEntities[$targetEntityName]) ||
$targetEntityName === $entity['name']) {
167 $targetEntity = $allowedEntities[$targetEntityName];
169 // Add the straight 1-1 join
170 $alias = $entity['name'] . '_' . $targetEntityName . '_' . $keyField['name'];
171 $joins[$entity['name']][] = [
172 'label' => $entity['title'] . ' ' . $targetEntity['title'],
173 'description' => $dynamicCol ?
'' : $keyField['label'],
174 'entity' => $targetEntityName,
175 'conditions' => self
::getJoinConditions($keyField['name'], $alias . '.' . $reference->getTargetKey(), $targetTable, $dynamicCol),
179 // Flip the conditions & add the reverse (1-n) join
180 $alias = $targetEntityName . '_' . $entity['name'] . '_' . $keyField['name'];
181 $joins[$targetEntityName][] = [
182 'label' => $targetEntity['title'] . ' ' . $entity['title_plural'],
183 'description' => $dynamicCol ?
'' : $keyField['label'],
184 'entity' => $entity['name'],
185 'conditions' => self
::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ?
$alias . '.' . $dynamicCol : NULL),
191 // Add joins for the two entities that connect through this bridge (n-n)
192 $symmetric = $baseEntity['name'] === $targetEntityName;
193 $targetsTitle = $symmetric ?
$allowedEntities[$bridge]['title_plural'] : $targetEntity['title_plural'];
194 $joins[$baseEntity['name']][] = [
195 'label' => $baseEntity['title'] . ' ' . $targetsTitle,
196 'description' => ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
197 'entity' => $targetEntityName,
198 'conditions' => [$bridge],
200 'alias' => $baseEntity['name'] . "_{$bridge}_" . $targetEntityName,
204 $joins[$targetEntityName][] = [
205 'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'],
206 'description' => ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
207 'entity' => $baseEntity['name'],
208 'conditions' => [$bridge],
210 'alias' => $targetEntityName . "_{$bridge}_" . $baseEntity['name'],
223 * Boilerplate join clause
225 * @param string $nearCol
226 * @param string $farCol
227 * @param string $targetTable
228 * @param string|null $dynamicCol
231 private static function getJoinConditions($nearCol, $farCol, $targetTable, $dynamicCol) {