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.
* 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;
}
* @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;
}
* @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;
}
* 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;
}
namespace Civi\Api4\Utils;
use Civi\API\Request;
+use Civi\Api4\Entity;
use Civi\Api4\Event\CreateApi4RequestEvent;
use CRM_Core_DAO_AllCoreTables as AllCoreTables;
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;
}
/**
* @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;
}
/**
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();
$info = MockBasicEntity::getInfo();
$this->assertEquals('MockBasicEntity', $info['name']);
$this->assertEquals(['identifier'], $info['primary_key']);
+ $this->assertEquals('identifier', CoreUtil::getIdFieldName('MockBasicEntity'));
}
public function testCrud() {