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 | ||
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 |