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