Update Copywrite year to be 2019
[civicrm-core.git] / CRM / Core / ManagedEntities.php
CommitLineData
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 */
8class 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}