From eb378b8a38c6f17d9348e08a72d8ab89c8ff1823 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 22 Mar 2021 12:01:43 -0400 Subject: [PATCH] APIv4 - Add function to get api class name Adding the "Case" entity is a problem because it's a reserved php keyword. So we need to be able to have entity names that don't match their classname. This function gives us a way to get api class names without guessing from the entity name. --- Civi/API/Request.php | 4 ++- Civi/Api4/Action/Entity/Get.php | 30 +++++++++--------- Civi/Api4/Action/Entity/GetLinks.php | 2 +- Civi/Api4/CustomValue.php | 20 ++++++++++-- Civi/Api4/Generic/AbstractAction.php | 3 +- Civi/Api4/Generic/AbstractEntity.php | 4 ++- Civi/Api4/Query/Api4SelectQuery.php | 6 ++-- Civi/Api4/Utils/CoreUtil.php | 31 ++++++++++++++----- .../phpunit/api/v4/Entity/ConformanceTest.php | 3 +- 9 files changed, 70 insertions(+), 33 deletions(-) diff --git a/Civi/API/Request.php b/Civi/API/Request.php index 5d0457ef7d..5ad9b9d219 100644 --- a/Civi/API/Request.php +++ b/Civi/API/Request.php @@ -10,6 +10,8 @@ */ namespace Civi\API; +use Civi\Api4\Utils\CoreUtil; + /** * Class Request * @package Civi\API @@ -48,7 +50,7 @@ class Request { $apiRequest = \Civi\Api4\CustomValue::$action(substr($entity, 7)); } else { - $callable = ["\\Civi\\Api4\\$entity", $action]; + $callable = [CoreUtil::getApiClass($entity), $action]; if (!is_callable($callable)) { throw new \Civi\API\Exception\NotImplementedException("API ($entity, $action) does not exist (join the API team and implement it!)"); } diff --git a/Civi/Api4/Action/Entity/Get.php b/Civi/Api4/Action/Entity/Get.php index 0deee772fe..765a3703a9 100644 --- a/Civi/Api4/Action/Entity/Get.php +++ b/Civi/Api4/Action/Entity/Get.php @@ -20,7 +20,9 @@ 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; /** * Get the names & docblocks of all APIv4 entities. @@ -57,14 +59,15 @@ class Get extends \Civi\Api4\Generic\BasicGetAction { foreach (glob("$dir/*.php") as $file) { $matches = []; preg_match('/(\w*)\.php$/', $file, $matches); - $entity = '\Civi\Api4\\' . $matches[1]; - if ( - (!$toGet || in_array($matches[1], $toGet)) - && is_a($entity, '\Civi\Api4\Generic\AbstractEntity', TRUE) - ) { - $info = $entity::getInfo(); + $className = '\Civi\Api4\\' . $matches[1]; + if (is_a($className, '\Civi\Api4\Generic\AbstractEntity', TRUE)) { + $info = $className::getInfo(); + $entityName = $info['name']; + $daoName = $info['dao'] ?? NULL; // Only include DAO entities from enabled components - if (empty($info['dao']) || !defined($info['dao'] . '::COMPONENT') || in_array(constant($info['dao'] . '::COMPONENT'), $enabledComponents)) { + if ((!$toGet || in_array($entityName, $toGet)) && + (!$daoName || !defined("{$daoName}::COMPONENT") || in_array($daoName::COMPONENT, $enabledComponents)) + ) { $entities[$info['name']] = $info; } } @@ -94,25 +97,20 @@ class Get extends \Civi\Api4\Generic\BasicGetAction { ->setSelect(['name', 'title', 'help_pre', 'help_post', 'extends', 'icon']) ->setCheckPermissions(FALSE) ->execute(); + $baseInfo = CustomValue::getInfo(); foreach ($customEntities as $customEntity) { $fieldName = 'Custom_' . $customEntity['name']; - $baseEntity = '\Civi\Api4\\' . CustomGroupJoinable::getEntityFromExtends($customEntity['extends']); + $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']]), - 'searchable' => TRUE, - 'type' => ['CustomValue'], 'paths' => [ 'view' => "civicrm/contact/view/cd?reset=1&gid={$customEntity['id']}&recId=[id]&multiRecordDisplay=single", ], - 'see' => [ - 'https://docs.civicrm.org/user/en/latest/organising-your-data/creating-custom-fields/#multiple-record-fieldsets', - '\\Civi\\Api4\\CustomGroup', - ], - 'icon' => $customEntity['icon'], - ]; + 'icon' => $customEntity['icon'] ?: NULL, + ] + $baseInfo; if (!empty($customEntity['help_pre'])) { $entities[$fieldName]['comment'] = $this->plainTextify($customEntity['help_pre']); } diff --git a/Civi/Api4/Action/Entity/GetLinks.php b/Civi/Api4/Action/Entity/GetLinks.php index 5a4bc69785..2d224da335 100644 --- a/Civi/Api4/Action/Entity/GetLinks.php +++ b/Civi/Api4/Action/Entity/GetLinks.php @@ -33,7 +33,7 @@ class GetLinks extends \Civi\Api4\Generic\BasicGetAction { foreach ($schema->getTables() as $table) { $entity = CoreUtil::getApiNameFromTableName($table->getName()); // Since this is an api function, exclude tables that don't have an api - if (strpos($entity, 'Custom_') === 0 || class_exists('\Civi\Api4\\' . $entity)) { + if ($entity) { $item = [ 'entity' => $entity, 'table' => $table->getName(), diff --git a/Civi/Api4/CustomValue.php b/Civi/Api4/CustomValue.php index 9cc2e5f753..ef7fdc1687 100644 --- a/Civi/Api4/CustomValue.php +++ b/Civi/Api4/CustomValue.php @@ -16,7 +16,6 @@ * @copyright CiviCRM LLC https://civicrm.org/licensing */ - namespace Civi\Api4; /** @@ -124,7 +123,8 @@ class CustomValue { } /** - * @inheritDoc + * @see \Civi\Api4\Generic\AbstractEntity::permissions() + * @return array */ public static function permissions() { $entity = 'contact'; @@ -134,4 +134,20 @@ class CustomValue { return \CRM_Utils_Array::value($entity, $permissions, []) + $permissions['default']; } + /** + * @see \Civi\Api4\Generic\AbstractEntity::getInfo() + * @return array + */ + public static function getInfo() { + return [ + 'class' => __CLASS__, + 'type' => ['CustomValue'], + 'searchable' => TRUE, + 'see' => [ + 'https://docs.civicrm.org/user/en/latest/organising-your-data/creating-custom-fields/#multiple-record-fieldsets', + '\Civi\Api4\CustomGroup', + ], + ]; + } + } diff --git a/Civi/Api4/Generic/AbstractAction.php b/Civi/Api4/Generic/AbstractAction.php index 2103f319c2..050fd5a6ca 100644 --- a/Civi/Api4/Generic/AbstractAction.php +++ b/Civi/Api4/Generic/AbstractAction.php @@ -18,6 +18,7 @@ namespace Civi\Api4\Generic; +use Civi\Api4\Utils\CoreUtil; use Civi\Api4\Utils\FormattingUtil; use Civi\Api4\Utils\ReflectionUtils; @@ -399,7 +400,7 @@ abstract class AbstractAction implements \ArrayAccess { * @return array */ public function getPermissions() { - $permissions = call_user_func(["\\Civi\\Api4\\" . $this->_entityName, 'permissions']); + $permissions = call_user_func([CoreUtil::getApiClass($this->_entityName), 'permissions']); $permissions += [ // applies to getFields, getActions, etc. 'meta' => ['access CiviCRM'], diff --git a/Civi/Api4/Generic/AbstractEntity.php b/Civi/Api4/Generic/AbstractEntity.php index a2d5e117cd..4db822d5f9 100644 --- a/Civi/Api4/Generic/AbstractEntity.php +++ b/Civi/Api4/Generic/AbstractEntity.php @@ -110,8 +110,9 @@ abstract class AbstractEntity { */ public static function __callStatic($action, $args) { $entity = self::getEntityName(); + $nameSpace = str_replace('Civi\Api4\\', 'Civi\Api4\Action\\', static::class); // Find class for this action - $entityAction = "\\Civi\\Api4\\Action\\$entity\\" . ucfirst($action); + $entityAction = "$nameSpace\\" . ucfirst($action); if (class_exists($entityAction)) { $actionObject = new $entityAction($entity, $action); if (isset($args[0]) && $args[0] === FALSE) { @@ -137,6 +138,7 @@ abstract class AbstractEntity { 'title_plural' => static::getEntityTitle(TRUE), 'type' => [self::stripNamespace(get_parent_class(static::class))], 'paths' => static::getEntityPaths(), + 'class' => static::class, ]; // Add info for entities with a corresponding DAO $dao = \CRM_Core_DAO_AllCoreTables::getFullName($info['name']); diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php index efaba5c296..3a2d80efd3 100644 --- a/Civi/Api4/Query/Api4SelectQuery.php +++ b/Civi/Api4/Query/Api4SelectQuery.php @@ -729,7 +729,7 @@ class Api4SelectQuery { } // For LEFT joins, construct a subquery to link the bridge & join tables as one else { - $joinEntityClass = '\Civi\Api4\\' . $joinEntity; + $joinEntityClass = CoreUtil::getApiClass($joinEntity); foreach ($joinEntityClass::get($this->getCheckPermissions())->entityFields() as $name => $field) { $bridgeFields[$field['column_name']] = '`' . $joinAlias . '`.`' . $field['column_name'] . '`'; } @@ -751,7 +751,7 @@ class Api4SelectQuery { */ private function getBridgeRefs(string $bridgeEntity, string $joinEntity): array { /* @var \Civi\Api4\Generic\DAOEntity $bridgeEntityClass */ - $bridgeEntityClass = '\Civi\Api4\\' . $bridgeEntity; + $bridgeEntityClass = CoreUtil::getApiClass($bridgeEntity); $bridgeInfo = $bridgeEntityClass::getInfo(); $bridgeFields = $bridgeInfo['bridge'] ?? []; // Sanity check - bridge entity should declare exactly 2 FK fields @@ -814,7 +814,7 @@ class Api4SelectQuery { private function registerBridgeJoinFields($bridgeEntity, $joinRef, $baseRef, string $alias, string $bridgeAlias, string $side): array { $fakeFields = []; $bridgeFkFields = [$joinRef->getReferenceKey(), $joinRef->getTypeColumn(), $baseRef->getReferenceKey(), $baseRef->getTypeColumn()]; - $bridgeEntityClass = '\Civi\Api4\\' . $bridgeEntity; + $bridgeEntityClass = CoreUtil::getApiClass($bridgeEntity); foreach ($bridgeEntityClass::get($this->getCheckPermissions())->entityFields() as $name => $field) { if ($name === 'id' || ($side === 'INNER' && in_array($name, $bridgeFkFields, TRUE))) { continue; diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php index ebe7913797..f7195d0d17 100644 --- a/Civi/Api4/Utils/CoreUtil.php +++ b/Civi/Api4/Utils/CoreUtil.php @@ -21,13 +21,9 @@ namespace Civi\Api4\Utils; use CRM_Core_DAO_AllCoreTables as AllCoreTables; -require_once 'api/v3/utils.php'; - class CoreUtil { /** - * todo this class should not rely on api3 code - * * @param $entityName * * @return \CRM_Core_DAO|string @@ -38,7 +34,27 @@ class CoreUtil { if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) { return 'CRM_Core_BAO_CustomValue'; } - return \_civicrm_api3_get_BAO($entityName); + $dao = self::getApiClass($entityName)::getInfo()['dao'] ?? NULL; + 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; + } + + /** + * @param $entityName + * @return string|\Civi\Api4\Generic\AbstractEntity + */ + public static function getApiClass($entityName) { + if (strpos($entityName, 'Custom_') === 0) { + return 'Civi\Api4\CustomValue'; + } + // Because "Case" is a reserved php keyword + $className = 'Civi\Api4\\' . ($entityName === 'Case' ? 'CiviCase' : $entityName); + return class_exists($className) ? $className : NULL; } /** @@ -66,9 +82,10 @@ class CoreUtil { $entityName = AllCoreTables::getBriefName(AllCoreTables::getClassForTable($tableName)); if (!$entityName) { $customGroup = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $tableName, 'name', 'table_name'); - $entityName = $customGroup ? "Custom_$customGroup" : NULL; + return $customGroup ? "Custom_$customGroup" : NULL; } - return $entityName; + // Verify class exists + return self::getApiClass($entityName) ? $entityName : NULL; } /** diff --git a/tests/phpunit/api/v4/Entity/ConformanceTest.php b/tests/phpunit/api/v4/Entity/ConformanceTest.php index eac1f5f9d5..a0529cbd68 100644 --- a/tests/phpunit/api/v4/Entity/ConformanceTest.php +++ b/tests/phpunit/api/v4/Entity/ConformanceTest.php @@ -21,6 +21,7 @@ namespace api\v4\Entity; use Civi\Api4\Entity; use api\v4\UnitTestCase; +use Civi\Api4\Utils\CoreUtil; /** * @group headless @@ -124,7 +125,7 @@ class ConformanceTest extends UnitTestCase { * @throws \API_Exception */ public function testConformance($entity): void { - $entityClass = 'Civi\Api4\\' . $entity; + $entityClass = CoreUtil::getApiClass($entity); $this->checkEntityInfo($entityClass); $actions = $this->checkActions($entityClass); -- 2.25.1