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