Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | ||
3 | /** | |
4 | * The ManagedEntities system allows modules to add records to the database | |
5 | * declaratively. Those records will be automatically inserted, updated, | |
6 | * deactivated, and deleted in tandem with their modules. | |
7 | */ | |
8 | class CRM_Core_ManagedEntities { | |
1f103dc4 | 9 | |
4b62bc4f EM |
10 | /** |
11 | * Get clean up options. | |
12 | * | |
13 | * @return array | |
14 | */ | |
1f103dc4 | 15 | public static function getCleanupOptions() { |
be2fb01f | 16 | return [ |
1f103dc4 TO |
17 | 'always' => ts('Always'), |
18 | 'never' => ts('Never'), | |
19 | 'unused' => ts('If Unused'), | |
be2fb01f | 20 | ]; |
1f103dc4 TO |
21 | } |
22 | ||
6a488035 | 23 | /** |
88718db2 TO |
24 | * @var array |
25 | * Array($status => array($name => CRM_Core_Module)). | |
6a488035 | 26 | */ |
e9b95545 | 27 | protected $moduleIndex; |
6a488035 TO |
28 | |
29 | /** | |
88718db2 TO |
30 | * @var array |
31 | * List of all entity declarations. | |
32 | * @see CRM_Utils_Hook::managed() | |
6a488035 | 33 | */ |
e9b95545 | 34 | protected $declarations; |
6a488035 TO |
35 | |
36 | /** | |
d09edf64 | 37 | * Get an instance. |
ba3228d1 EM |
38 | * @param bool $fresh |
39 | * @return \CRM_Core_ManagedEntities | |
6a488035 TO |
40 | */ |
41 | public static function singleton($fresh = FALSE) { | |
42 | static $singleton; | |
43 | if ($fresh || !$singleton) { | |
e9b95545 | 44 | $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module::getAll(), NULL); |
6a488035 TO |
45 | } |
46 | return $singleton; | |
47 | } | |
48 | ||
9c19292b TO |
49 | /** |
50 | * Perform an asynchronous reconciliation when the transaction ends. | |
51 | */ | |
ba3228d1 | 52 | public static function scheduleReconciliation() { |
9c19292b TO |
53 | CRM_Core_Transaction::addCallback( |
54 | CRM_Core_Transaction::PHASE_POST_COMMIT, | |
55 | function () { | |
e70a7fc0 | 56 | CRM_Core_ManagedEntities::singleton(TRUE)->reconcile(); |
9c19292b | 57 | }, |
be2fb01f | 58 | [], |
9c19292b TO |
59 | 'ManagedEntities::reconcile' |
60 | ); | |
61 | } | |
62 | ||
6a488035 | 63 | /** |
6a0b768e TO |
64 | * @param array $modules |
65 | * CRM_Core_Module. | |
66 | * @param array $declarations | |
67 | * Per hook_civicrm_managed. | |
6a488035 TO |
68 | */ |
69 | public function __construct($modules, $declarations) { | |
70 | $this->moduleIndex = self::createModuleIndex($modules); | |
e9b95545 TO |
71 | |
72 | if ($declarations !== NULL) { | |
73 | $this->declarations = self::cleanDeclarations($declarations); | |
0db6c3e1 TO |
74 | } |
75 | else { | |
e9b95545 TO |
76 | $this->declarations = NULL; |
77 | } | |
6a488035 TO |
78 | } |
79 | ||
80 | /** | |
88718db2 | 81 | * Read a managed entity using APIv3. |
378e2654 | 82 | * |
88718db2 TO |
83 | * @param string $moduleName |
84 | * The name of the module which declared entity. | |
85 | * @param string $name | |
86 | * The symbolic name of the entity. | |
72b3a70c CW |
87 | * @return array|NULL |
88 | * API representation, or NULL if the entity does not exist | |
6a488035 TO |
89 | */ |
90 | public function get($moduleName, $name) { | |
91 | $dao = new CRM_Core_DAO_Managed(); | |
92 | $dao->module = $moduleName; | |
93 | $dao->name = $name; | |
94 | if ($dao->find(TRUE)) { | |
be2fb01f | 95 | $params = [ |
6a488035 | 96 | 'id' => $dao->entity_id, |
be2fb01f | 97 | ]; |
bbf66e9c | 98 | $result = NULL; |
637ea2cf E |
99 | try { |
100 | $result = civicrm_api3($dao->entity_type, 'getsingle', $params); | |
101 | } | |
102 | catch (Exception $e) { | |
e9b95545 | 103 | $this->onApiError($dao->entity_type, 'getsingle', $params, $result); |
6a488035 | 104 | } |
637ea2cf | 105 | return $result; |
0db6c3e1 TO |
106 | } |
107 | else { | |
6a488035 TO |
108 | return NULL; |
109 | } | |
110 | } | |
111 | ||
88718db2 TO |
112 | /** |
113 | * Identify any enabled/disabled modules. Add new entities, update | |
114 | * existing entities, and remove orphaned (stale) entities. | |
115 | * | |
116 | * @throws Exception | |
117 | */ | |
6a488035 | 118 | public function reconcile() { |
e9b95545 | 119 | if ($error = $this->validate($this->getDeclarations())) { |
6a488035 TO |
120 | throw new Exception($error); |
121 | } | |
122 | $this->reconcileEnabledModules(); | |
123 | $this->reconcileDisabledModules(); | |
124 | $this->reconcileUnknownModules(); | |
125 | } | |
126 | ||
88718db2 TO |
127 | /** |
128 | * For all enabled modules, add new entities, update | |
129 | * existing entities, and remove orphaned (stale) entities. | |
130 | * | |
131 | * @throws Exception | |
132 | */ | |
6a488035 TO |
133 | public function reconcileEnabledModules() { |
134 | // Note: any thing currently declared is necessarily from | |
135 | // an active module -- because we got it from a hook! | |
136 | ||
137 | // index by moduleName,name | |
e9b95545 | 138 | $decls = self::createDeclarationIndex($this->moduleIndex, $this->getDeclarations()); |
6a488035 TO |
139 | foreach ($decls as $moduleName => $todos) { |
140 | if (isset($this->moduleIndex[TRUE][$moduleName])) { | |
141 | $this->reconcileEnabledModule($this->moduleIndex[TRUE][$moduleName], $todos); | |
0db6c3e1 TO |
142 | } |
143 | elseif (isset($this->moduleIndex[FALSE][$moduleName])) { | |
6a488035 | 144 | // do nothing -- module should get swept up later |
0db6c3e1 TO |
145 | } |
146 | else { | |
6a488035 TO |
147 | throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]"); |
148 | } | |
149 | } | |
150 | } | |
151 | ||
152 | /** | |
88718db2 TO |
153 | * For one enabled module, add new entities, update existing entities, |
154 | * and remove orphaned (stale) entities. | |
6a488035 | 155 | * |
88718db2 | 156 | * @param \CRM_Core_Module $module |
5a4f6742 | 157 | * @param array $todos |
88718db2 TO |
158 | * List of entities currently declared by this module. |
159 | * array(string $name => array $entityDef). | |
6a488035 TO |
160 | */ |
161 | public function reconcileEnabledModule(CRM_Core_Module $module, $todos) { | |
162 | $dao = new CRM_Core_DAO_Managed(); | |
163 | $dao->module = $module->name; | |
164 | $dao->find(); | |
165 | while ($dao->fetch()) { | |
3bd831aa | 166 | if (isset($todos[$dao->name]) && $todos[$dao->name]) { |
6a488035 | 167 | // update existing entity; remove from $todos |
7cddb4ae | 168 | $this->updateExistingEntity($dao, $todos[$dao->name]); |
6a488035 | 169 | unset($todos[$dao->name]); |
0db6c3e1 TO |
170 | } |
171 | else { | |
6a488035 | 172 | // remove stale entity; not in $todos |
bbf66e9c | 173 | $this->removeStaleEntity($dao); |
6a488035 TO |
174 | } |
175 | } | |
176 | ||
177 | // create new entities from leftover $todos | |
178 | foreach ($todos as $name => $todo) { | |
7cddb4ae | 179 | $this->insertNewEntity($todo); |
6a488035 TO |
180 | } |
181 | } | |
182 | ||
88718db2 TO |
183 | /** |
184 | * For all disabled modules, disable any managed entities. | |
185 | */ | |
6a488035 TO |
186 | public function reconcileDisabledModules() { |
187 | if (empty($this->moduleIndex[FALSE])) { | |
188 | return; | |
189 | } | |
190 | ||
191 | $in = CRM_Core_DAO::escapeStrings(array_keys($this->moduleIndex[FALSE])); | |
192 | $dao = new CRM_Core_DAO_Managed(); | |
193 | $dao->whereAdd("module in ($in)"); | |
04b08baa | 194 | $dao->orderBy('id DESC'); |
6a488035 TO |
195 | $dao->find(); |
196 | while ($dao->fetch()) { | |
7cddb4ae TO |
197 | $this->disableEntity($dao); |
198 | ||
6a488035 TO |
199 | } |
200 | } | |
201 | ||
88718db2 TO |
202 | /** |
203 | * Remove any orphaned (stale) entities that are linked to | |
204 | * unknown modules. | |
205 | */ | |
6a488035 | 206 | public function reconcileUnknownModules() { |
be2fb01f | 207 | $knownModules = []; |
6a488035 TO |
208 | if (array_key_exists(0, $this->moduleIndex) && is_array($this->moduleIndex[0])) { |
209 | $knownModules = array_merge($knownModules, array_keys($this->moduleIndex[0])); | |
210 | } | |
211 | if (array_key_exists(1, $this->moduleIndex) && is_array($this->moduleIndex[1])) { | |
212 | $knownModules = array_merge($knownModules, array_keys($this->moduleIndex[1])); | |
6a488035 TO |
213 | } |
214 | ||
215 | $dao = new CRM_Core_DAO_Managed(); | |
216 | if (!empty($knownModules)) { | |
217 | $in = CRM_Core_DAO::escapeStrings($knownModules); | |
218 | $dao->whereAdd("module NOT IN ($in)"); | |
04b08baa | 219 | $dao->orderBy('id DESC'); |
6a488035 TO |
220 | } |
221 | $dao->find(); | |
222 | while ($dao->fetch()) { | |
bbf66e9c TO |
223 | $this->removeStaleEntity($dao); |
224 | } | |
225 | } | |
6a488035 | 226 | |
7cddb4ae | 227 | /** |
88718db2 | 228 | * Create a new entity. |
7cddb4ae | 229 | * |
6a0b768e TO |
230 | * @param array $todo |
231 | * Entity specification (per hook_civicrm_managedEntities). | |
7cddb4ae TO |
232 | */ |
233 | public function insertNewEntity($todo) { | |
234 | $result = civicrm_api($todo['entity'], 'create', $todo['params']); | |
235 | if ($result['is_error']) { | |
e9b95545 | 236 | $this->onApiError($todo['entity'], 'create', $todo['params'], $result); |
7cddb4ae TO |
237 | } |
238 | ||
239 | $dao = new CRM_Core_DAO_Managed(); | |
240 | $dao->module = $todo['module']; | |
241 | $dao->name = $todo['name']; | |
242 | $dao->entity_type = $todo['entity']; | |
243 | $dao->entity_id = $result['id']; | |
1f103dc4 | 244 | $dao->cleanup = CRM_Utils_Array::value('cleanup', $todo); |
7cddb4ae TO |
245 | $dao->save(); |
246 | } | |
247 | ||
248 | /** | |
249 | * Update an entity which (a) is believed to exist and which (b) ought to be active. | |
250 | * | |
251 | * @param CRM_Core_DAO_Managed $dao | |
6a0b768e TO |
252 | * @param array $todo |
253 | * Entity specification (per hook_civicrm_managedEntities). | |
7cddb4ae TO |
254 | */ |
255 | public function updateExistingEntity($dao, $todo) { | |
0dd54586 TO |
256 | $policy = CRM_Utils_Array::value('update', $todo, 'always'); |
257 | $doUpdate = ($policy == 'always'); | |
258 | ||
259 | if ($doUpdate) { | |
be2fb01f | 260 | $defaults = [ |
0dd54586 | 261 | 'id' => $dao->entity_id, |
518fa0ee SL |
262 | // FIXME: test whether is_active is valid |
263 | 'is_active' => 1, | |
be2fb01f | 264 | ]; |
0dd54586 TO |
265 | $params = array_merge($defaults, $todo['params']); |
266 | $result = civicrm_api($dao->entity_type, 'create', $params); | |
267 | if ($result['is_error']) { | |
353ffa53 | 268 | $this->onApiError($dao->entity_type, 'create', $params, $result); |
0dd54586 | 269 | } |
7cddb4ae | 270 | } |
1f103dc4 TO |
271 | |
272 | if (isset($todo['cleanup'])) { | |
273 | $dao->cleanup = $todo['cleanup']; | |
274 | $dao->update(); | |
275 | } | |
7cddb4ae TO |
276 | } |
277 | ||
278 | /** | |
279 | * Update an entity which (a) is believed to exist and which (b) ought to be | |
280 | * inactive. | |
281 | * | |
282 | * @param CRM_Core_DAO_Managed $dao | |
283 | */ | |
284 | public function disableEntity($dao) { | |
285 | // FIXME: if ($dao->entity_type supports is_active) { | |
286 | if (TRUE) { | |
287 | // FIXME cascading for payproc types? | |
be2fb01f | 288 | $params = [ |
7cddb4ae TO |
289 | 'version' => 3, |
290 | 'id' => $dao->entity_id, | |
291 | 'is_active' => 0, | |
be2fb01f | 292 | ]; |
7cddb4ae TO |
293 | $result = civicrm_api($dao->entity_type, 'create', $params); |
294 | if ($result['is_error']) { | |
353ffa53 | 295 | $this->onApiError($dao->entity_type, 'create', $params, $result); |
7cddb4ae TO |
296 | } |
297 | } | |
298 | } | |
299 | ||
bbf66e9c | 300 | /** |
88718db2 | 301 | * Remove a stale entity (if policy allows). |
bbf66e9c TO |
302 | * |
303 | * @param CRM_Core_DAO_Managed $dao | |
88718db2 | 304 | * @throws Exception |
bbf66e9c TO |
305 | */ |
306 | public function removeStaleEntity($dao) { | |
1f103dc4 | 307 | $policy = empty($dao->cleanup) ? 'always' : $dao->cleanup; |
378e2654 TO |
308 | switch ($policy) { |
309 | case 'always': | |
310 | $doDelete = TRUE; | |
311 | break; | |
ea100cb5 | 312 | |
378e2654 TO |
313 | case 'never': |
314 | $doDelete = FALSE; | |
315 | break; | |
ea100cb5 | 316 | |
378e2654 | 317 | case 'unused': |
be2fb01f | 318 | $getRefCount = civicrm_api3($dao->entity_type, 'getrefcount', [ |
378e2654 | 319 | 'debug' => 1, |
21dfd5f5 | 320 | 'id' => $dao->entity_id, |
be2fb01f | 321 | ]); |
378e2654 TO |
322 | |
323 | $total = 0; | |
324 | foreach ($getRefCount['values'] as $refCount) { | |
325 | $total += $refCount['count']; | |
326 | } | |
327 | ||
328 | $doDelete = ($total == 0); | |
329 | break; | |
ea100cb5 | 330 | |
378e2654 TO |
331 | default: |
332 | throw new \Exception('Unrecognized cleanup policy: ' . $policy); | |
333 | } | |
bbf66e9c | 334 | |
1f103dc4 | 335 | if ($doDelete) { |
be2fb01f | 336 | $params = [ |
1f103dc4 TO |
337 | 'version' => 3, |
338 | 'id' => $dao->entity_id, | |
be2fb01f | 339 | ]; |
a60c0bc8 SL |
340 | $check = civicrm_api3($dao->entity_type, 'get', $params); |
341 | if ((bool) $check['count']) { | |
342 | $result = civicrm_api($dao->entity_type, 'delete', $params); | |
343 | if ($result['is_error']) { | |
344 | $this->onApiError($dao->entity_type, 'delete', $params, $result); | |
345 | } | |
a60c0bc8 | 346 | } |
be2fb01f CW |
347 | CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', [ |
348 | 1 => [$dao->id, 'Integer'], | |
349 | ]); | |
1f103dc4 | 350 | } |
6a488035 TO |
351 | } |
352 | ||
2e2605fe EM |
353 | /** |
354 | * Get declarations. | |
355 | * | |
356 | * @return array|null | |
357 | */ | |
e9b95545 TO |
358 | public function getDeclarations() { |
359 | if ($this->declarations === NULL) { | |
be2fb01f | 360 | $this->declarations = []; |
e9b95545 TO |
361 | foreach (CRM_Core_Component::getEnabledComponents() as $component) { |
362 | /** @var CRM_Core_Component_Info $component */ | |
363 | $this->declarations = array_merge($this->declarations, $component->getManagedEntities()); | |
364 | } | |
365 | CRM_Utils_Hook::managed($this->declarations); | |
366 | $this->declarations = self::cleanDeclarations($this->declarations); | |
367 | } | |
368 | return $this->declarations; | |
369 | } | |
370 | ||
6a488035 | 371 | /** |
88718db2 TO |
372 | * @param array $modules |
373 | * Array<CRM_Core_Module>. | |
77b97be7 | 374 | * |
a6c01b45 CW |
375 | * @return array |
376 | * indexed by is_active,name | |
6a488035 TO |
377 | */ |
378 | protected static function createModuleIndex($modules) { | |
be2fb01f | 379 | $result = []; |
6a488035 TO |
380 | foreach ($modules as $module) { |
381 | $result[$module->is_active][$module->name] = $module; | |
382 | } | |
383 | return $result; | |
384 | } | |
385 | ||
386 | /** | |
88718db2 TO |
387 | * @param array $moduleIndex |
388 | * @param array $declarations | |
77b97be7 | 389 | * |
a6c01b45 CW |
390 | * @return array |
391 | * indexed by module,name | |
6a488035 TO |
392 | */ |
393 | protected static function createDeclarationIndex($moduleIndex, $declarations) { | |
be2fb01f | 394 | $result = []; |
6a488035 TO |
395 | if (!isset($moduleIndex[TRUE])) { |
396 | return $result; | |
397 | } | |
398 | foreach ($moduleIndex[TRUE] as $moduleName => $module) { | |
399 | if ($module->is_active) { | |
400 | // need an empty array() for all active modules, even if there are no current $declarations | |
be2fb01f | 401 | $result[$moduleName] = []; |
6a488035 TO |
402 | } |
403 | } | |
404 | foreach ($declarations as $declaration) { | |
405 | $result[$declaration['module']][$declaration['name']] = $declaration; | |
406 | } | |
407 | return $result; | |
408 | } | |
409 | ||
410 | /** | |
fd31fa4c EM |
411 | * @param $declarations |
412 | * | |
72b3a70c CW |
413 | * @return string|bool |
414 | * string on error, or FALSE | |
6a488035 TO |
415 | */ |
416 | protected static function validate($declarations) { | |
417 | foreach ($declarations as $declare) { | |
be2fb01f | 418 | foreach (['name', 'module', 'entity', 'params'] as $key) { |
6a488035 TO |
419 | if (empty($declare[$key])) { |
420 | $str = print_r($declare, TRUE); | |
421 | return ("Managed Entity is missing field \"$key\": $str"); | |
422 | } | |
423 | } | |
424 | // FIXME: validate that each 'module' is known | |
425 | } | |
426 | return FALSE; | |
427 | } | |
428 | ||
a0ee3941 | 429 | /** |
72b3a70c | 430 | * @param array $declarations |
a0ee3941 | 431 | * |
72b3a70c | 432 | * @return array |
a0ee3941 | 433 | */ |
6a488035 TO |
434 | protected static function cleanDeclarations($declarations) { |
435 | foreach ($declarations as $name => &$declare) { | |
436 | if (!array_key_exists('name', $declare)) { | |
437 | $declare['name'] = $name; | |
438 | } | |
439 | } | |
440 | return $declarations; | |
441 | } | |
442 | ||
a0ee3941 | 443 | /** |
e9b95545 TO |
444 | * @param string $entity |
445 | * @param string $action | |
446 | * @param array $params | |
447 | * @param array $result | |
a0ee3941 EM |
448 | * |
449 | * @throws Exception | |
450 | */ | |
e9b95545 | 451 | protected function onApiError($entity, $action, $params, $result) { |
be2fb01f | 452 | CRM_Core_Error::debug_var('ManagedEntities_failed', [ |
e9b95545 TO |
453 | 'entity' => $entity, |
454 | 'action' => $action, | |
6a488035 TO |
455 | 'params' => $params, |
456 | 'result' => $result, | |
be2fb01f | 457 | ]); |
6a488035 | 458 | throw new Exception('API error: ' . $result['error_message']); |
cbb7c7e0 | 459 | } |
96025800 | 460 | |
6a488035 | 461 | } |