mixin/scan-classes@1 - Allow extensions to opt-in to class scanning
authorTim Otten <totten@civicrm.org>
Tue, 21 Jun 2022 09:39:53 +0000 (02:39 -0700)
committerTim Otten <totten@civicrm.org>
Tue, 28 Jun 2022 00:18:21 +0000 (17:18 -0700)
mixin/scan-classes@1/example/CRM/Shimmy/ShimmyMessage.php [new file with mode: 0644]
mixin/scan-classes@1/example/tests/mixin/ScanClassesTest.php [new file with mode: 0644]
mixin/scan-classes@1/mixin.php [new file with mode: 0644]

diff --git a/mixin/scan-classes@1/example/CRM/Shimmy/ShimmyMessage.php b/mixin/scan-classes@1/example/CRM/Shimmy/ShimmyMessage.php
new file mode 100644 (file)
index 0000000..8c6811a
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+
+class CRM_Shimmy_ShimmyMessage extends Civi\WorkflowMessage\GenericWorkflowMessage {
+
+  public const WORKFLOW = 'shimmy_message_example';
+
+  /**
+   * @var string
+   * @scope tplParams
+   */
+  protected $foobar;
+
+}
diff --git a/mixin/scan-classes@1/example/tests/mixin/ScanClassesTest.php b/mixin/scan-classes@1/example/tests/mixin/ScanClassesTest.php
new file mode 100644 (file)
index 0000000..e993c84
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace Civi\Shimmy\Mixins;
+
+/**
+ * Assert that the 'scan-classes' mixin is working properly.
+ *
+ * This class defines the assertions to run when installing or uninstalling the extension.
+ * It is called as part of E2E_Shimmy_LifecycleTest.
+ *
+ * @see E2E_Shimmy_LifecycleTest
+ */
+class ScanClassesTest extends \PHPUnit\Framework\Assert {
+
+  public function testPreConditions($cv) {
+    $this->assertFileExists(static::getPath('/CRM/Shimmy/ShimmyMessage.php'), 'The shimmy extension must have example PHP files.');
+  }
+
+  public function testInstalled($cv) {
+    // Assert that WorkflowMessageInterface's are registered.
+    $items = $cv->api4('WorkflowMessage', 'get', ['where' => [['name', '=', 'shimmy_message_example']]]);
+    $this->assertEquals('CRM_Shimmy_ShimmyMessage', $items[0]['class']);
+  }
+
+  public function testDisabled($cv) {
+    // Assert that WorkflowMessageInterface's are removed.
+    $items = $cv->api4('WorkflowMessage', 'get', ['where' => [['name', '=', 'shimmy_message_example']]]);
+    $this->assertEmpty($items);
+  }
+
+  public function testUninstalled($cv) {
+    // Assert that WorkflowMessageInterface's are removed.
+    $items = $cv->api4('WorkflowMessage', 'get', ['where' => [['name', '=', 'shimmy_message_example']]]);
+    $this->assertEmpty($items);
+  }
+
+  protected static function getPath($suffix = ''): string {
+    return dirname(__DIR__, 2) . $suffix;
+  }
+
+}
diff --git a/mixin/scan-classes@1/mixin.php b/mixin/scan-classes@1/mixin.php
new file mode 100644 (file)
index 0000000..f23daba
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * Scan for files which implement common Civi-PHP interfaces.
+ *
+ * Specifically, this listens to `hook_scanClasses` and reports any classes with Civi-related
+ * interfaces (eg `CRM_Foo_BarInterface` or `Civi\Foo\BarInterface`). For example:
+ *
+ *   - \Civi\Core\HookInterface
+ *   - \Civi\Test\ExampleDataInterface
+ *   - \Civi\WorkflowMessage\WorkflowMessageInterface
+ *
+ * If you are adding this to an existing extension, take care that you meet these assumptions:
+ *
+ *   - Classes live in 'CRM_' ('./CRM/**.php') or 'Civi\' ('./Civi/**.php').
+ *   - Class files only begin with uppercase letters.
+ *   - Class files only contain alphanumerics.
+ *   - Class files never have multiple dots in the name. ("CRM/Foo.php" is a class; "CRM/Foo.bar.php" is not).
+ *   - The ONLY files which match these patterns are STRICTLY class files.
+ *   - The ONLY classes which match these patterns are SAFE/INTENDED for use with `hook_scanClasses`.
+ *
+ * To minimize unintended activations, this only loads Civi interfaces. It skips other interfaces.
+ *
+ * @mixinName scan-classes
+ * @mixinVersion 1.0.0
+ * @since 5.52
+ *
+ * @param CRM_Extension_MixInfo $mixInfo
+ *   On newer deployments, this will be an instance of MixInfo. On older deployments, Civix may polyfill with a work-a-like.
+ * @param \CRM_Extension_BootCache $bootCache
+ *   On newer deployments, this will be an instance of MixInfo. On older deployments, Civix may polyfill with a work-a-like.
+ */
+
+/**
+ * @param \CRM_Extension_MixInfo $mixInfo
+ * @param \CRM_Extension_BootCache $bootCache
+ */
+return function ($mixInfo, $bootCache) {
+  /**
+   * @param \Civi\Core\Event\GenericHookEvent $event
+   */
+  Civi::dispatcher()->addListener('hook_civicrm_scanClasses', function ($event) use ($mixInfo) {
+    if (!$mixInfo->isActive()) {
+      return;
+    }
+
+    $cache = \Civi\Core\ClassScanner::cache('structure');
+    $cacheKey = $mixInfo->longName;
+    $all = $cache->get($cacheKey);
+    if ($all === NULL) {
+      $baseDir = CRM_Utils_File::addTrailingSlash($mixInfo->getPath());
+      $all = [];
+
+      \Civi\Core\ClassScanner::scanFolders($all, $baseDir, 'CRM', '_');
+      \Civi\Core\ClassScanner::scanFolders($all, $baseDir, 'Civi', '\\');
+      if (defined('CIVICRM_TEST')) {
+        \Civi\Core\ClassScanner::scanFolders($all, "$baseDir/tests/phpunit", 'CRM', '_');
+        \Civi\Core\ClassScanner::scanFolders($all, "$baseDir/tests/phpunit", 'Civi', '\\');
+      }
+      $cache->set($cacheKey, $all, \Civi\Core\ClassScanner::TTL);
+    }
+
+    $event->classes = array_merge($event->classes, $all);
+  });
+
+};