APIv4 - Add function to get api class name
authorColeman Watts <coleman@civicrm.org>
Mon, 22 Mar 2021 16:01:43 +0000 (12:01 -0400)
committerColeman Watts <coleman@civicrm.org>
Mon, 22 Mar 2021 20:14:21 +0000 (16:14 -0400)
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
Civi/Api4/Action/Entity/Get.php
Civi/Api4/Action/Entity/GetLinks.php
Civi/Api4/CustomValue.php
Civi/Api4/Generic/AbstractAction.php
Civi/Api4/Generic/AbstractEntity.php
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Utils/CoreUtil.php
tests/phpunit/api/v4/Entity/ConformanceTest.php

index 5d0457ef7d2933bed191870593027c0349824ff5..5ad9b9d21996acc4424cd11b620bdae0609f50b9 100644 (file)
@@ -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!)");
           }
index 0deee772fed5bd92823f2b7f1c36d69dfe6a8aa6..765a3703a9f4507e969ff0ffaac1b80b37978f08 100644 (file)
@@ -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']);
       }
index 5a4bc69785b2cda6c3ef6b95d37f505a6b2e3d7e..2d224da3355936ba034533575b9115e88564201a 100644 (file)
@@ -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(),
index 9cc2e5f753e937dadc2738b60a6157583b5d996b..ef7fdc168792c8bd582712b51180a329fccc0ed5 100644 (file)
@@ -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',
+      ],
+    ];
+  }
+
 }
index 2103f319c211e5c498667104fa2156459fbbdc3e..050fd5a6ca43757495e47d8d3e50a64ec1c85e3c 100644 (file)
@@ -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'],
index a2d5e117cd9c3b52e0154a1a06a4f34f673ffdd5..4db822d5f93cd88b753d80a843cda840f4ee1ad6 100644 (file)
@@ -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']);
index efaba5c29645878f93b87fc652f01e1e9d9dbb54..3a2d80efd3320f2fb66aa918034f7b2e207dc6df 100644 (file)
@@ -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;
index ebe79137970d4e7a01ee6e310af5b044f49e44e7..f7195d0d178c92dc5553069e93de762a1f60e8d0 100644 (file)
@@ -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;
   }
 
   /**
index eac1f5f9d5387c10dd3ffa923d11ffc3d00728d5..a0529cbd6809e325b5deaeeb782706d827864687 100644 (file)
@@ -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);