--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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"],
+ ];
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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,
+ ],
+ ],
+ ];
+ },
+];
--- /dev/null
+<?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']);
+ }
+
+}