WorkflowMessageExample - Add API for searching examples
authorTim Otten <totten@civicrm.org>
Tue, 27 Jul 2021 05:20:35 +0000 (22:20 -0700)
committerTim Otten <totten@civicrm.org>
Mon, 13 Sep 2021 22:31:53 +0000 (15:31 -0700)
Civi/Api4/Action/WorkflowMessageExample/Get.php [new file with mode: 0644]
Civi/Api4/WorkflowMessageExample.php [new file with mode: 0644]
Civi/WorkflowMessage/Examples.php [new file with mode: 0644]
Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php [new file with mode: 0644]
tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php [new file with mode: 0644]

diff --git a/Civi/Api4/Action/WorkflowMessageExample/Get.php b/Civi/Api4/Action/WorkflowMessageExample/Get.php
new file mode 100644 (file)
index 0000000..c084fc7
--- /dev/null
@@ -0,0 +1,65 @@
+<?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\Action\WorkflowMessageExample;
+
+use Civi\Api4\Generic\BasicGetAction;
+use Civi\Api4\Generic\Result;
+use Civi\WorkflowMessage\Examples;
+
+/**
+ * Get a list of example data-sets.
+ *
+ * Examples are generated by scanning `*.ex.php` files. The scanner caches
+ * metadata fields (`name`, `title`, `tags`, `file`) to avoid extraneous scanning, but
+ * substantive fields (`data`) are computed as-needed.
+ *
+ * FIXME: When we have an update for dev-docs, include a `@link` here.
+ */
+class Get extends BasicGetAction {
+
+  /**
+   * @var \Civi\WorkflowMessage\Examples
+   */
+  private $_scanner;
+
+  public function _run(Result $result) {
+    if ($this->select !== [] && !in_array('name', $this->select)) {
+      $this->select[] = 'name';
+    }
+    parent::_run($result);
+  }
+
+  protected function getRecords() {
+    $this->_scanner = new Examples();
+    $all = $this->_scanner->findAll();
+    foreach ($all as &$example) {
+      $example['tags'] = !empty($example['tags']) ? \CRM_Utils_Array::implodePadded($example['tags']) : '';
+    }
+    return $all;
+  }
+
+  protected function selectArray($values) {
+    $result = parent::selectArray($values);
+
+    $heavyFields = array_intersect(['data', 'asserts'], $this->select ?: []);
+    if (!empty($heavyFields)) {
+      foreach ($result as &$item) {
+        $heavy = $this->_scanner->getHeavy($item['name']);
+        $item = array_merge($item, \CRM_Utils_Array::subset($heavy, $heavyFields));
+      }
+    }
+
+    return $result;
+  }
+
+}
diff --git a/Civi/Api4/WorkflowMessageExample.php b/Civi/Api4/WorkflowMessageExample.php
new file mode 100644 (file)
index 0000000..6b05bc4
--- /dev/null
@@ -0,0 +1,93 @@
+<?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;
+
+/**
+ * Search for example data.
+ *
+ * @searchable none
+ * @since 5.43
+ * @package Civi\Api4
+ */
+class WorkflowMessageExample extends \Civi\Api4\Generic\AbstractEntity {
+
+  /**
+   * @param bool $checkPermissions
+   * @return Generic\AbstractGetAction
+   */
+  public static function get($checkPermissions = TRUE) {
+    return (new Action\WorkflowMessageExample\Get(__CLASS__, __FILE__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
+  /**
+   * @param bool $checkPermissions
+   * @return Generic\BasicGetFieldsAction
+   */
+  public static function getFields($checkPermissions = TRUE) {
+    return (new Generic\BasicGetFieldsAction(__CLASS__, __FUNCTION__, function () {
+      return [
+        [
+          'name' => 'name',
+          'title' => 'Example Name',
+          'data_type' => 'String',
+        ],
+        [
+          'name' => 'title',
+          'title' => 'Example Title',
+          'data_type' => 'String',
+        ],
+        [
+          'name' => 'workflow',
+          'title' => 'Workflow Name',
+          'data_type' => 'String',
+        ],
+        [
+          'name' => 'file',
+          'title' => 'File Path',
+          'data_type' => 'String',
+          'description' => 'If the example is loaded from a file, this is the location.',
+        ],
+        [
+          'name' => 'tags',
+          'title' => 'Tags',
+          'data_type' => 'String',
+          'serialize' => \CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND,
+        ],
+        [
+          'name' => 'data',
+          'title' => 'Example data',
+          'data_type' => 'String',
+          'serialize' => \CRM_Core_DAO::SERIALIZE_JSON,
+        ],
+        [
+          'name' => 'asserts',
+          'title' => 'Test assertions',
+          'data_type' => 'String',
+          'serialize' => \CRM_Core_DAO::SERIALIZE_JSON,
+        ],
+      ];
+    }))->setCheckPermissions($checkPermissions);
+  }
+
+  /**
+   * @return array
+   */
+  public static function permissions() {
+    return [
+      // FIXME: Perhaps use 'edit message templates' or similar?
+      "meta" => ["access CiviCRM"],
+      "default" => ["administer CiviCRM"],
+    ];
+  }
+
+}
diff --git a/Civi/WorkflowMessage/Examples.php b/Civi/WorkflowMessage/Examples.php
new file mode 100644 (file)
index 0000000..52ec6da
--- /dev/null
@@ -0,0 +1,172 @@
+<?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\WorkflowMessage;
+
+use Civi\Test\Invasive;
+
+/**
+ * @internal
+ */
+class Examples {
+
+  /**
+   * @var \CRM_Utils_Cache_Interface
+   */
+  private $cache;
+
+  /**
+   * @var string
+   */
+  private $cacheKey;
+
+  private $heavyCache = [];
+
+  /**
+   * ExampleScanner constructor.
+   * @param \CRM_Utils_Cache_Interface|NULL $cache
+   */
+  public function __construct(?\CRM_Utils_Cache_Interface $cache = NULL) {
+    $this->cache = $cache ?: \Civi::cache('short' /* long */);
+    $this->cacheKey = \CRM_Utils_String::munge(__CLASS__);
+  }
+
+  /**
+   * Get a list of all examples, including basic metadata (name, title, workflow).
+   *
+   * @return array
+   *   Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]]
+   * @throws \ReflectionException
+   */
+  public function findAll(): array {
+    $all = $this->cache->get($this->cacheKey);
+    if ($all === NULL) {
+      $all = [];
+      $wfClasses = Invasive::call([WorkflowMessage::class, 'getWorkflowNameClassMap']);
+      foreach ($wfClasses as $workflow => $class) {
+        try {
+          $classFile = (new \ReflectionClass($class))->getFileName();
+        }
+        catch (\ReflectionException $e) {
+          throw new \RuntimeException("Failed to locate workflow class ($class)", 0, $e);
+        }
+        $classDir = preg_replace('/\.php$/', '', $classFile);
+        if (is_dir($classDir)) {
+          $all = array_merge($all, $this->scanDir($classDir, $workflow));
+        }
+      }
+    }
+    return $all;
+  }
+
+  /**
+   * @param string $dir
+   * @param string $workflow
+   * @return array
+   *   Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]]
+   */
+  protected function scanDir($dir, $workflow) {
+    $all = [];
+    $files = (array) glob($dir . "/*.ex.php");
+    foreach ($files as $file) {
+      $name = $workflow . '.' . preg_replace('/\.ex.php/', '', basename($file));
+      $scanRecord = [
+        'name' => $name,
+        'title' => $name,
+        'workflow' => $workflow,
+        'tags' => [],
+        'file' => $file,
+        // ^^ relativize?
+      ];
+      $rawRecord = $this->loadFile($file);
+      $all[$name] = array_merge($scanRecord, \CRM_Utils_Array::subset($rawRecord, ['name', 'title', 'workflow', 'tags']));
+    }
+    return $all;
+  }
+
+  /**
+   * Load an example data file (based on its file path).
+   *
+   * @param string $_exFile
+   *   Loadable PHP filename.
+   * @return array
+   *   The raw/unevaluated dataset.
+   */
+  public function loadFile($_exFile): array {
+    // Isolate variables.
+    // If you need export values, use something like `extract($_tplVars);`
+    return require $_exFile;
+  }
+
+  /**
+   * Get example data (based on its symbolic name).
+   *
+   * @param string|string[] $nameOrPath
+   *   Ex: "foo" -> load all the data from example "foo"
+   *   Ex: "foo.b.a.r" -> load the example "foo" and pull out the data from $foo['b']['a']['r']
+   *   Ex: ["foo","b","a","r"] - Same as above. But there is no ambiguity with nested dots.
+   * @return array
+   */
+  public function get($nameOrPath) {
+    $path = is_array($nameOrPath) ? $nameOrPath : explode('.', $nameOrPath);
+    $exampleName = array_shift($path) . '.' . array_shift($path);
+    return \CRM_Utils_Array::pathGet($this->getHeavy($exampleName), $path);
+  }
+
+  /**
+   * Get one of the "heavy" properties.
+   *
+   * @param string $name
+   * @return array
+   * @throws \ReflectionException
+   */
+  public function getHeavy(string $name): array {
+    if (isset($this->heavyCache[$name])) {
+      return $this->heavyCache[$name];
+
+    }
+    $all = $this->findAll();
+    if (!isset($all[$name])) {
+      throw new \RuntimeException("Cannot load example ($name)");
+    }
+    $heavyRecord = $all[$name];
+    $loaded = $this->loadFile($all[$name]['file']);
+    foreach (['data', 'asserts'] as $heavyField) {
+      if (isset($loaded[$heavyField])) {
+        $heavyRecord[$heavyField] = $loaded[$heavyField] instanceof \Closure
+          ? call_user_func($loaded[$heavyField], $this)
+          : $loaded[$heavyField];
+      }
+    }
+
+    $this->heavyCache[$name] = $heavyRecord;
+    return $this->heavyCache[$name];
+  }
+
+  /**
+   * Get an example and merge/extend it with more data.
+   *
+   * @param string|string[] $nameOrPath
+   *   Ex: "foo" -> load all the data from example "foo"
+   *   Ex: "foo.b.a.r" -> load the example "foo" and pull out the data from $foo['b']['a']['r']
+   *   Ex: ["foo","b","a","r"] - Same as above. But there is no ambiguity with nested dots.
+   * @param array $overrides
+   *   Data to add.
+   * @return array
+   *   The result of merging the original example with the $overrides.
+   */
+  public function extend($nameOrPath, $overrides = []) {
+    $data = $this->get($nameOrPath);
+    \CRM_Utils_Array::extend($data, $overrides);
+    return $data;
+  }
+
+}
diff --git a/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php b/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php
new file mode 100644 (file)
index 0000000..a2ccd32
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+return [
+  'tags' => [],
+  'data' => function(\Civi\WorkflowMessage\Examples $examples) {
+    return [
+      'modelProps' => [
+        'contact' => [
+          'contact_id' => '100',
+          'contact_type' => 'Individual',
+          'contact_sub_type' => NULL,
+          'sort_name' => 'D\u00edaz, Alex',
+          'display_name' => 'Dr. Alex D\u00edaz',
+          'do_not_email' => '1',
+          'do_not_phone' => '1',
+          'do_not_mail' => '0',
+          'do_not_sms' => '0',
+          'do_not_trade' => '0',
+          'is_opt_out' => '0',
+          'legal_identifier' => NULL,
+          'external_identifier' => NULL,
+          'nick_name' => NULL,
+          'legal_name' => NULL,
+          'image_URL' => NULL,
+          'preferred_communication_method' => NULL,
+          'preferred_language' => NULL,
+          'preferred_mail_format' => 'Both',
+          'first_name' => 'Alex',
+          'middle_name' => '',
+          'last_name' => 'D\u00edaz',
+          'prefix_id' => '4',
+          'suffix_id' => NULL,
+          'formal_title' => NULL,
+          'communication_style_id' => NULL,
+          'job_title' => NULL,
+          'gender_id' => '1',
+          'birth_date' => '1994-04-21',
+          'is_deceased' => '0',
+          'deceased_date' => NULL,
+          'household_name' => NULL,
+          'organization_name' => NULL,
+          'sic_code' => NULL,
+          'contact_is_deleted' => '0',
+          'current_employer' => NULL,
+          'address_id' => NULL,
+          'street_address' => NULL,
+          'supplemental_address_1' => NULL,
+          'supplemental_address_2' => NULL,
+          'supplemental_address_3' => NULL,
+          'city' => NULL,
+          'postal_code_suffix' => NULL,
+          'postal_code' => NULL,
+          'geo_code_1' => NULL,
+          'geo_code_2' => NULL,
+          'state_province_id' => NULL,
+          'country_id' => NULL,
+          'phone_id' => '7',
+          'phone_type_id' => '1',
+          'phone' => '293-6934',
+          'email_id' => '7',
+          'email' => 'daz.alex67@testing.net',
+          'on_hold' => '0',
+          'im_id' => NULL,
+          'provider_id' => NULL,
+          'im' => NULL,
+          'worldregion_id' => NULL,
+          'world_region' => NULL,
+          'languages' => NULL,
+          'individual_prefix' => 'Dr.',
+          'individual_suffix' => NULL,
+          'communication_style' => NULL,
+          'gender' => 'Female',
+          'state_province_name' => NULL,
+          'state_province' => NULL,
+          'country' => NULL,
+        ],
+      ],
+    ];
+  },
+];
diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php b/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php
new file mode 100644 (file)
index 0000000..3965945
--- /dev/null
@@ -0,0 +1,59 @@
+<?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       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace api\v4\Entity;
+
+use api\v4\UnitTestCase;
+
+/**
+ * @group headless
+ */
+class WorkflowMessageExampleTest extends UnitTestCase {
+
+  /**
+   * Basic canary test fetching a specific example.
+   *
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  public function testGet() {
+    $file = \Civi::paths()->getPath('[civicrm.root]/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php');
+    $workflow = 'generic';
+    $name = 'generic.alex';
+
+    $this->assertTrue(file_exists($file), "Expect find canary file ($file)");
+
+    $get = \Civi\Api4\WorkflowMessageExample::get()
+      ->addWhere('name', '=', $name)
+      ->execute()
+      ->single();
+    $this->assertEquals($workflow, $get['workflow']);
+    $this->assertTrue(!isset($get['data']));
+    $this->assertTrue(!isset($get['asserts']));
+
+    $get = \Civi\Api4\WorkflowMessageExample::get()
+      ->addWhere('name', '=', $name)
+      ->addSelect('workflow', 'data')
+      ->execute()
+      ->single();
+    $this->assertEquals($workflow, $get['workflow']);
+    $this->assertEquals(100, $get['data']['modelProps']['contact']['contact_id']);
+  }
+
+}