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