Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REF] APIv4 - Dispatch event during Entity.get #21803

Merged
merged 1 commit into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 51 additions & 67 deletions Civi/Api4/Action/Entity/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

namespace Civi\Api4\Action\Entity;

use Civi\Api4\CustomGroup;
use Civi\Api4\CustomValue;
use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable;
use Civi\Api4\Utils\CoreUtil;
use Civi\Core\Event\GenericHookEvent;

/**
* Get the names & docblocks of all APIv4 entities.
Expand All @@ -36,31 +36,23 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
* Scan all api directories to discover entities
*/
protected function getRecords() {
$entities = [];
$namesRequested = $this->_itemsToGet('name');
$cache = \Civi::cache('metadata');
$entities = $cache->get('api4.entities.info', []);

if ($namesRequested) {
foreach ($namesRequested as $entityName) {
if (strpos($entityName, 'Custom_') !== 0) {
$className = CoreUtil::getApiClass($entityName);
if ($className) {
$this->loadEntity($className, $entities);
}
}
}
}
else {
if (!$entities) {
foreach ($this->getAllApiClasses() as $className) {
// Load entities declared in API files
$this->loadEntity($className, $entities);
// Load entities based on custom data
$entities = array_merge($entities, $this->getCustomEntities());
// Allow extensions to modify the list of entities
$event = GenericHookEvent::create(['entities' => &$entities]);
\Civi::dispatcher()->dispatch('civi.api4.entityTypes', $event);
}
ksort($entities);
$cache->set('api4.entities.info', $entities);
}

// Fetch custom entities unless we've already fetched everything requested
if (!$namesRequested || array_diff($namesRequested, array_keys($entities))) {
$entities = array_merge($entities, $this->getCustomEntities());
}

ksort($entities);
return $entities;
}

Expand All @@ -81,24 +73,20 @@ private function loadEntity($className, array &$entities) {
* @return \Civi\Api4\Generic\AbstractEntity[]
*/
private function getAllApiClasses() {
$cache = \Civi::cache('metadata');
$classNames = $cache->get('api4.entities.classNames', []);
if (!$classNames) {
$locations = array_merge([\Civi::paths()->getPath('[civicrm.root]/Civi.php')],
array_column(\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(), 'filePath')
);
foreach ($locations as $location) {
$dir = \CRM_Utils_File::addTrailingSlash(dirname($location)) . 'Civi/Api4';
if (is_dir($dir)) {
foreach (glob("$dir/*.php") as $file) {
$className = 'Civi\Api4\\' . basename($file, '.php');
if (is_a($className, 'Civi\Api4\Generic\AbstractEntity', TRUE)) {
$classNames[] = $className;
}
$classNames = [];
$locations = array_merge([\Civi::paths()->getPath('[civicrm.root]/Civi.php')],
array_column(\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(), 'filePath')
);
foreach ($locations as $location) {
$dir = \CRM_Utils_File::addTrailingSlash(dirname($location)) . 'Civi/Api4';
if (is_dir($dir)) {
foreach (glob("$dir/*.php") as $file) {
$className = 'Civi\Api4\\' . basename($file, '.php');
if (is_a($className, 'Civi\Api4\Generic\AbstractEntity', TRUE)) {
$classNames[] = $className;
}
}
}
$cache->set('api4.entities.classNames', $classNames);
}
return $classNames;
}
Expand All @@ -109,39 +97,35 @@ private function getAllApiClasses() {
* @return array[]
*/
private function getCustomEntities() {
$cache = \Civi::cache('metadata');
$entities = $cache->get('api4.entities.custom');
if (!isset($entities)) {
$entities = [];
$customEntities = CustomGroup::get()
->addWhere('is_multiple', '=', 1)
->addWhere('is_active', '=', 1)
->setSelect(['name', 'title', 'help_pre', 'help_post', 'extends', 'icon'])
->setCheckPermissions(FALSE)
->execute();
$baseInfo = CustomValue::getInfo();
foreach ($customEntities as $customEntity) {
$fieldName = 'Custom_' . $customEntity['name'];
$baseEntity = CoreUtil::getApiClass(CustomGroupJoinable::getEntityFromExtends($customEntity['extends']));
$entities[$fieldName] = [
'name' => $fieldName,
'title' => $customEntity['title'],
'title_plural' => $customEntity['title'],
'description' => ts('Custom group for %1', [1 => $baseEntity::getInfo()['title_plural']]),
'paths' => [
'view' => "civicrm/contact/view/cd?reset=1&gid={$customEntity['id']}&recId=[id]&multiRecordDisplay=single",
],
'icon' => $customEntity['icon'] ?: NULL,
] + $baseInfo;
if (!empty($customEntity['help_pre'])) {
$entities[$fieldName]['comment'] = $this->plainTextify($customEntity['help_pre']);
}
if (!empty($customEntity['help_post'])) {
$pre = empty($entities[$fieldName]['comment']) ? '' : $entities[$fieldName]['comment'] . "\n\n";
$entities[$fieldName]['comment'] = $pre . $this->plainTextify($customEntity['help_post']);
}
$entities = [];
$baseInfo = CustomValue::getInfo();
$select = \CRM_Utils_SQL_Select::from('civicrm_custom_group')
->where('is_multiple = 1')
->where('is_active = 1')
->toSQL();
$group = \CRM_Core_DAO::executeQuery($select);
while ($group->fetch()) {
$fieldName = 'Custom_' . $group->name;
$baseEntity = CoreUtil::getApiClass(CustomGroupJoinable::getEntityFromExtends($group->extends));
$entities[$fieldName] = [
'name' => $fieldName,
'title' => $group->title,
'title_plural' => $group->title,
'description' => ts('Custom group for %1', [1 => $baseEntity::getInfo()['title_plural']]),
'paths' => [
'view' => "civicrm/contact/view/cd?reset=1&gid={$group->id}&recId=[id]&multiRecordDisplay=single",
],
] + $baseInfo;
if (!empty($group->icon)) {
$entities[$fieldName]['icon'] = $group->icon;
}
if (!empty($group->help_pre)) {
$entities[$fieldName]['comment'] = $this->plainTextify($group->help_pre);
}
if (!empty($group->help_post)) {
$pre = empty($entities[$fieldName]['comment']) ? '' : $entities[$fieldName]['comment'] . "\n\n";
$entities[$fieldName]['comment'] = $pre . $this->plainTextify($group->help_post);
}
$cache->set('api4.entities.custom', $entities);
}
return $entities;
}
Expand Down
65 changes: 30 additions & 35 deletions Civi/Api4/Generic/AbstractEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,45 +129,40 @@ public static function __callStatic($action, $args) {
* Reflection function called by Entity::get()
*
* @see \Civi\Api4\Action\Entity\Get
* @return array
* @return array{name: string, title: string, description: string, title_plural: string, type: string, paths: array, class: string, primary_key: array, searchable: string, dao: string, label_field: string, icon: string}
*/
public static function getInfo() {
$cache = \Civi::cache('metadata');
$entityName = static::getEntityName();
$info = $cache->get("api4.$entityName.info");
if (!$info) {
$info = [
'name' => $entityName,
'title' => static::getEntityTitle(),
'title_plural' => static::getEntityTitle(TRUE),
'type' => [self::stripNamespace(get_parent_class(static::class))],
'paths' => static::getEntityPaths(),
'class' => static::class,
'primary_key' => ['id'],
// Entities without a @searchable annotation will default to secondary,
// which makes them visible in SearchKit but not at the top of the list.
'searchable' => 'secondary',
];
// Add info for entities with a corresponding DAO
$dao = \CRM_Core_DAO_AllCoreTables::getFullName($info['name']);
if ($dao) {
$info['paths'] = $dao::getEntityPaths();
$info['primary_key'] = $dao::$_primaryKey;
$info['icon'] = $dao::$_icon;
$info['label_field'] = $dao::$_labelField;
$info['dao'] = $dao;
}
foreach (ReflectionUtils::getTraits(static::class) as $trait) {
$info['type'][] = self::stripNamespace($trait);
}
$reflection = new \ReflectionClass(static::class);
$info = array_merge($info, ReflectionUtils::getCodeDocs($reflection, NULL, ['entity' => $info['name']]));
if ($dao) {
$info['description'] = $dao::getEntityDescription() ?? $info['description'] ?? NULL;
}
unset($info['package'], $info['method']);
$cache->set("api4.$entityName.info", $info);
$info = [
'name' => $entityName,
'title' => static::getEntityTitle(),
'title_plural' => static::getEntityTitle(TRUE),
'type' => [self::stripNamespace(get_parent_class(static::class))],
'paths' => static::getEntityPaths(),
'class' => static::class,
'primary_key' => ['id'],
// Entities without a @searchable annotation will default to secondary,
// which makes them visible in SearchKit but not at the top of the list.
'searchable' => 'secondary',
];
// Add info for entities with a corresponding DAO
$dao = \CRM_Core_DAO_AllCoreTables::getFullName($info['name']);
if ($dao) {
$info['paths'] = $dao::getEntityPaths();
$info['primary_key'] = $dao::$_primaryKey;
$info['icon'] = $dao::$_icon;
$info['label_field'] = $dao::$_labelField;
$info['dao'] = $dao;
}
foreach (ReflectionUtils::getTraits(static::class) as $trait) {
$info['type'][] = self::stripNamespace($trait);
}
$reflection = new \ReflectionClass(static::class);
$info = array_merge($info, ReflectionUtils::getCodeDocs($reflection, NULL, ['entity' => $info['name']]));
if ($dao) {
$info['description'] = $dao::getEntityDescription() ?? $info['description'] ?? NULL;
}
unset($info['package'], $info['method']);
return $info;
}

Expand Down
18 changes: 8 additions & 10 deletions Civi/Api4/Utils/CoreUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Civi\Api4\Utils;

use Civi\API\Request;
use Civi\Api4\Entity;
use Civi\Api4\Event\CreateApi4RequestEvent;
use CRM_Core_DAO_AllCoreTables as AllCoreTables;

Expand All @@ -29,14 +30,8 @@ public static function getBAOFromApiName($entityName) {
if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) {
return 'CRM_Core_BAO_CustomValue';
}
$dao = self::getInfoItem($entityName, 'dao');
if (!$dao) {
return NULL;
}
$bao = str_replace("DAO", "BAO", $dao);
// Check if this entity actually has a BAO. Fall back on the DAO if not.
$file = strtr($bao, '_', '/') . '.php';
return stream_resolve_include_path($file) ? $bao : $dao;
$dao = AllCoreTables::getFullName($entityName);
return $dao ? AllCoreTables::getBAOClassName($dao) : NULL;
}

/**
Expand All @@ -57,8 +52,11 @@ public static function getApiClass($entityName) {
* @return mixed
*/
public static function getInfoItem(string $entityName, string $keyToReturn) {
$className = self::getApiClass($entityName);
return $className ? $className::getInfo()[$keyToReturn] ?? NULL : NULL;
$info = Entity::get(FALSE)
->addWhere('name', '=', $entityName)
->addSelect($keyToReturn)
->execute()->first();
return $info ? $info[$keyToReturn] ?? NULL : NULL;
}

/**
Expand Down
1 change: 1 addition & 0 deletions tests/phpunit/api/v3/CampaignTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function setUp(): void {
'created_date' => 'first sat of July 2008',
];
parent::setUp();
\CRM_Core_BAO_ConfigSetting::enableComponent('CiviCampaign');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, this failed because CampaignTest neglected to enable the CiviCampaign component!

$this->useTransaction(TRUE);
}

Expand Down
21 changes: 20 additions & 1 deletion tests/phpunit/api/v4/Action/BasicActionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,29 @@

use api\v4\UnitTestCase;
use Civi\Api4\MockBasicEntity;
use Civi\Api4\Utils\CoreUtil;
use Civi\Core\Event\GenericHookEvent;
use Civi\Test\HookInterface;

/**
* @group headless
*/
class BasicActionsTest extends UnitTestCase {
class BasicActionsTest extends UnitTestCase implements HookInterface {

/**
* Listens for civi.api4.entityTypes event to manually add this nonstandard entity
*
* @param \Civi\Core\Event\GenericHookEvent $e
*/
public function on_civi_api4_entityTypes(GenericHookEvent $e): void {
$e->entities['MockBasicEntity'] = MockBasicEntity::getInfo();
}

public function setUpHeadless() {
// Ensure MockBasicEntity gets added via above listener
\Civi::cache('metadata')->clear();
return parent::setUpHeadless();
}

private function replaceRecords(&$records) {
MockBasicEntity::delete()->addWhere('identifier', '>', 0)->execute();
Expand All @@ -38,6 +56,7 @@ public function testGetInfo() {
$info = MockBasicEntity::getInfo();
$this->assertEquals('MockBasicEntity', $info['name']);
$this->assertEquals(['identifier'], $info['primary_key']);
$this->assertEquals('identifier', CoreUtil::getIdFieldName('MockBasicEntity'));
}

public function testCrud() {
Expand Down