-
-
Notifications
You must be signed in to change notification settings - Fork 827
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SearchKit - Add display of type
entity
An entity display does not produce user-facing output, instead it writes to a SQL table which can then be queried from SearchKit, the API, or other SQL-based tools like Drupal Views.
- Loading branch information
Showing
14 changed files
with
663 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
220 changes: 220 additions & 0 deletions
220
ext/search_kit/Civi/Api4/Event/Subscriber/SKEntitySubscriber.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
<?php | ||
/* | ||
+--------------------------------------------------------------------+ | ||
| Copyright CiviCRM LLC. All rights reserved. | | ||
| | | ||
| This work is published under the GNU AGPLv3 license with some | | ||
| permitted exceptions and without any warranty. For full license | | ||
| and copyright information, see https://civicrm.org/licensing | | ||
+--------------------------------------------------------------------+ | ||
*/ | ||
|
||
namespace Civi\Api4\Event\Subscriber; | ||
|
||
use Civi\Api4\Generic\Traits\SavedSearchInspectorTrait; | ||
use Civi\Api4\SKEntity; | ||
use Civi\Api4\Utils\CoreUtil; | ||
use Civi\Core\Event\GenericHookEvent; | ||
use Civi\Core\Event\PostEvent; | ||
use Civi\Core\Event\PreEvent; | ||
use Civi\Core\Service\AutoService; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
|
||
/** | ||
* Manages tables and API entities created from search displays of type "entity" | ||
* @service | ||
* @internal | ||
*/ | ||
class SKEntitySubscriber extends AutoService implements EventSubscriberInterface { | ||
|
||
use SavedSearchInspectorTrait; | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public static function getSubscribedEvents(): array { | ||
return [ | ||
'civi.api4.entityTypes' => 'on_civi_api4_entityTypes', | ||
'hook_civicrm_pre' => 'onPreSaveDisplay', | ||
'hook_civicrm_post' => 'onPostSaveDisplay', | ||
]; | ||
} | ||
|
||
/** | ||
* Register SearchDisplays of type 'entity' | ||
* | ||
* @param \Civi\Core\Event\GenericHookEvent $event | ||
*/ | ||
public static function on_civi_api4_entityTypes(GenericHookEvent $event): void { | ||
// Can't use the API to fetch search displays because this hook is called when the API boots | ||
foreach (_search_kit_get_entity_displays() as $display) { | ||
$event->entities[$display['entityName']] = [ | ||
'name' => $display['entityName'], | ||
'title' => $display['label'], | ||
'title_plural' => $display['label'], | ||
'description' => $display['description'] ?? NULL, | ||
'primary_key' => ['_row'], | ||
'type' => ['SavedSearch'], | ||
'table_name' => $display['tableName'], | ||
'class_args' => [$display['apiName']], | ||
'label_field' => NULL, | ||
'searchable' => 'secondary', | ||
'class' => SKEntity::class, | ||
'icon' => 'fa-search-plus', | ||
]; | ||
} | ||
} | ||
|
||
/** | ||
* @param \Civi\Core\Event\PreEvent $event | ||
*/ | ||
public function onPreSaveDisplay(PreEvent $event): void { | ||
if (!$this->applies($event)) { | ||
return; | ||
} | ||
$newSettings = $event->params['settings'] ?? NULL; | ||
// No changes made, nothing to do | ||
if (!$newSettings && $event->action !== 'delete') { | ||
return; | ||
} | ||
$oldSettings = !$event->id ? NULL : | ||
\CRM_Core_DAO::unSerializeField(\CRM_Core_DAO::getFieldValue('CRM_Search_DAO_SearchDisplay', $event->id, 'settings'), \CRM_Core_DAO::SERIALIZE_JSON); | ||
if (!empty($oldSettings['tableName'])) { | ||
\CRM_Core_BAO_SchemaHandler::dropTable($oldSettings['tableName']); | ||
} | ||
if ($event->action === 'delete') { | ||
return; | ||
} | ||
$ssId = $event->params['saved_search_id'] ?? \CRM_Core_DAO::getFieldValue('CRM_Search_DAO_SearchDisplay', $event->id, 'saved_search_id'); | ||
$this->loadSavedSearch($ssId); | ||
$table = [ | ||
'name' => $newSettings['tableName'], | ||
'is_multiple' => FALSE, | ||
'attributes' => 'ENGINE=InnoDB', | ||
'fields' => [], | ||
]; | ||
// Primary key field | ||
$table['fields'][] = [ | ||
'name' => '_row', | ||
'type' => 'int unsigned', | ||
'primary' => TRUE, | ||
'required' => TRUE, | ||
'attributes' => 'AUTO_INCREMENT', | ||
'comment' => 'Row number', | ||
]; | ||
foreach ($newSettings['columns'] as &$column) { | ||
$expr = $this->getSelectExpression($column['key']); | ||
if (!$expr) { | ||
continue; | ||
} | ||
$column['spec'] = $this->formatFieldSpec($column, $expr); | ||
$table['fields'][] = $this->formatSQLSpec($column, $expr); | ||
} | ||
$event->params['settings'] = $newSettings; | ||
$sql = \CRM_Core_BAO_SchemaHandler::buildTableSQL($table); | ||
// do not i18n-rewrite | ||
\CRM_Core_DAO::executeQuery($sql, [], TRUE, NULL, FALSE, FALSE); | ||
} | ||
|
||
/** | ||
* @param array $column | ||
* @param array{fields: array, expr: SqlExpression, dataType: string} $expr | ||
* @return array | ||
*/ | ||
private function formatFieldSpec(array $column, array $expr): array { | ||
// Strip the pseuoconstant suffix | ||
[$name, $suffix] = array_pad(explode(':', $column['key']), 2, NULL); | ||
// Sanitize the name | ||
$name = \CRM_Utils_String::munge($name, '_', 255); | ||
$spec = [ | ||
'name' => $name, | ||
'data_type' => $expr['dataType'], | ||
'suffixes' => $suffix ? ['id', $suffix] : NULL, | ||
'options' => FALSE, | ||
]; | ||
if ($expr['expr']->getType() === 'SqlField') { | ||
$field = \CRM_Utils_Array::first($expr['fields']); | ||
$spec['fk_entity'] = $field['fk_entity'] ?? NULL; | ||
$spec['original_field_name'] = $field['name']; | ||
$spec['original_field_entity'] = $field['entity']; | ||
if ($suffix) { | ||
// Options will be looked up by SKEntitySpecProvider::getOptionsForSKEntityField | ||
$spec['options'] = TRUE; | ||
} | ||
} | ||
elseif ($expr['expr']->getType() === 'SqlFunction') { | ||
if ($suffix) { | ||
$spec['options'] = CoreUtil::formatOptionList($expr['expr']::getOptions(), $spec['suffixes']); | ||
} | ||
} | ||
return $spec; | ||
} | ||
|
||
/** | ||
* @param array $column | ||
* @param array{fields: array, expr: SqlExpression, dataType: string} $expr | ||
* @return array | ||
*/ | ||
private function formatSQLSpec(array $column, array $expr): array { | ||
// Try to use the exact sql column type as the original field | ||
$field = \CRM_Utils_Array::first($expr['fields']); | ||
if (!empty($field['column_name']) && !empty($field['table_name'])) { | ||
$columns = \CRM_Core_DAO::executeQuery("DESCRIBE `{$field['table_name']}`") | ||
->fetchMap('Field', 'Type'); | ||
$type = $columns[$field['column_name']] ?? NULL; | ||
} | ||
// If we can't get the exact data type from the column, take an educated guess | ||
if (empty($type) || | ||
($expr['expr']->getType() !== 'SqlField' && $field['data_type'] !== $expr['dataType']) | ||
) { | ||
$map = [ | ||
'Array' => 'text', | ||
'Boolean' => 'tinyint', | ||
'Date' => 'date', | ||
'Float' => 'double', | ||
'Integer' => 'int', | ||
'String' => 'text', | ||
'Text' => 'text', | ||
'Timestamp' => 'datetime', | ||
]; | ||
$type = $map[$expr['dataType']] ?? $type; | ||
} | ||
$defn = [ | ||
'name' => $column['spec']['name'], | ||
'type' => $type, | ||
'searchable' => TRUE, | ||
]; | ||
// Add FK indexes | ||
if ($expr['expr']->getType() === 'SqlField' && !empty($field['fk_entity'])) { | ||
$defn['fk_table_name'] = CoreUtil::getTableName($field['fk_entity']); | ||
// FIXME look up fk_field_name from schema, don't assume it's always "id" | ||
$defn['fk_field_name'] = 'id'; | ||
$defn['fk_attributes'] = ' ON DELETE SET NULL'; | ||
} | ||
return $defn; | ||
} | ||
|
||
/** | ||
* @param \Civi\Core\Event\PostEvent $event | ||
*/ | ||
public function onPostSaveDisplay(PostEvent $event): void { | ||
if ($this->applies($event)) { | ||
\Civi::cache('metadata')->clear(); | ||
} | ||
} | ||
|
||
/** | ||
* Check if pre/post hook applies to a SearchDisplay type 'entity' | ||
* | ||
* @param \Civi\Core\Event\PreEvent|\Civi\Core\Event\PostEvent $event | ||
* @return bool | ||
*/ | ||
private function applies(GenericHookEvent $event): bool { | ||
if ($event->entity !== 'SearchDisplay') { | ||
return FALSE; | ||
} | ||
$type = $event->params['type'] ?? $event->object->type ?? \CRM_Core_DAO::getFieldValue('CRM_Search_DAO_SearchDisplay', $event->id, 'type'); | ||
return $type === 'entity'; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php | ||
|
||
namespace Civi\Api4; | ||
|
||
/** | ||
* Virtual API entities provided by SearchDisplays of type "entity" | ||
* @package Civi\Api4 | ||
*/ | ||
class SKEntity { | ||
|
||
/** | ||
* @param string $displayEntity | ||
* @param bool $checkPermissions | ||
* | ||
* @return \Civi\Api4\Generic\DAOGetFieldsAction | ||
*/ | ||
public static function getFields(string $displayEntity, bool $checkPermissions = TRUE): Generic\DAOGetFieldsAction { | ||
return (new Generic\DAOGetFieldsAction('SK_' . $displayEntity, __FUNCTION__)) | ||
->setCheckPermissions($checkPermissions); | ||
} | ||
|
||
/** | ||
* @param string $displayEntity | ||
* @param bool $checkPermissions | ||
* @return \Civi\Api4\Generic\DAOGetAction | ||
* @throws \CRM_Core_Exception | ||
*/ | ||
public static function get(string $displayEntity, bool $checkPermissions = TRUE): Generic\DAOGetAction { | ||
return (new Generic\DAOGetAction('SK_' . $displayEntity, __FUNCTION__)) | ||
->setCheckPermissions($checkPermissions); | ||
} | ||
|
||
/** | ||
* @param string $displayEntity | ||
* @param bool $checkPermissions | ||
* @return \Civi\Api4\Action\GetActions | ||
*/ | ||
public static function getActions(string $displayEntity, bool $checkPermissions = TRUE): Action\GetActions { | ||
return (new Action\GetActions('SK_' . $displayEntity, __FUNCTION__)) | ||
->setCheckPermissions($checkPermissions); | ||
} | ||
|
||
/** | ||
* @param string $displayEntity | ||
* @return \Civi\Api4\Generic\CheckAccessAction | ||
* @throws \CRM_Core_Exception | ||
*/ | ||
public static function checkAccess(string $displayEntity): Generic\CheckAccessAction { | ||
return new Generic\CheckAccessAction('SK_' . $displayEntity, __FUNCTION__); | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public static function permissions(): array { | ||
return []; | ||
} | ||
|
||
} |
Oops, something went wrong.