APIv4 - use entityTypes event to load custom records
authorColeman Watts <coleman@civicrm.org>
Thu, 24 Feb 2022 00:40:54 +0000 (19:40 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 24 Feb 2022 00:43:10 +0000 (19:43 -0500)
This standardizes things to use the ActionObjectProvider service more like it was intended,
with getEntityNames now returning what it's supposed to, and removing a direct-cache-access hack
from CoreUtil, in favor of using the service.

Civi/Api4/Action/Entity/Get.php
Civi/Api4/Provider/ActionObjectProvider.php
Civi/Api4/Provider/CustomEntityProvider.php [new file with mode: 0644]
Civi/Api4/Utils/CoreUtil.php
Civi/Core/Container.php

index 69d64e409b7b5694a5fe1e6b4653aea94b38d7db..9a8ccbb5f979f0845508a9d1ab3f9fe7dea741b3 100644 (file)
 
 namespace Civi\Api4\Action\Entity;
 
-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.
  *
@@ -33,111 +28,11 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
   protected $includeCustom;
 
   /**
-   * Scan all api directories to discover entities
+   * Returns all APIv4 entities
    */
   protected function getRecords() {
-    $cache = \Civi::cache('metadata');
-    $entities = $cache->get('api4.entities.info', []);
-
-    if (!$entities) {
-      // Load entities declared in API files
-      foreach ($this->getAllApiClasses() as $className) {
-        $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);
-    }
-
-    return $entities;
-  }
-
-  /**
-   * @param \Civi\Api4\Generic\AbstractEntity $className
-   * @param array $entities
-   */
-  private function loadEntity($className, array &$entities) {
-    $info = $className::getInfo();
-    $daoName = $info['dao'] ?? NULL;
-    // Only include DAO entities from enabled components
-    if (!$daoName || !defined("{$daoName}::COMPONENT") || \CRM_Core_Component::isEnabled($daoName::COMPONENT)) {
-      $entities[$info['name']] = $info;
-    }
-  }
-
-  /**
-   * @return \Civi\Api4\Generic\AbstractEntity[]
-   */
-  private function getAllApiClasses() {
-    $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;
-          }
-        }
-      }
-    }
-    return $classNames;
-  }
-
-  /**
-   * Get custom-field pseudo-entities
-   *
-   * @return array[]
-   */
-  private function getCustomEntities() {
-    $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);
-      }
-    }
-    return $entities;
-  }
-
-  /**
-   * Convert html to plain text.
-   *
-   * @param $input
-   * @return mixed
-   */
-  private function plainTextify($input) {
-    return html_entity_decode(strip_tags($input), ENT_QUOTES | ENT_HTML5, 'UTF-8');
+    $provider = \Civi::service('action_object_provider');
+    return $provider->getEntities();
   }
 
 }
index ced568e7b795b5c48957b8829515b247476f6450..2c284e3cc04f2930faf337fbdde9369a007b71ed 100644 (file)
@@ -16,6 +16,7 @@ use Civi\API\Provider\ProviderInterface;
 use Civi\Api4\Generic\AbstractAction;
 use Civi\API\Events;
 use Civi\Api4\Utils\ReflectionUtils;
+use Civi\Core\Event\GenericHookEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -127,8 +128,7 @@ class ActionObjectProvider implements EventSubscriberInterface, ProviderInterfac
    * @return array
    */
   public function getEntityNames($version) {
-    /** FIXME */
-    return [];
+    return $version === 4 ? array_keys($this->getEntities()) : [];
   }
 
   /**
@@ -142,4 +142,62 @@ class ActionObjectProvider implements EventSubscriberInterface, ProviderInterfac
     return [];
   }
 
+  /**
+   * Get all APIv4 entities
+   */
+  public function getEntities() {
+    $cache = \Civi::cache('metadata');
+    $entities = $cache->get('api4.entities.info', []);
+
+    if (!$entities) {
+      // Load entities declared in API files
+      foreach ($this->getAllApiClasses() as $className) {
+        $this->loadEntity($className, $entities);
+      }
+      // 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);
+    }
+
+    return $entities;
+  }
+
+  /**
+   * @param \Civi\Api4\Generic\AbstractEntity $className
+   * @param array $entities
+   */
+  private function loadEntity($className, array &$entities) {
+    $info = $className::getInfo();
+    $daoName = $info['dao'] ?? NULL;
+    // Only include DAO entities from enabled components
+    if (!$daoName || !defined("{$daoName}::COMPONENT") || \CRM_Core_Component::isEnabled($daoName::COMPONENT)) {
+      $entities[$info['name']] = $info;
+    }
+  }
+
+  /**
+   * Scan all api directories to discover entities
+   * @return \Civi\Api4\Generic\AbstractEntity[]
+   */
+  private function getAllApiClasses() {
+    $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;
+          }
+        }
+      }
+    }
+    return $classNames;
+  }
+
 }
diff --git a/Civi/Api4/Provider/CustomEntityProvider.php b/Civi/Api4/Provider/CustomEntityProvider.php
new file mode 100644 (file)
index 0000000..7763cd0
--- /dev/null
@@ -0,0 +1,66 @@
+<?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\Provider;
+
+use Civi\Api4\CustomValue;
+use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable;
+use Civi\Api4\Utils\CoreUtil;
+use Civi\Core\Event\GenericHookEvent;
+
+class CustomEntityProvider {
+
+  /**
+   * Get custom-field pseudo-entities
+   */
+  public static function addCustomEntities(GenericHookEvent $e) {
+    $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));
+      $e->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)) {
+        $e->entities[$fieldName]['icon'] = $group->icon;
+      }
+      if (!empty($group->help_pre)) {
+        $e->entities[$fieldName]['comment'] = self::plainTextify($group->help_pre);
+      }
+      if (!empty($group->help_post)) {
+        $pre = empty($e->entities[$fieldName]['comment']) ? '' : $e->entities[$fieldName]['comment'] . "\n\n";
+        $e->entities[$fieldName]['comment'] = $pre . self::plainTextify($group->help_post);
+      }
+    }
+  }
+
+  /**
+   * Convert html to plain text.
+   *
+   * @param $input
+   * @return mixed
+   */
+  private static function plainTextify($input) {
+    return html_entity_decode(strip_tags($input), ENT_QUOTES | ENT_HTML5, 'UTF-8');
+  }
+
+}
index d6d6320ca41a009e9c8bce7f22669d4924aff137..fbbde7780ab5b4c0d3be27f160a4df616d9dff8b 100644 (file)
@@ -14,7 +14,6 @@ namespace Civi\Api4\Utils;
 
 use Civi\API\Exception\NotImplementedException;
 use Civi\API\Request;
-use Civi\Api4\Entity;
 use Civi\Api4\Event\CreateApi4RequestEvent;
 use CRM_Core_DAO_AllCoreTables as AllCoreTables;
 
@@ -53,20 +52,8 @@ class CoreUtil {
    * @return mixed
    */
   public static function getInfoItem(string $entityName, string $keyToReturn) {
-    // Because this function might be called thousands of times per request, read directly
-    // from the cache set by Apiv4 Entity.get to avoid the processing overhead of the API wrapper.
-    $cached = \Civi::cache('metadata')->get('api4.entities.info');
-    if ($cached) {
-      $info = $cached[$entityName] ?? NULL;
-    }
-    // If the cache is empty, calling Entity.get will populate it and we'll use it next time.
-    else {
-      $info = Entity::get(FALSE)
-        ->addWhere('name', '=', $entityName)
-        ->addSelect($keyToReturn)
-        ->execute()->first();
-    }
-    return $info ? $info[$keyToReturn] ?? NULL : NULL;
+    $provider = \Civi::service('action_object_provider');
+    return $provider->getEntities()[$entityName][$keyToReturn] ?? NULL;
   }
 
   /**
index a6e6d555afbbf8bf86ada284423973403dcbf2c2..c7573f0d0934b074592b14b274963ae4d73cf08e 100644 (file)
@@ -406,6 +406,7 @@ class Container {
 
     $dispatcher->addListener('civi.api4.validate', $aliasMethodEvent('civi.api4.validate', 'getEntityName'), 100);
     $dispatcher->addListener('civi.api4.authorizeRecord', $aliasMethodEvent('civi.api4.authorizeRecord', 'getEntityName'), 100);
+    $dispatcher->addListener('civi.api4.entityTypes', ['\Civi\Api4\Provider\CustomEntityProvider', 'addCustomEntities'], 100);
 
     $dispatcher->addListener('civi.core.install', ['\Civi\Core\InstallationCanary', 'check']);
     $dispatcher->addListener('civi.core.install', ['\Civi\Core\DatabaseInitializer', 'initialize']);