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.
8 class CRM_Core_ManagedEntities
{
10 public static function getCleanupOptions() {
12 'always' => ts('Always'),
13 'never' => ts('Never'),
14 'unused' => ts('If Unused'),
20 * Array($status => array($name => CRM_Core_Module)).
22 protected $moduleIndex;
26 * List of all entity declarations.
27 * @see CRM_Utils_Hook::managed()
29 protected $declarations;
34 * @return \CRM_Core_ManagedEntities
36 public static function singleton($fresh = FALSE) {
38 if ($fresh ||
!$singleton) {
39 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module
::getAll(), NULL);
45 * Perform an asynchronous reconciliation when the transaction ends.
47 public static function scheduleReconciliation() {
48 CRM_Core_Transaction
::addCallback(
49 CRM_Core_Transaction
::PHASE_POST_COMMIT
,
51 CRM_Core_ManagedEntities
::singleton(TRUE)->reconcile();
54 'ManagedEntities::reconcile'
59 * @param array $modules
61 * @param array $declarations
62 * Per hook_civicrm_managed.
64 public function __construct($modules, $declarations) {
65 $this->moduleIndex
= self
::createModuleIndex($modules);
67 if ($declarations !== NULL) {
68 $this->declarations
= self
::cleanDeclarations($declarations);
71 $this->declarations
= NULL;
76 * Read a managed entity using APIv3.
78 * @param string $moduleName
79 * The name of the module which declared entity.
81 * The symbolic name of the entity.
83 * API representation, or NULL if the entity does not exist
85 public function get($moduleName, $name) {
86 $dao = new CRM_Core_DAO_Managed();
87 $dao->module
= $moduleName;
89 if ($dao->find(TRUE)) {
91 'id' => $dao->entity_id
,
95 $result = civicrm_api3($dao->entity_type
, 'getsingle', $params);
97 catch (Exception
$e) {
98 $this->onApiError($dao->entity_type
, 'getsingle', $params, $result);
108 * Identify any enabled/disabled modules. Add new entities, update
109 * existing entities, and remove orphaned (stale) entities.
113 public function reconcile() {
114 if ($error = $this->validate($this->getDeclarations())) {
115 throw new Exception($error);
117 $this->reconcileEnabledModules();
118 $this->reconcileDisabledModules();
119 $this->reconcileUnknownModules();
123 * For all enabled modules, add new entities, update
124 * existing entities, and remove orphaned (stale) entities.
128 public function reconcileEnabledModules() {
129 // Note: any thing currently declared is necessarily from
130 // an active module -- because we got it from a hook!
132 // index by moduleName,name
133 $decls = self
::createDeclarationIndex($this->moduleIndex
, $this->getDeclarations());
134 foreach ($decls as $moduleName => $todos) {
135 if (isset($this->moduleIndex
[TRUE][$moduleName])) {
136 $this->reconcileEnabledModule($this->moduleIndex
[TRUE][$moduleName], $todos);
138 elseif (isset($this->moduleIndex
[FALSE][$moduleName])) {
139 // do nothing -- module should get swept up later
142 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
148 * For one enabled module, add new entities, update existing entities,
149 * and remove orphaned (stale) entities.
151 * @param \CRM_Core_Module $module
152 * @param array $todos
153 * List of entities currently declared by this module.
154 * array(string $name => array $entityDef).
156 public function reconcileEnabledModule(CRM_Core_Module
$module, $todos) {
157 $dao = new CRM_Core_DAO_Managed();
158 $dao->module
= $module->name
;
160 while ($dao->fetch()) {
161 if (isset($todos[$dao->name
]) && $todos[$dao->name
]) {
162 // update existing entity; remove from $todos
163 $this->updateExistingEntity($dao, $todos[$dao->name
]);
164 unset($todos[$dao->name
]);
167 // remove stale entity; not in $todos
168 $this->removeStaleEntity($dao);
172 // create new entities from leftover $todos
173 foreach ($todos as $name => $todo) {
174 $this->insertNewEntity($todo);
179 * For all disabled modules, disable any managed entities.
181 public function reconcileDisabledModules() {
182 if (empty($this->moduleIndex
[FALSE])) {
186 $in = CRM_Core_DAO
::escapeStrings(array_keys($this->moduleIndex
[FALSE]));
187 $dao = new CRM_Core_DAO_Managed();
188 $dao->whereAdd("module in ($in)");
190 while ($dao->fetch()) {
191 $this->disableEntity($dao);
197 * Remove any orphaned (stale) entities that are linked to
200 public function reconcileUnknownModules() {
201 $knownModules = array();
202 if (array_key_exists(0, $this->moduleIndex
) && is_array($this->moduleIndex
[0])) {
203 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[0]));
205 if (array_key_exists(1, $this->moduleIndex
) && is_array($this->moduleIndex
[1])) {
206 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[1]));
209 $dao = new CRM_Core_DAO_Managed();
210 if (!empty($knownModules)) {
211 $in = CRM_Core_DAO
::escapeStrings($knownModules);
212 $dao->whereAdd("module NOT IN ($in)");
215 while ($dao->fetch()) {
216 $this->removeStaleEntity($dao);
221 * Create a new entity.
224 * Entity specification (per hook_civicrm_managedEntities).
226 public function insertNewEntity($todo) {
227 $result = civicrm_api($todo['entity'], 'create', $todo['params']);
228 if ($result['is_error']) {
229 $this->onApiError($todo['entity'], 'create', $todo['params'], $result);
232 $dao = new CRM_Core_DAO_Managed();
233 $dao->module
= $todo['module'];
234 $dao->name
= $todo['name'];
235 $dao->entity_type
= $todo['entity'];
236 $dao->entity_id
= $result['id'];
237 $dao->cleanup
= CRM_Utils_Array
::value('cleanup', $todo);
242 * Update an entity which (a) is believed to exist and which (b) ought to be active.
244 * @param CRM_Core_DAO_Managed $dao
246 * Entity specification (per hook_civicrm_managedEntities).
248 public function updateExistingEntity($dao, $todo) {
249 $policy = CRM_Utils_Array
::value('update', $todo, 'always');
250 $doUpdate = ($policy == 'always');
254 'id' => $dao->entity_id
,
255 'is_active' => 1, // FIXME: test whether is_active is valid
257 $params = array_merge($defaults, $todo['params']);
258 $result = civicrm_api($dao->entity_type
, 'create', $params);
259 if ($result['is_error']) {
260 $this->onApiError($dao->entity_type
, 'create', $params, $result);
264 if (isset($todo['cleanup'])) {
265 $dao->cleanup
= $todo['cleanup'];
271 * Update an entity which (a) is believed to exist and which (b) ought to be
274 * @param CRM_Core_DAO_Managed $dao
276 public function disableEntity($dao) {
277 // FIXME: if ($dao->entity_type supports is_active) {
279 // FIXME cascading for payproc types?
282 'id' => $dao->entity_id
,
285 $result = civicrm_api($dao->entity_type
, 'create', $params);
286 if ($result['is_error']) {
287 $this->onApiError($dao->entity_type
, 'create', $params, $result);
293 * Remove a stale entity (if policy allows).
295 * @param CRM_Core_DAO_Managed $dao
298 public function removeStaleEntity($dao) {
299 $policy = empty($dao->cleanup
) ?
'always' : $dao->cleanup
;
310 $getRefCount = civicrm_api3($dao->entity_type
, 'getrefcount', array(
312 'id' => $dao->entity_id
,
316 foreach ($getRefCount['values'] as $refCount) {
317 $total +
= $refCount['count'];
320 $doDelete = ($total == 0);
324 throw new \
Exception('Unrecognized cleanup policy: ' . $policy);
330 'id' => $dao->entity_id
,
332 $result = civicrm_api($dao->entity_type
, 'delete', $params);
333 if ($result['is_error']) {
334 $this->onApiError($dao->entity_type
, 'delete', $params, $result);
337 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
338 1 => array($dao->id
, 'Integer'),
343 public function getDeclarations() {
344 if ($this->declarations
=== NULL) {
345 $this->declarations
= array();
346 foreach (CRM_Core_Component
::getEnabledComponents() as $component) {
347 /** @var CRM_Core_Component_Info $component */
348 $this->declarations
= array_merge($this->declarations
, $component->getManagedEntities());
350 CRM_Utils_Hook
::managed($this->declarations
);
351 $this->declarations
= self
::cleanDeclarations($this->declarations
);
353 return $this->declarations
;
357 * @param array $modules
358 * Array<CRM_Core_Module>.
361 * indexed by is_active,name
363 protected static function createModuleIndex($modules) {
365 foreach ($modules as $module) {
366 $result[$module->is_active
][$module->name
] = $module;
372 * @param array $moduleIndex
373 * @param array $declarations
376 * indexed by module,name
378 protected static function createDeclarationIndex($moduleIndex, $declarations) {
380 if (!isset($moduleIndex[TRUE])) {
383 foreach ($moduleIndex[TRUE] as $moduleName => $module) {
384 if ($module->is_active
) {
385 // need an empty array() for all active modules, even if there are no current $declarations
386 $result[$moduleName] = array();
389 foreach ($declarations as $declaration) {
390 $result[$declaration['module']][$declaration['name']] = $declaration;
396 * @param $declarations
398 * @return string|bool
399 * string on error, or FALSE
401 protected static function validate($declarations) {
402 foreach ($declarations as $declare) {
403 foreach (array('name', 'module', 'entity', 'params') as $key) {
404 if (empty($declare[$key])) {
405 $str = print_r($declare, TRUE);
406 return ("Managed Entity is missing field \"$key\": $str");
409 // FIXME: validate that each 'module' is known
415 * @param array $declarations
419 protected static function cleanDeclarations($declarations) {
420 foreach ($declarations as $name => &$declare) {
421 if (!array_key_exists('name', $declare)) {
422 $declare['name'] = $name;
425 return $declarations;
429 * @param string $entity
430 * @param string $action
431 * @param array $params
432 * @param array $result
436 protected function onApiError($entity, $action, $params, $result) {
437 CRM_Core_Error
::debug_var('ManagedEntities_failed', array(
443 throw new Exception('API error: ' . $result['error_message']);