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 TO |
15 | public static function getCleanupOptions() { |
16 | return array( | |
17 | 'always' => ts('Always'), | |
18 | 'never' => ts('Never'), | |
19 | 'unused' => ts('If Unused'), | |
20 | ); | |
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 TO |
57 | }, |
58 | array(), | |
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)) { | |
95 | $params = array( | |
6a488035 TO |
96 | 'id' => $dao->entity_id, |
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 TO |
206 | public function reconcileUnknownModules() { |
207 | $knownModules = array(); | |
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) { | |
260 | $defaults = array( | |
261 | 'id' => $dao->entity_id, | |
262 | 'is_active' => 1, // FIXME: test whether is_active is valid | |
263 | ); | |
264 | $params = array_merge($defaults, $todo['params']); | |
265 | $result = civicrm_api($dao->entity_type, 'create', $params); | |
266 | if ($result['is_error']) { | |
353ffa53 | 267 | $this->onApiError($dao->entity_type, 'create', $params, $result); |
0dd54586 | 268 | } |
7cddb4ae | 269 | } |
1f103dc4 TO |
270 | |
271 | if (isset($todo['cleanup'])) { | |
272 | $dao->cleanup = $todo['cleanup']; | |
273 | $dao->update(); | |
274 | } | |
7cddb4ae TO |
275 | } |
276 | ||
277 | /** | |
278 | * Update an entity which (a) is believed to exist and which (b) ought to be | |
279 | * inactive. | |
280 | * | |
281 | * @param CRM_Core_DAO_Managed $dao | |
282 | */ | |
283 | public function disableEntity($dao) { | |
284 | // FIXME: if ($dao->entity_type supports is_active) { | |
285 | if (TRUE) { | |
286 | // FIXME cascading for payproc types? | |
287 | $params = array( | |
288 | 'version' => 3, | |
289 | 'id' => $dao->entity_id, | |
290 | 'is_active' => 0, | |
291 | ); | |
292 | $result = civicrm_api($dao->entity_type, 'create', $params); | |
293 | if ($result['is_error']) { | |
353ffa53 | 294 | $this->onApiError($dao->entity_type, 'create', $params, $result); |
7cddb4ae TO |
295 | } |
296 | } | |
297 | } | |
298 | ||
bbf66e9c | 299 | /** |
88718db2 | 300 | * Remove a stale entity (if policy allows). |
bbf66e9c TO |
301 | * |
302 | * @param CRM_Core_DAO_Managed $dao | |
88718db2 | 303 | * @throws Exception |
bbf66e9c TO |
304 | */ |
305 | public function removeStaleEntity($dao) { | |
1f103dc4 | 306 | $policy = empty($dao->cleanup) ? 'always' : $dao->cleanup; |
378e2654 TO |
307 | switch ($policy) { |
308 | case 'always': | |
309 | $doDelete = TRUE; | |
310 | break; | |
ea100cb5 | 311 | |
378e2654 TO |
312 | case 'never': |
313 | $doDelete = FALSE; | |
314 | break; | |
ea100cb5 | 315 | |
378e2654 TO |
316 | case 'unused': |
317 | $getRefCount = civicrm_api3($dao->entity_type, 'getrefcount', array( | |
318 | 'debug' => 1, | |
21dfd5f5 | 319 | 'id' => $dao->entity_id, |
378e2654 TO |
320 | )); |
321 | ||
322 | $total = 0; | |
323 | foreach ($getRefCount['values'] as $refCount) { | |
324 | $total += $refCount['count']; | |
325 | } | |
326 | ||
327 | $doDelete = ($total == 0); | |
328 | break; | |
ea100cb5 | 329 | |
378e2654 TO |
330 | default: |
331 | throw new \Exception('Unrecognized cleanup policy: ' . $policy); | |
332 | } | |
bbf66e9c | 333 | |
1f103dc4 TO |
334 | if ($doDelete) { |
335 | $params = array( | |
336 | 'version' => 3, | |
337 | 'id' => $dao->entity_id, | |
338 | ); | |
a60c0bc8 SL |
339 | $check = civicrm_api3($dao->entity_type, 'get', $params); |
340 | if ((bool) $check['count']) { | |
341 | $result = civicrm_api($dao->entity_type, 'delete', $params); | |
342 | if ($result['is_error']) { | |
343 | $this->onApiError($dao->entity_type, 'delete', $params, $result); | |
344 | } | |
a60c0bc8 | 345 | } |
7f096a9b MM |
346 | CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array( |
347 | 1 => array($dao->id, 'Integer'), | |
348 | )); | |
1f103dc4 | 349 | } |
6a488035 TO |
350 | } |
351 | ||
2e2605fe EM |
352 | /** |
353 | * Get declarations. | |
354 | * | |
355 | * @return array|null | |
356 | */ | |
e9b95545 TO |
357 | public function getDeclarations() { |
358 | if ($this->declarations === NULL) { | |
359 | $this->declarations = array(); | |
360 | foreach (CRM_Core_Component::getEnabledComponents() as $component) { | |
361 | /** @var CRM_Core_Component_Info $component */ | |
362 | $this->declarations = array_merge($this->declarations, $component->getManagedEntities()); | |
363 | } | |
364 | CRM_Utils_Hook::managed($this->declarations); | |
365 | $this->declarations = self::cleanDeclarations($this->declarations); | |
366 | } | |
367 | return $this->declarations; | |
368 | } | |
369 | ||
6a488035 | 370 | /** |
88718db2 TO |
371 | * @param array $modules |
372 | * Array<CRM_Core_Module>. | |
77b97be7 | 373 | * |
a6c01b45 CW |
374 | * @return array |
375 | * indexed by is_active,name | |
6a488035 TO |
376 | */ |
377 | protected static function createModuleIndex($modules) { | |
378 | $result = array(); | |
379 | foreach ($modules as $module) { | |
380 | $result[$module->is_active][$module->name] = $module; | |
381 | } | |
382 | return $result; | |
383 | } | |
384 | ||
385 | /** | |
88718db2 TO |
386 | * @param array $moduleIndex |
387 | * @param array $declarations | |
77b97be7 | 388 | * |
a6c01b45 CW |
389 | * @return array |
390 | * indexed by module,name | |
6a488035 TO |
391 | */ |
392 | protected static function createDeclarationIndex($moduleIndex, $declarations) { | |
393 | $result = array(); | |
394 | if (!isset($moduleIndex[TRUE])) { | |
395 | return $result; | |
396 | } | |
397 | foreach ($moduleIndex[TRUE] as $moduleName => $module) { | |
398 | if ($module->is_active) { | |
399 | // need an empty array() for all active modules, even if there are no current $declarations | |
400 | $result[$moduleName] = array(); | |
401 | } | |
402 | } | |
403 | foreach ($declarations as $declaration) { | |
404 | $result[$declaration['module']][$declaration['name']] = $declaration; | |
405 | } | |
406 | return $result; | |
407 | } | |
408 | ||
409 | /** | |
fd31fa4c EM |
410 | * @param $declarations |
411 | * | |
72b3a70c CW |
412 | * @return string|bool |
413 | * string on error, or FALSE | |
6a488035 TO |
414 | */ |
415 | protected static function validate($declarations) { | |
416 | foreach ($declarations as $declare) { | |
417 | foreach (array('name', 'module', 'entity', 'params') as $key) { | |
418 | if (empty($declare[$key])) { | |
419 | $str = print_r($declare, TRUE); | |
420 | return ("Managed Entity is missing field \"$key\": $str"); | |
421 | } | |
422 | } | |
423 | // FIXME: validate that each 'module' is known | |
424 | } | |
425 | return FALSE; | |
426 | } | |
427 | ||
a0ee3941 | 428 | /** |
72b3a70c | 429 | * @param array $declarations |
a0ee3941 | 430 | * |
72b3a70c | 431 | * @return array |
a0ee3941 | 432 | */ |
6a488035 TO |
433 | protected static function cleanDeclarations($declarations) { |
434 | foreach ($declarations as $name => &$declare) { | |
435 | if (!array_key_exists('name', $declare)) { | |
436 | $declare['name'] = $name; | |
437 | } | |
438 | } | |
439 | return $declarations; | |
440 | } | |
441 | ||
a0ee3941 | 442 | /** |
e9b95545 TO |
443 | * @param string $entity |
444 | * @param string $action | |
445 | * @param array $params | |
446 | * @param array $result | |
a0ee3941 EM |
447 | * |
448 | * @throws Exception | |
449 | */ | |
e9b95545 | 450 | protected function onApiError($entity, $action, $params, $result) { |
6a488035 | 451 | CRM_Core_Error::debug_var('ManagedEntities_failed', array( |
e9b95545 TO |
452 | 'entity' => $entity, |
453 | 'action' => $action, | |
6a488035 TO |
454 | 'params' => $params, |
455 | 'result' => $result, | |
456 | )); | |
457 | throw new Exception('API error: ' . $result['error_message']); | |
cbb7c7e0 | 458 | } |
96025800 | 459 | |
6a488035 | 460 | } |