From 7b15eb82b9abf2c2b527ee2c19c7701c2006a8be Mon Sep 17 00:00:00 2001 From: colemanw Date: Sat, 21 Oct 2023 13:45:28 -0400 Subject: [PATCH] ManagedEntity - Replicate multi-domain entities when multisite is enabled --- Civi/Foo/MultisiteManaged.php | 89 --------------- Civi/Managed/MultisiteManaged.php | 107 ++++++++++++++++++ .../api/v4/Entity/ManagedEntityTest.php | 54 +++++++++ 3 files changed, 161 insertions(+), 89 deletions(-) delete mode 100644 Civi/Foo/MultisiteManaged.php create mode 100644 Civi/Managed/MultisiteManaged.php diff --git a/Civi/Foo/MultisiteManaged.php b/Civi/Foo/MultisiteManaged.php deleted file mode 100644 index 614b6d3a46..0000000000 --- a/Civi/Foo/MultisiteManaged.php +++ /dev/null @@ -1,89 +0,0 @@ -mgd@2.0`) - * - * This variant has an extra guard so that clever extensions (which multiply entities themselves) don't get entities-squared. - * - * @service - * @internal - */ -class MultisiteManaged extends AutoService implements EventSubscriberInterface { - - protected $entities = ['Navigation', 'Dashboard']; - - public static function getSubscribedEvents() { - return [ - '&hook_civicrm_managed' => ['generateDomainEntities', -1000], - ]; - } - - public function generateDomainEntities(array &$entities): void { - $selfMultipliedModules = $this->findSelfMultipliedModules($entities); - foreach ($selfMultipliedModules as $module) { - \CRM_Core_Error::deprecatedWarning(sprintf('Module (%s) has self-multiplied some records across domains. This is deprecated.', $module)); - } - - $templates = []; - - // Figure out which entities to copy - foreach (array_keys($entities) as $entityKey) { - if ($this->isCopiable($entities[$entityKey]) && !in_array($entities[$entityKey]['module'], $selfMultipliedModules)) { - $templates[] = $entities[$entityKey]; - unset($entities[$entityKey]); - } - } - - // Make the real entities, one for each domain - foreach ($templates as $template) { - foreach ($this->makeCopies($template) as $entity) { - $entities[] = $entity; - } - } - } - - protected function makeCopies(array $entity): array { - $copies = []; - $domains = \Civi\Api4\Domain::get(FALSE)->addSelect('id')->execute(); - foreach ($domains as $domain) { - $copy = $entity; - $copy['name'] = $entity['name'] . '_' . $domain['id']; - $copy['params']['values']['domain_id'] = $domain['id']; - $copies[] = $copy; - } - return $copies; - } - - protected function isCopiable(array $entity) { - return in_array($entity['entity'], $this->entities) && ($entity['params']['version'] ?? 3) == 4; - } - - protected function findSelfMultipliedModules(array $entities): array { - $moduleDomains = []; - foreach ($entities as $entity) { - if ($this->isCopiable($entity) && !empty($entity['params']['values']['domain_id'])) { - $moduleDomains[$entity['module']][] = $entity['params']['values']['domain_id']; - } - } - $results = []; - foreach ($moduleDomains as $module => $domains) { - if (count(array_unique($domains)) > 1) { - $results[] = $module; - } - } - return $results; - } - -} diff --git a/Civi/Managed/MultisiteManaged.php b/Civi/Managed/MultisiteManaged.php new file mode 100644 index 0000000000..12e8dd8f6d --- /dev/null +++ b/Civi/Managed/MultisiteManaged.php @@ -0,0 +1,107 @@ + ['generateDomainEntities', -1000], + ]; + } + + /** + * @implements \CRM_Utils_Hook::managed() + * @param array $managedRecords + */ + public function generateDomainEntities(array &$managedRecords): void { + $multisiteEnabled = Setting::get(FALSE) + ->addSelect('is_enabled') + ->execute()->first(); + if (empty($multisiteEnabled['value'])) { + return; + } + + // array_splice needs array keys to be orderly + $managedRecords = array_values($managedRecords); + // Replace every single-domain record with one record per domain + // Walk the array in reverse order so the keys being processed remain stable even as the length changes. + foreach (array_reverse(array_keys($managedRecords)) as $index) { + if ($this->isCopiable($managedRecords[$index])) { + array_splice($managedRecords, $index, 1, $this->makeCopies($managedRecords[$index])); + } + } + } + + protected function makeCopies(array $managedRecord): array { + $copies = []; + foreach ($this->getDomains() as $index => $domainId) { + $copy = $managedRecord; + // For a smoother transition between enabling/disabling multisite, don't rename the first copy + if ($index) { + $copy['name'] .= '_' . $domainId; + } + $copy['params']['values']['domain_id'] = $domainId; + $copies[] = $copy; + } + return $copies; + } + + /** + * Check if a managed record is an APIv4 Entity that should exist on all domains. + * + * Follows the same logic for determining an entity belongs on multiple domains as `FieldDomainIdSpecProvider` + * @see \Civi\Api4\Service\Spec\Provider\FieldDomainIdSpecProvider + * + * @param array $managedRecord + * @return bool + */ + protected function isCopiable(array $managedRecord): bool { + if ($managedRecord['params']['version'] != 4) { + return FALSE; + } + // Extra guard so that clever extensions (which multiply entities themselves) don't get entities-squared. + if (is_numeric($managedRecord['params']['values']['domain_id'] ?? NULL) || !empty($managedRecord['params']['values']['domain_id.name'])) { + \CRM_Core_Error::deprecatedWarning(sprintf('Module "%s" has self-multiplied managed entity "%s" across domains. This is deprecated.', $managedRecord['module'], $managedRecord['name'])); + return FALSE; + } + if (!isset($this->entities[$managedRecord['entity']])) { + try { + $this->entities[$managedRecord['entity']] = (bool) civicrm_api4($managedRecord['entity'], 'getFields', [ + 'checkPermissions' => FALSE, + 'action' => 'create', + 'where' => [ + ['name', '=', 'domain_id'], + ['default_value', '=', 'current_domain'], + ], + ])->count(); + } + catch (\CRM_Core_Exception $e) { + $this->entities[$managedRecord['entity']] = FALSE; + } + } + return $this->entities[$managedRecord['entity']]; + } + + private function getDomains(): array { + if (!isset($this->domains)) { + $this->domains = Domain::get(FALSE)->addSelect('id')->addOrderBy('id')->execute()->column('id'); + } + return $this->domains; + } + +} diff --git a/tests/phpunit/api/v4/Entity/ManagedEntityTest.php b/tests/phpunit/api/v4/Entity/ManagedEntityTest.php index 9bc8946b14..f9829c9a7e 100644 --- a/tests/phpunit/api/v4/Entity/ManagedEntityTest.php +++ b/tests/phpunit/api/v4/Entity/ManagedEntityTest.php @@ -56,6 +56,8 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti public function tearDown(): void { \Civi::settings()->revert('debug_enabled'); + // Disable multisite + \Civi::settings()->revert('is_enabled'); parent::tearDown(); } @@ -663,6 +665,58 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti $this->assertEquals(TRUE, $nav['is_active']); } + /** + * Test multisite managed entities + * @see \Civi\Managed\MultisiteManaged + */ + public function testMultiDomainNavigation(): void { + $this->_managedEntities[] = [ + 'module' => 'unit.test.fake.ext', + 'name' => 'Navigation_Test_Domains', + 'entity' => 'Navigation', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'label' => 'Test Domains', + 'name' => 'Test_Domains', + 'url' => 'civicrm/foo/bar', + 'icon' => 'crm-i test', + 'permission' => ['access CiviCRM'], + 'weight' => 50, + 'domain_id' => 'current_domain', + ], + ], + ]; + $managedRecords = []; + \CRM_Utils_Hook::managed($managedRecords, ['unit.test.fake.ext']); + $result = \CRM_Utils_Array::findAll($managedRecords, ['module' => 'unit.test.fake.ext', 'name' => 'Navigation_Test_Domains']); + $this->assertCount(1, $result); + + // Enable multisite with multiple domains + \Civi::settings()->set('is_enabled', TRUE); + Domain::create(FALSE) + ->addValue('name', 'Another domain') + ->addValue('version', CRM_Utils_System::version()) + ->execute()->single(); + $allDomains = Domain::get(FALSE)->addSelect('id')->addOrderBy('id')->execute(); + $this->assertGreaterThan(1, $allDomains->count()); + + $managedRecords = []; + \CRM_Utils_Hook::managed($managedRecords, ['unit.test.fake.ext']); + + // Base entity should not have been renamed + $result = \CRM_Utils_Array::findAll($managedRecords, ['module' => 'unit.test.fake.ext', 'name' => 'Navigation_Test_Domains']); + $this->assertCount(1, $result); + + // New item should have been inserted for extra domains + foreach (array_slice($allDomains->column('id'), 1) as $domain) { + $result = \CRM_Utils_Array::findAll($managedRecords, ['module' => 'unit.test.fake.ext', 'name' => 'Navigation_Test_Domains_' . $domain]); + $this->assertCount(1, $result); + } + } + /** * @throws \CRM_Core_Exception */ -- 2.25.1