From 31236900a84338576791324d9c0650ce86871a62 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 16 Apr 2021 23:23:20 -0700 Subject: [PATCH] Extensions - Define the upgrader base class This class is based on refactoring the civix template. Major sections have been split out into traits. This should make it more readable (and potentially make it easier to remix). This base-class should generally provide an equivalent DX for subclass authors: * Variables have the same names. * Most method signatures are identical (e.g. `executeFoo()`) * Some methods have been redeclared in equivalent form (`addTask()` - using splat instead `func_get_args()`). * Two internal functions have slightly diff signatures (`enqueuePendingRevisions()`, `_queueAdapter()``). --- CRM/Extension/Upgrader/Base.php | 134 ++++++++++++++++++++++ CRM/Extension/Upgrader/IdentityTrait.php | 55 +++++++++ CRM/Extension/Upgrader/QueueTrait.php | 108 +++++++++++++++++ CRM/Extension/Upgrader/RevisionsTrait.php | 131 +++++++++++++++++++++ CRM/Extension/Upgrader/TasksTrait.php | 100 ++++++++++++++++ 5 files changed, 528 insertions(+) create mode 100644 CRM/Extension/Upgrader/Base.php create mode 100644 CRM/Extension/Upgrader/IdentityTrait.php create mode 100644 CRM/Extension/Upgrader/QueueTrait.php create mode 100644 CRM/Extension/Upgrader/RevisionsTrait.php create mode 100644 CRM/Extension/Upgrader/TasksTrait.php diff --git a/CRM/Extension/Upgrader/Base.php b/CRM/Extension/Upgrader/Base.php new file mode 100644 index 0000000000..2e56e33e97 --- /dev/null +++ b/CRM/Extension/Upgrader/Base.php @@ -0,0 +1,134 @@ +getExtensionDir() . '/sql/*_install.sql'); + if (is_array($files)) { + foreach ($files as $file) { + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + } + } + $files = glob($this->getExtensionDir() . '/sql/*_install.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } + $files = glob($this->getExtensionDir() . '/xml/*_install.xml'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeCustomDataFileByAbsPath($file); + } + } + if (is_callable([$this, 'install'])) { + $this->install(); + } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall + */ + public function onPostInstall() { + $revisions = $this->getRevisions(); + if (!empty($revisions)) { + $this->setCurrentRevision(max($revisions)); + } + if (is_callable([$this, 'postInstall'])) { + $this->postInstall(); + } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_unnstall + */ + public function onUninstall() { + $files = glob($this->getExtensionDir() . '/sql/*_uninstall.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } + if (is_callable([$this, 'uninstall'])) { + $this->uninstall(); + } + $files = glob($this->getExtensionDir() . '/sql/*_uninstall.sql'); + if (is_array($files)) { + foreach ($files as $file) { + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + } + } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable + */ + public function onEnable() { + // stub for possible future use + if (is_callable([$this, 'enable'])) { + $this->enable(); + } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable + */ + public function onDisable() { + // stub for possible future use + if (is_callable([$this, 'disable'])) { + $this->disable(); + } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade + */ + public function onUpgrade($op, CRM_Queue_Queue $queue = NULL) { + switch ($op) { + case 'check': + return [$this->hasPendingRevisions()]; + + case 'enqueue': + $this->setQueue($queue); + return $this->enqueuePendingRevisions(); + + default: + } + } + +} diff --git a/CRM/Extension/Upgrader/IdentityTrait.php b/CRM/Extension/Upgrader/IdentityTrait.php new file mode 100644 index 0000000000..8f27d44a92 --- /dev/null +++ b/CRM/Extension/Upgrader/IdentityTrait.php @@ -0,0 +1,55 @@ +extensionName = $params['key']; + $system = CRM_Extension_System::singleton(); + $mapper = $system->getMapper(); + $this->extensionDir = $mapper->keyToBasePath($this->extensionName); + } + + /** + * @return string + * Ex: 'org.example.foobar' + */ + public function getExtensionKey() { + return $this->extensionName; + } + + /** + * @return string + * Ex: '/var/www/sites/default/ext/org.example.foobar' + */ + public function getExtensionDir() { + return $this->extensionDir; + } + +} diff --git a/CRM/Extension/Upgrader/QueueTrait.php b/CRM/Extension/Upgrader/QueueTrait.php new file mode 100644 index 0000000000..c154e49fb1 --- /dev/null +++ b/CRM/Extension/Upgrader/QueueTrait.php @@ -0,0 +1,108 @@ +getMapper()->getUpgrader($extensionKey); + if ($upgrader->ctx !== NULL) { + throw new \RuntimeException(sprintf("Cannot execute task for %s (%s::%s) - task already active.", $extensionKey, get_class($upgrader), $method)); + } + + $upgrader->ctx = $ctx; + $upgrader->queue = $ctx->queue; + try { + return call_user_func_array([$upgrader, $method], $args); + } finally { + $upgrader->ctx = NULL; + } + } + + public function addTask(string $title, string $funcName, ...$options) { + return $this->prependTask($title, $funcName, ...$options); + } + + /** + * Enqueue a task based on a method in this class. + * + * The task is weighted so that it is processed as part of the currently-pending revision. + * + * After passing the $funcName, you can also pass parameters that will go to + * the function. Note that all params must be serializable. + */ + public function prependTask(string $title, string $funcName, ...$options) { + $task = new CRM_Queue_Task( + [get_class($this), '_queueAdapter'], + array_merge([$this->getExtensionKey(), $funcName], $options), + $title + ); + return $this->queue->createItem($task, ['weight' => -1]); + } + + /** + * Enqueue a task based on a method in this class. + * + * The task has a default weight. + * + * @return mixed + */ + protected function appendTask(string $title, string $funcName, ...$options) { + $task = new CRM_Queue_Task( + [get_class($this), '_queueAdapter'], + array_merge([$this->getExtensionKey(), $funcName], $options), + $title + ); + return $this->queue->createItem($task); + } + + // ******** Basic getters/setters ******** + + /** + * @return \CRM_Queue_Queue + */ + public function getQueue(): \CRM_Queue_Queue { + return $this->queue; + } + + /** + * @param \CRM_Queue_Queue $queue + */ + public function setQueue(\CRM_Queue_Queue $queue): void { + $this->queue = $queue; + } + +} diff --git a/CRM/Extension/Upgrader/RevisionsTrait.php b/CRM/Extension/Upgrader/RevisionsTrait.php new file mode 100644 index 0000000000..0c586fcf3e --- /dev/null +++ b/CRM/Extension/Upgrader/RevisionsTrait.php @@ -0,0 +1,131 @@ +getRevisions(); + $currentRevision = $this->getCurrentRevision(); + + if (empty($revisions)) { + return FALSE; + } + if (empty($currentRevision)) { + return TRUE; + } + + return ($currentRevision < max($revisions)); + } + + /** + * Add any pending revisions to the queue. + */ + public function enqueuePendingRevisions() { + $currentRevision = $this->getCurrentRevision(); + foreach ($this->getRevisions() as $revision) { + if ($revision > $currentRevision) { + $title = ts('Upgrade %1 to revision %2', [ + 1 => $this->getExtensionKey(), + 2 => $revision, + ]); + + // note: don't use addTask() because it sets weight=-1 + + $this->appendTask($title, 'upgrade_' . $revision); + $this->appendTask($title, 'setCurrentRevision', $revision); + } + } + } + + /** + * Get a list of revisions. + * + * @return array + * revisionNumbers sorted numerically + */ + public function getRevisions() { + if (!is_array($this->revisions)) { + $this->revisions = []; + + $clazz = new \ReflectionClass(get_class($this)); + $methods = $clazz->getMethods(); + foreach ($methods as $method) { + if (preg_match('/^upgrade_(.*)/', $method->name, $matches)) { + $this->revisions[] = $matches[1]; + } + } + sort($this->revisions, SORT_NUMERIC); + } + + return $this->revisions; + } + + public function getCurrentRevision() { + $revision = CRM_Core_BAO_Extension::getSchemaVersion($this->getExtensionKey()); + if (!$revision) { + $revision = $this->getCurrentRevisionDeprecated(); + } + return $revision; + } + + private function getCurrentRevisionDeprecated() { + $key = $this->getExtensionKey() . ':version'; + if ($revision = \Civi::settings()->get($key)) { + $this->revisionStorageIsDeprecated = TRUE; + } + return $revision; + } + + public function setCurrentRevision($revision) { + CRM_Core_BAO_Extension::setSchemaVersion($this->getExtensionKey(), $revision); + // clean up legacy schema version store (CRM-19252) + $this->deleteDeprecatedRevision(); + return TRUE; + } + + private function deleteDeprecatedRevision() { + if ($this->revisionStorageIsDeprecated) { + $setting = new \CRM_Core_BAO_Setting(); + $setting->name = $this->getExtensionKey() . ':version'; + $setting->delete(); + CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->getExtensionKey()} from civicrm_setting (deprecated) to civicrm_extension.\n"); + } + } + +} diff --git a/CRM/Extension/Upgrader/TasksTrait.php b/CRM/Extension/Upgrader/TasksTrait.php new file mode 100644 index 0000000000..3cbf6cf43e --- /dev/null +++ b/CRM/Extension/Upgrader/TasksTrait.php @@ -0,0 +1,100 @@ +getExtensionDir() . '/' . $relativePath; + return $this->executeCustomDataFileByAbsPath($xml_file); + } + + /** + * Run a CustomData file + * + * @param string $xml_file + * the CustomData XML file path (absolute path) + * + * @return bool + */ + protected function executeCustomDataFileByAbsPath($xml_file) { + $import = new CRM_Utils_Migrate_Import(); + $import->run($xml_file); + return TRUE; + } + + /** + * Run a SQL file. + * + * @param string $tplFile + * The SQL file path (relative to this extension's dir, or absolute) + * + * @return bool + */ + public function executeSqlFile($tplFile) { + $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->getExtensionDir() . DIRECTORY_SEPARATOR . $tplFile; + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $tplFile); + return TRUE; + } + + /** + * Run the sql commands in the specified file. + * + * @param string $tplFile + * The SQL file path (relative to this extension's dir, or absolute). + * Ex: "sql/mydata.mysql.tpl". + * + * @return bool + * @throws \CRM_Core_Exception + */ + public function executeSqlTemplate($tplFile) { + // Assign multilingual variable to Smarty. + $upgrade = new CRM_Upgrade_Form(); + + $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->getExtensionDir() . DIRECTORY_SEPARATOR . $tplFile; + $smarty = CRM_Core_Smarty::singleton(); + $smarty->assign('domainID', CRM_Core_Config::domainID()); + CRM_Utils_File::sourceSQLFile( + CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE + ); + return TRUE; + } + + /** + * Run one SQL query. + * + * This is just a wrapper for CRM_Core_DAO::executeSql, but it + * provides syntactic sugar for queueing several tasks that + * run different queries + * + * @return bool + */ + public function executeSql($query, $params = []) { + // FIXME verify that we raise an exception on error + CRM_Core_DAO::executeQuery($query, $params); + return TRUE; + } + +} -- 2.25.1