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 | |
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 |
6a0b768e TO |
132 | * @param $todos |
133 | * Array $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']) { | |
e9b95545 | 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']) { | |
e9b95545 | 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; | |
276 | case 'never': | |
277 | $doDelete = FALSE; | |
278 | break; | |
279 | case 'unused': | |
280 | $getRefCount = civicrm_api3($dao->entity_type, 'getrefcount', array( | |
281 | 'debug' => 1, | |
282 | 'id' => $dao->entity_id | |
283 | )); | |
284 | ||
285 | $total = 0; | |
286 | foreach ($getRefCount['values'] as $refCount) { | |
287 | $total += $refCount['count']; | |
288 | } | |
289 | ||
290 | $doDelete = ($total == 0); | |
291 | break; | |
292 | default: | |
293 | throw new \Exception('Unrecognized cleanup policy: ' . $policy); | |
294 | } | |
bbf66e9c | 295 | |
1f103dc4 TO |
296 | if ($doDelete) { |
297 | $params = array( | |
298 | 'version' => 3, | |
299 | 'id' => $dao->entity_id, | |
300 | ); | |
301 | $result = civicrm_api($dao->entity_type, 'delete', $params); | |
302 | if ($result['is_error']) { | |
e9b95545 | 303 | $this->onApiError($dao->entity_type, 'delete', $params, $result); |
1f103dc4 TO |
304 | } |
305 | ||
306 | CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array( | |
307 | 1 => array($dao->id, 'Integer') | |
308 | )); | |
309 | } | |
6a488035 TO |
310 | } |
311 | ||
e9b95545 TO |
312 | public function getDeclarations() { |
313 | if ($this->declarations === NULL) { | |
314 | $this->declarations = array(); | |
315 | foreach (CRM_Core_Component::getEnabledComponents() as $component) { | |
316 | /** @var CRM_Core_Component_Info $component */ | |
317 | $this->declarations = array_merge($this->declarations, $component->getManagedEntities()); | |
318 | } | |
319 | CRM_Utils_Hook::managed($this->declarations); | |
320 | $this->declarations = self::cleanDeclarations($this->declarations); | |
321 | } | |
322 | return $this->declarations; | |
323 | } | |
324 | ||
6a488035 | 325 | /** |
77b97be7 EM |
326 | * @param $modules |
327 | * | |
6a488035 TO |
328 | * @return array indexed by is_active,name |
329 | */ | |
330 | protected static function createModuleIndex($modules) { | |
331 | $result = array(); | |
332 | foreach ($modules as $module) { | |
333 | $result[$module->is_active][$module->name] = $module; | |
334 | } | |
335 | return $result; | |
336 | } | |
337 | ||
338 | /** | |
77b97be7 EM |
339 | * @param $moduleIndex |
340 | * @param $declarations | |
341 | * | |
6a488035 TO |
342 | * @return array indexed by module,name |
343 | */ | |
344 | protected static function createDeclarationIndex($moduleIndex, $declarations) { | |
345 | $result = array(); | |
346 | if (!isset($moduleIndex[TRUE])) { | |
347 | return $result; | |
348 | } | |
349 | foreach ($moduleIndex[TRUE] as $moduleName => $module) { | |
350 | if ($module->is_active) { | |
351 | // need an empty array() for all active modules, even if there are no current $declarations | |
352 | $result[$moduleName] = array(); | |
353 | } | |
354 | } | |
355 | foreach ($declarations as $declaration) { | |
356 | $result[$declaration['module']][$declaration['name']] = $declaration; | |
357 | } | |
358 | return $result; | |
359 | } | |
360 | ||
361 | /** | |
fd31fa4c EM |
362 | * @param $declarations |
363 | * | |
6a488035 TO |
364 | * @return mixed string on error, or FALSE |
365 | */ | |
366 | protected static function validate($declarations) { | |
367 | foreach ($declarations as $declare) { | |
368 | foreach (array('name', 'module', 'entity', 'params') as $key) { | |
369 | if (empty($declare[$key])) { | |
370 | $str = print_r($declare, TRUE); | |
371 | return ("Managed Entity is missing field \"$key\": $str"); | |
372 | } | |
373 | } | |
374 | // FIXME: validate that each 'module' is known | |
375 | } | |
376 | return FALSE; | |
377 | } | |
378 | ||
a0ee3941 EM |
379 | /** |
380 | * @param $declarations | |
381 | * | |
382 | * @return mixed | |
383 | */ | |
6a488035 TO |
384 | protected static function cleanDeclarations($declarations) { |
385 | foreach ($declarations as $name => &$declare) { | |
386 | if (!array_key_exists('name', $declare)) { | |
387 | $declare['name'] = $name; | |
388 | } | |
389 | } | |
390 | return $declarations; | |
391 | } | |
392 | ||
a0ee3941 | 393 | /** |
e9b95545 TO |
394 | * @param string $entity |
395 | * @param string $action | |
396 | * @param array $params | |
397 | * @param array $result | |
a0ee3941 EM |
398 | * |
399 | * @throws Exception | |
400 | */ | |
e9b95545 | 401 | protected function onApiError($entity, $action, $params, $result) { |
6a488035 | 402 | CRM_Core_Error::debug_var('ManagedEntities_failed', array( |
e9b95545 TO |
403 | 'entity' => $entity, |
404 | 'action' => $action, | |
6a488035 TO |
405 | 'params' => $params, |
406 | 'result' => $result, | |
407 | )); | |
408 | throw new Exception('API error: ' . $result['error_message']); | |
cbb7c7e0 | 409 | } |
6a488035 | 410 | } |