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