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