Improve civicrm_api4 docblock and use it for help display in Explorer
authorColeman Watts <coleman@civicrm.org>
Thu, 9 Jan 2020 16:44:39 +0000 (11:44 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 9 Jan 2020 16:44:39 +0000 (11:44 -0500)
CRM/Api4/Page/Api4Explorer.php
Civi/Api4/Utils/ReflectionUtils.php
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js
api/api.php
css/api4-explorer.css

index ec5718c657fa53797555d69025215617f8864719..65af14d1475c01ac744a96a86923b86d73065d32 100644 (file)
 class CRM_Api4_Page_Api4Explorer extends CRM_Core_Page {
 
   public function run() {
+    $apiDoc = new ReflectionFunction('civicrm_api4');
     $vars = [
       'operators' => \CRM_Core_DAO::acceptedSQLOperators(),
       'basePath' => Civi::resources()->getUrl('civicrm'),
       'schema' => (array) \Civi\Api4\Entity::get()->setChain(['fields' => ['$name', 'getFields']])->execute(),
       'links' => (array) \Civi\Api4\Entity::getLinks()->execute(),
+      'docs' => \Civi\Api4\Utils\ReflectionUtils::parseDocBlock($apiDoc->getDocComment()),
     ];
     Civi::resources()
       ->addVars('api4', $vars)
index aa125662bb1552013c5e12e6014b7f80613068d2..58b3aab9519739266663209b324fdc25653e5b8a 100644 (file)
@@ -63,27 +63,44 @@ class ReflectionUtils {
    */
   public static function parseDocBlock($comment) {
     $info = [];
+    $param = NULL;
     foreach (preg_split("/((\r?\n)|(\r\n?))/", $comment) as $num => $line) {
       if (!$num || strpos($line, '*/') !== FALSE) {
         continue;
       }
-      $line = ltrim(trim($line), '* ');
+      $line = preg_replace('/[ ]+/', ' ', ltrim(trim($line), '* '));
       if (strpos($line, '@') === 0) {
         $words = explode(' ', $line);
-        $key = substr($words[0], 1);
+        $key = substr(array_shift($words), 1);
+        $param = NULL;
         if ($key == 'var') {
-          $info['type'] = explode('|', $words[1]);
+          $info['type'] = explode('|', $words[0]);
         }
         elseif ($key == 'options') {
-          $val = str_replace(', ', ',', implode(' ', array_slice($words, 1)));
+          $val = str_replace(', ', ',', implode(' ', $words));
           $info['options'] = explode(',', $val);
         }
+        elseif ($key == 'throws') {
+          $info[$key][] = implode(' ', $words);
+        }
+        elseif ($key == 'param' && $words) {
+          $type = $words[0][0] !== '$' ? explode('|', array_shift($words)) : NULL;
+          $param = rtrim(array_shift($words), '-:()/');
+          $info['params'][$param] = [
+            'type' => $type,
+            'description' => $words ? ltrim(implode(' ', $words), '-: ') : '',
+            'comment' => '',
+          ];
+        }
         else {
           // Unrecognized annotation, but we'll duly add it to the info array
-          $val = implode(' ', array_slice($words, 1));
+          $val = implode(' ', $words);
           $info[$key] = strlen($val) ? $val : TRUE;
         }
       }
+      elseif ($param) {
+        $info['params'][$param]['comment'] .= $line . "\n";
+      }
       elseif ($num == 1) {
         $info['description'] = $line;
       }
index 24326c695696e1b9410d85ad1230e00c5d35c77a..d016b467de8cfb4999743bae27b9059846665274 100644 (file)
@@ -2,7 +2,7 @@
   <div crm-ui-debug="availableParams"></div>
 
   <h1 crm-page-title>
-    {{ ts('CiviCRM API v4') }}{{ entity ? (' (' + entity + '::' + action + ')') : '' }}
+    {{ ts('CiviCRM APIv4') }}{{ entity ? (' (' + entity + '::' + action + ')') : '' }}
   </h1>
 
   <!--This warning will show if bootstrap is unavailable. Normally it will be hidden by the bootstrap .collapse class.-->
       <form name="api4-explorer" class="panel panel-default explorer-params-panel">
         <div class="panel-heading">
           <div class="form-inline">
-            <input class="collapsible-optgroups form-control" ng-model="entity" ng-disabled="!entities.length" ng-class="{loading: !entities.length}" crm-ui-select="{placeholder: ts('Entity'), data: entities}" />
-            <input class="collapsible-optgroups form-control" ng-model="action" ng-disabled="!entity || !actions.length" ng-class="{loading: entity && !actions.length}" crm-ui-select="{placeholder: ts('Action'), data: actions}" />
-            <input class="form-control api4-index" type="search" ng-model="index" ng-mouseenter="help('index', indexHelp)" ng-mouseleave="help()" placeholder="{{ ts('Index') }}" />
+            <span ng-mouseenter="help('entity', paramDoc('$entity'))" ng-mouseleave="help()">
+              <input class="collapsible-optgroups form-control" ng-model="entity" ng-disabled="!entities.length" ng-class="{loading: !entities.length}" crm-ui-select="{placeholder: ts('Entity'), data: entities}" />
+            </span>
+            <span ng-mouseenter="help('action', paramDoc('$action'))" ng-mouseleave="help()">
+              <input class="collapsible-optgroups form-control" ng-model="action" ng-disabled="!entity || !actions.length" ng-class="{loading: entity && !actions.length}" crm-ui-select="{placeholder: ts('Action'), data: actions}" />
+            </span>
+            <input class="form-control api4-index" type="search" ng-model="index" ng-mouseenter="help('index', paramDoc('$index'))" ng-mouseleave="help()" placeholder="{{ ts('Index') }}" />
             <button class="btn btn-success pull-right" crm-icon="fa-bolt" ng-disabled="!entity || !action || loading" ng-click="execute()">{{ ts('Execute') }}</button>
           </div>
         </div>
         <div class="panel-body">
           <h4>{{ helpContent.description }}</h4>
           <div ng-if="helpContent.comment">
-            <p ng-repeat='text in helpContent.comment.split("\n\n")'>{{ text }}</p>
+            <div ng-repeat='text in helpContent.comment.split("\n\n")'>
+              <p ng-if="text[0] !== '-' && text[0] !== '*'">{{ text }}</p>
+              <ul ng-if="text[0] === '-' || text[0] === '*'">
+                <li ng-repeat='item in text.split("\n")'>{{ item.substr(1) }}</li>
+              </ul>
+            </div>
           </div>
           <p ng-repeat="(key, item) in helpContent" ng-if="key !== 'description' && key !== 'comment'">
             <strong>{{ key }}:</strong> {{ item }}
index a37c12ad60cd6b8bcb1a24a6d224d4b21597ad42..2bfc82c9e4a1d79d7cf2e579106de18853312c0a 100644 (file)
@@ -31,6 +31,7 @@
     $scope.index = '';
     var getMetaParams = {},
       objectParams = {orderBy: 'ASC', values: '', chain: ['Entity', '', '{}']},
+      docs = CRM.vars.api4.docs,
       helpTitle = '',
       helpContent = {};
     $scope.helpTitle = '';
     }
 
     if (!$scope.entity) {
-      $scope.helpTitle = helpTitle = ts('Help');
-      $scope.helpContent = helpContent = {description: ts('Welcome to the api explorer.'), comment: ts('Select an entity to begin.')};
+      $scope.helpTitle = helpTitle = ts('APIv4 Explorer');
+      $scope.helpContent = helpContent = {description: docs.description, comment: docs.comment};
     } else if (!actions.length && !getEntity().actions) {
       getMetaParams.actions = [$scope.entity, 'getActions', {chain: {fields: [$scope.entity, 'getFields', {action: '$name'}]}}];
       fetchMeta();
       }
     });
 
-    $scope.indexHelp = {
-      description: ts('(string|int) Index results or select by index.'),
-      comment: ts('Pass a string to index the results by a field value. E.g. index: "name" will return an associative array with names as keys.') + '\n\n' +
-        ts('Pass an integer to return a single result; e.g. index: 0 will return the first result, 1 will return the second, and -1 will return the last.')
+    $scope.paramDoc = function(name) {
+      return docs.params[name];
     };
 
     $scope.$watch('params', writeCode, true);
index b1da77ed72d886743df8b9230fabc87e47c7ef40..cccad8b01740dcb399a87030c4b5f98b80c9f7f9 100644 (file)
@@ -24,15 +24,34 @@ function civicrm_api(string $entity = NULL, string $action, array $params, $extr
 }
 
 /**
- * Procedural wrapper for the OO api version 4.
+ * Calls the CiviCRM APIv4 with supplied parameters and returns a Result object.
  *
- * @param string $entity
- * @param string $action
- * @param array $params
- * @param string|int|array $index
- *   If $index is a string, the results array will be indexed by that key.
- *   If $index is an integer, only the result at that index will be returned.
- *   $index can also be a single-item array representing key|value pairs to be returned ex ['id' => 'title'].
+ * This API (Application Programming Interface) is used to access and manage data in CiviCRM.
+ *
+ * APIv4 is the latest stable version.
+ *
+ * @see http://example.com/civicrm/api4/explorer
+ *
+ * @param string $entity Name of the CiviCRM entity to access.
+ *   All entity names are capitalized CamelCase, e.g. "ContributionPage".
+ *   Most entities correspond to a database table (e.g. "Contact" is the table "civicrm_contact").
+ *   For a complete list of available entities, call civicrm_api4('Entity', 'get');
+ *
+ * @param string $action The "verb" of the api call.
+ *   For a complete list of actions for a given entity (e.g. Contact), call civicrm_api4('Contact', 'getActions');
+ *
+ * @param array $params An array of API input keyed by parameter name.
+ *   The easiest way to discover all available parameters is to visit the API Explorer on your CiviCRM site.
+ *   The API Explorer is listed in the CiviCRM menu under Support -> Developer.
+ *
+ * @param string|int|array $index Controls the Result array format.
+ *   By default the api Result contains a non-associative array of data. Passing an $index tells the api to
+ *   automatically reformat the array, depending on the variable type passed:
+ *
+ *     - Integer: return a single result array; e.g. index = 0 will return the first result, 1 will return the second, and -1 will return the last.
+ *     - String: index the results by a field value; e.g. index = "name" will return an associative array with the field 'name' as keys.
+ *     - Non-associative array: return a single value from each result; e.g. index = ['title'] will return a non-associative array of strings - the 'title' field from each result.
+ *     - Associative array: a combination of the previous two modes; e.g. index = ['name' => 'title'] will return an array of strings - the 'title' field keyed by the 'name' field.
  *
  * @return \Civi\Api4\Generic\Result
  * @throws \API_Exception
index 3ce3f37d9aece3bdd40748aea081b42b580a2221..9451bc26d60d864d4cc3568444aa0f06d994c3d5 100644 (file)
@@ -22,7 +22,7 @@
 #bootstrap-theme.api4-explorer-page > div > .panel {
   flex: 1;
   margin: 10px;
-  min-height: 400px;
+  min-height: 500px;
 }
 #bootstrap-theme.api4-explorer-page > div > form.panel {
   flex: 2;
   word-break: break-word;
 }
 
+/* because each help p is in a div, this undoes bootstrap removing margin from last-child */
+#bootstrap-theme.api4-explorer-page .explorer-help-panel .panel-body p {
+  margin-bottom: 10px;
+}
+
 #bootstrap-theme.api4-explorer-page form label {
   text-transform: capitalize;
 }