Issue #3483: parent_id not always available as index
[civicrm-core.git] / CRM / Extension / Upgrades.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 use MJS\TopSort\CircularDependencyException;
13 use MJS\TopSort\ElementNotFoundException;
14
15 /**
16 * This class stores logic for managing schema upgrades in CiviCRM extensions.
17 *
18 * @package CRM
19 * @copyright CiviCRM LLC https://civicrm.org/licensing
20 */
21 class CRM_Extension_Upgrades {
22
23 const QUEUE_NAME = 'ext-upgrade';
24
25 /**
26 * Determine whether any extensions have pending upgrades.
27 *
28 * @return bool
29 */
30 public static function hasPending() {
31 $hasTrue = function($checks) {
32 if (is_array($checks)) {
33 foreach ($checks as $check) {
34 if ($check) {
35 return TRUE;
36 }
37 }
38 }
39 return FALSE;
40 };
41
42 foreach (self::getActiveUpgraders() as $upgrader) {
43 /** @var \CRM_Extension_Upgrader_Interface $upgrader */
44 if ($hasTrue($upgrader->notify('upgrade', ['check']))) {
45 return TRUE;
46 }
47 }
48
49 $checks = CRM_Utils_Hook::upgrade('check');
50 return $hasTrue($checks);
51 }
52
53 /**
54 * Fill a queue with upgrade tasks.
55 *
56 * @return CRM_Queue_Queue
57 */
58 public static function createQueue() {
59 $queue = CRM_Queue_Service::singleton()->create([
60 'type' => 'Sql',
61 'name' => self::QUEUE_NAME,
62 'reset' => TRUE,
63 ]);
64
65 foreach (self::getActiveUpgraders() as $upgrader) {
66 /** @var \CRM_Extension_Upgrader_Interface $upgrader */
67 $upgrader->notify('upgrade', ['enqueue', $queue]);
68 }
69
70 CRM_Utils_Hook::upgrade('enqueue', $queue);
71
72 // dev/core#1618 When Extension Upgrades are run reconcile log tables
73 $task = new CRM_Queue_Task(
74 [__CLASS__, 'upgradeLogTables'],
75 [],
76 ts('Update log tables')
77 );
78 // Set weight low so that it will be run last.
79 $queue->createItem($task, -2);
80
81 return $queue;
82 }
83
84 /**
85 * Update log tables following execution of extension upgrades
86 */
87 public static function upgradeLogTables() {
88 $logging = new CRM_Logging_Schema();
89 $logging->fixSchemaDifferences();
90 return TRUE;
91 }
92
93 /**
94 * @return array
95 * Array(string $extKey => CRM_Extension_Upgrader_Interface $upgrader)
96 */
97 protected static function getActiveUpgraders() {
98 $mapper = \CRM_Extension_System::singleton()->getMapper();
99 $keys = self::getActiveKeys();
100
101 $upgraders = [];
102 foreach ($keys as $key) {
103 $upgrader = $mapper->getUpgrader($key);
104 if ($upgrader !== NULL) {
105 $upgraders[$key] = $upgrader;
106 }
107 }
108 return $upgraders;
109 }
110
111 /**
112 * @return string[]
113 */
114 protected static function getActiveKeys() {
115 $mapper = \CRM_Extension_System::singleton()->getMapper();
116 try {
117 return self::sortKeys(array_column($mapper->getActiveModuleFiles(), 'fullName'));
118 }
119 catch (CircularDependencyException $e) {
120 CRM_Core_Error::debug_log_message("Failed to identify extensions. Circular dependency. " . $e->getMessage());
121 return [];
122 }
123 catch (ElementNotFoundException $e) {
124 CRM_Core_Error::debug_log_message("Failed to identify extensions. Unrecognized dependency. " . $e->getMessage());
125 return [];
126 }
127 }
128
129 /**
130 * Sorts active extensions according to their dependencies
131 *
132 * @param string[] $keys
133 * Names of all active modules
134 *
135 * @return string[]
136 * @throws \CRM_Extension_Exception
137 * @throws \MJS\TopSort\CircularDependencyException
138 * @throws \MJS\TopSort\ElementNotFoundException
139 */
140 protected static function sortKeys($keys) {
141 $infos = CRM_Extension_System::singleton()->getMapper()->getAllInfos();
142
143 // Ensure a stable starting order.
144 $todoKeys = array_unique($keys);
145 sort($todoKeys);
146
147 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
148
149 foreach ($todoKeys as $key) {
150 /** @var CRM_Extension_Info $info */
151 $info = $infos[$key] ?? NULL;
152
153 // Add dependencies
154 if ($info) {
155 // Filter out missing dependencies; missing modules cannot be upgraded
156 $requires = array_intersect($info->requires ?? [], $keys);
157 $sorter->add($key, $requires);
158 }
159 // This shouldn't ever happen if this function is being passed a list of active extensions.
160 else {
161 throw new CRM_Extension_Exception('Invalid extension key: "' . $key . '"');
162 }
163 }
164 return $sorter->sort();
165 }
166
167 }