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