AfformScanner - Use `ang/*.aff.json` file-naming convention
authorTim Otten <totten@civicrm.org>
Sat, 9 Feb 2019 00:56:03 +0000 (19:56 -0500)
committerCiviCRM <info@civicrm.org>
Wed, 16 Sep 2020 02:13:17 +0000 (19:13 -0700)
__Before__: Scan each `$extension` for `$extension/afform/*/meta.json`

__After__: Scan each `$extension` for `$extension/ang/*.aff.json`

__Comments__: This is a significant breaking change for v0.2.

It preserves some key design-goals from the old naming convention

* The symbol in HTML (e.g.  `afform-email`) should have symmetry with the file-name (e.g.  `afform/Email/*` or `afform-email.*`)
* The file-name in the base-code provided by an extension should match the file-name in the local override folder.

However, the new naming convention also:

* Makes it easier downstream to choose their own prefixes. We're not boxed-in or hard-coded to `afform-`.
  This will be useful, e.g., with distinguishing the stdlib from the business-forms that use the stdlib.
* Puts Afform-Angular content in the same folder structure as Regular-Angular content.

ext/afform/core/CRM/Afform/AfformScanner.php
ext/afform/core/Civi/Api4/Action/Afform/Revert.php
ext/afform/core/Civi/Api4/Traits/AfformCrudTrait.php
ext/afform/core/afform.php
ext/afform/core/tests/phpunit/CRM/Afform/UtilTest.php [new file with mode: 0644]

index fb03cffbc4a15d5dfd0c67ae86afa65d267ddfdc..6c64f988e0503ca600028a84572392f1d50ec7c7 100644 (file)
@@ -10,7 +10,7 @@
  */
 class CRM_Afform_AfformScanner {
 
-  const METADATA_FILE = 'meta.json';
+  const METADATA_FILE = 'aff.json';
 
   const DEFAULT_REQUIRES = 'afformCore';
 
@@ -28,6 +28,7 @@ class CRM_Afform_AfformScanner {
       'group' => md5('afform_' . CRM_Core_Config_Runtime::getId() . $this->getSiteLocalPath()),
       'prefetch' => FALSE,
     ]);
+    // $this->cache = new CRM_Utils_Cache_Arraycache([]);
   }
 
   /**
@@ -51,7 +52,7 @@ class CRM_Afform_AfformScanner {
     foreach ($mapper->getModules() as $module) {
       /** @var $module CRM_Core_Module */
       if ($module->is_active) {
-        $this->appendFilePaths($paths, dirname($mapper->keyToPath($module->name)) . DIRECTORY_SEPARATOR . 'afform', 20);
+        $this->appendFilePaths($paths, dirname($mapper->keyToPath($module->name)) . DIRECTORY_SEPARATOR . 'ang', 20);
       }
     }
 
@@ -66,18 +67,18 @@ class CRM_Afform_AfformScanner {
    *
    * @param string $formName
    *   Ex: 'view-individual'
-   * @param string $subFile
-   *   Ex: 'meta.json'
+   * @param string $suffix
+   *   Ex: 'aff.json'
    * @return string|NULL
-   *   Ex: '/var/www/sites/default/files/civicrm/afform/view-individual'
+   *   Ex: '/var/www/sites/default/files/civicrm/afform/view-individual.aff.json'
    */
-  public function findFilePath($formName, $subFile) {
+  public function findFilePath($formName, $suffix) {
     $paths = $this->findFilePaths();
 
     if (isset($paths[$formName])) {
       foreach ($paths[$formName] as $path) {
-        if (file_exists($path . DIRECTORY_SEPARATOR . $subFile)) {
-          return $path . DIRECTORY_SEPARATOR . $subFile;
+        if (file_exists($path . '.' . $suffix)) {
+          return $path . '.' . $suffix;
         }
       }
     }
@@ -92,12 +93,12 @@ class CRM_Afform_AfformScanner {
    * @param string $formName
    *   Ex: 'view-individual'
    * @param string $file
-   *   Ex: 'meta.json'
+   *   Ex: 'aff.json'
    * @return string|NULL
-   *   Ex: '/var/www/sites/default/files/civicrm/afform/view-individual'
+   *   Ex: '/var/www/sites/default/files/civicrm/afform/view-individual.aff.json'
    */
   public function createSiteLocalPath($formName, $file) {
-    return $this->getSiteLocalPath() . DIRECTORY_SEPARATOR . $formName . DIRECTORY_SEPARATOR . $file;
+    return $this->getSiteLocalPath() . DIRECTORY_SEPARATOR . $formName . '.' . $file;
   }
 
   public function clear() {
@@ -111,7 +112,7 @@ class CRM_Afform_AfformScanner {
    *   Ex: 'view-individual'
    * @return array
    *   An array with some mix of the following keys: name, title, description, client_route, server_route, requires.
-   *   NOTE: This is only data available in meta.json. It does *NOT* include layout.
+   *   NOTE: This is only data available in *.aff.json. It does *NOT* include layout.
    *   Ex: [
    *     'name' => 'view-individual',
    *     'title' => 'View an individual contact',
@@ -142,7 +143,7 @@ class CRM_Afform_AfformScanner {
    *
    * @return array
    *   A list of all forms, keyed by form name.
-   *   NOTE: This is only data available in meta.json. It does *NOT* include layout.
+   *   NOTE: This is only data available in *.aff.json. It does *NOT* include layout.
    *   Ex: ['view-individual' => ['title' => 'View an individual contact', ...]]
    */
   public function getMetas() {
@@ -156,21 +157,18 @@ class CRM_Afform_AfformScanner {
   /**
    * @param array $formPaths
    *   List of all form paths.
-   *   Ex: ['foo' => [0 => '/var/www/org.example.foobar/afform//foo']]
+   *   Ex: ['foo' => [0 => '/var/www/org.example.foobar/ang']]
    * @param string $parent
    *   Ex: '/var/www/org.example.foobar/afform/'
    * @param int $priority
    *   Lower priority files override higher priority files.
    */
   private function appendFilePaths(&$formPaths, $parent, $priority) {
-    $parent = CRM_Utils_File::addTrailingSlash($parent);
-    if (is_dir($parent) && $handle = opendir($parent)) {
-      while (FALSE !== ($entry = readdir($handle))) {
-        if ($entry{0} !== '.' && is_dir($parent . $entry)) {
-          $formPaths[$entry][$priority] = $parent . $entry;
-          ksort($formPaths[$entry]);
-        }
-      }
+    $files = (array) glob("$parent/*." . self::METADATA_FILE);
+    foreach ($files as $file) {
+      $name = _afform_angular_module_name(preg_replace('/\.aff\.json$/', '', basename($file)));
+      $formPaths[$name][$priority] = preg_replace('/\.aff\.json$/', '', $file);
+      ksort($formPaths[$name]);
     }
   }
 
@@ -183,7 +181,7 @@ class CRM_Afform_AfformScanner {
   private function getSiteLocalPath() {
     // TODO Allow a setting override.
     // return Civi::paths()->getPath(Civi::settings()->get('afformPath'));
-    return Civi::paths()->getPath('[civicrm.files]/afform');
+    return Civi::paths()->getPath('[civicrm.files]/ang');
   }
 
 }
index 206d5addd68dbb05c4513e64609b35f4f36e6735..895890adfec363ecabdefdc96bd6f4baec15fc62 100644 (file)
@@ -13,7 +13,7 @@ class Revert extends Get {
 
     parent::_run($result);
 
-    $files = [\CRM_Afform_AfformScanner::METADATA_FILE, 'layout.html'];
+    $files = [\CRM_Afform_AfformScanner::METADATA_FILE, 'aff.html'];
 
     foreach ($result as $afform) {
       foreach ($files as $file) {
index 668b90fbd2990ecd5071767100a7bd1a2eee6b72..6360c9f05d70711df2e527d2de417bbdf8574926 100644 (file)
@@ -23,7 +23,7 @@ trait AfformCrudTrait {
     $values = [];
     foreach ($names as $name) {
       $record = $scanner->getMeta($name);
-      $layout = $scanner->findFilePath($name, 'layout.html');
+      $layout = $scanner->findFilePath($name, 'aff.html');
       if ($layout) {
         // FIXME check for file existence+substance+validity
         $record['layout'] = $converter->convertHtmlToArray(file_get_contents($layout));
@@ -56,15 +56,15 @@ trait AfformCrudTrait {
     // FIXME validate all field data.
     $updates = _afform_fields_filter($record);
 
-    // Create or update layout.html.
+    // Create or update aff.html.
     if (isset($updates['layout'])) {
-      $layoutPath = $scanner->createSiteLocalPath($name, 'layout.html');
+      $layoutPath = $scanner->createSiteLocalPath($name, 'aff.html');
       \ CRM_Utils_File::createDir(dirname($layoutPath));
       file_put_contents($layoutPath, $converter->convertArrayToHtml($updates['layout']));
       // FIXME check for writability then success. Report errors.
     }
 
-    // Create or update meta.json.
+    // Create or update *.aff.json.
     $orig = \Civi\Api4\Afform::get()
       ->setCheckPermissions($this->getCheckPermissions())
       ->addWhere('name', '=', $name)
index ad8630867f784189b94e94baea8280fbd59766d5..09700d2fef0b604496db018023422106d50bcbcb 100644 (file)
@@ -156,7 +156,7 @@ function afform_civicrm_angularModules(&$angularModules) {
   $names = array_keys($scanner->findFilePaths());
   foreach ($names as $name) {
     $meta = $scanner->getMeta($name);
-    $angularModules[_afform_angular_module_name($name)] = [
+    $angularModules[_afform_angular_module_name($name, 'camel')] = [
       'ext' => E::LONG_NAME,
       'js' => ['assetBuilder://afform.js?name=' . urlencode($name)],
       'requires' => $meta['requires'],
@@ -213,10 +213,10 @@ function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
 
   $smarty = CRM_Core_Smarty::singleton();
   $smarty->assign('afform', [
-    'camel' => _afform_angular_module_name($name),
+    'camel' => _afform_angular_module_name($name, 'camel'),
     'meta' => $meta,
     'metaJson' => json_encode($meta),
-    'layout' => file_get_contents($scanner->findFilePath($name, 'layout.html'))
+    'layout' => file_get_contents($scanner->findFilePath($name, 'aff.html'))
   ]);
   $mimeType = 'text/javascript';
   $content = $smarty->fetch('afform/AfformAngularModule.tpl');
@@ -247,21 +247,24 @@ function afform_civicrm_alterMenu(&$items) {
 }
 
 /**
- * @param string $name
- *   Ex: fooBar
+ * @param string $fileBaseName
+ *   Ex: foo-bar
  * @param string $format
  *   'camel' or 'dash'.
  * @return string
  *   Ex: 'FooBar' or 'foo-bar'.
  */
-function _afform_angular_module_name($name, $format = 'camel') {
+function _afform_angular_module_name($fileBaseName, $format = 'camel') {
   switch ($format) {
     case 'camel':
-      return 'afform' . strtoupper($name{0}) . substr($name, 1);
+      $camelCase = '';
+      foreach (explode('-', $fileBaseName) as $shortNamePart) {
+        $camelCase .= ucfirst($shortNamePart);
+      }
+      return strtolower($camelCase{0}) . substr($camelCase, 1);
 
     case 'dash':
-      $camel = _afform_angular_module_name($name, 'camel');
-      return strtolower(implode('-', array_filter(preg_split('/(?=[A-Z])/', $camel))));
+      return strtolower(implode('-', array_filter(preg_split('/(?=[A-Z])/', $fileBaseName))));
 
     default:
       throw new \Exception("Unrecognized format");
diff --git a/ext/afform/core/tests/phpunit/CRM/Afform/UtilTest.php b/ext/afform/core/tests/phpunit/CRM/Afform/UtilTest.php
new file mode 100644 (file)
index 0000000..32263e1
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+use Civi\Test\HeadlessInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * @group headless
+ */
+class CRM_Afform_UtilTest extends \PHPUnit_Framework_TestCase implements HeadlessInterface, TransactionalInterface {
+
+  /**
+   * Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+   * See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md
+   */
+  public function setUpHeadless() {
+    return \Civi\Test::headless()
+      ->installMe(__DIR__)
+      ->install(['org.civicrm.api4'])
+      ->apply();
+  }
+
+  public function getNameExamples() {
+    $exs = [];
+    $exs[] = ['ab-cd-ef', 'camel', 'abCdEf'];
+    $exs[] = ['abCd', 'camel', 'abCd'];
+    $exs[] = ['AbCd', 'camel', 'abCd'];
+    $exs[] = ['ab-cd', 'dash', 'ab-cd'];
+    $exs[] = ['abCd', 'dash', 'ab-cd'];
+    $exs[] = ['AbCd', 'dash', 'ab-cd'];
+
+    $exs[] = ['ab-cd-ef23', 'camel', 'abCdEf23'];
+    $exs[] = ['abCd23', 'camel', 'abCd23'];
+    $exs[] = ['AbCd23', 'camel', 'abCd23'];
+    $exs[] = ['ab-cd23', 'dash', 'ab-cd23'];
+    $exs[] = ['abCd23', 'dash', 'ab-cd23'];
+    $exs[] = ['AbCd23', 'dash', 'ab-cd23'];
+
+    return $exs;
+  }
+
+  /**
+   * @param $inputFileName
+   * @param $toFormat
+   * @param $expected
+   * @dataProvider getNameExamples
+   */
+  public function testNameConversion($inputFileName, $toFormat, $expected) {
+    $actual = _afform_angular_module_name($inputFileName, $toFormat);
+    $this->assertEquals($expected, $actual);
+  }
+
+}