Commit | Line | Data |
---|---|---|
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 | */ | |
8 | class 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 TO |
398 | } |
399 |