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'),
19 * @var array($status => array($name => CRM_Core_Module))
21 protected $moduleIndex;
24 * @var array per hook_civicrm_managed
26 protected $declarations;
31 * @return \CRM_Core_ManagedEntities
33 public static function singleton($fresh = FALSE) {
35 if ($fresh ||
!$singleton) {
36 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module
::getAll(), NULL);
42 * Perform an asynchronous reconciliation when the transaction ends.
44 public static function scheduleReconciliation() {
45 CRM_Core_Transaction
::addCallback(
46 CRM_Core_Transaction
::PHASE_POST_COMMIT
,
48 CRM_Core_ManagedEntities
::singleton(TRUE)->reconcile();
51 'ManagedEntities::reconcile'
56 * @param array $modules CRM_Core_Module
57 * @param array $declarations per hook_civicrm_managed
59 public function __construct($modules, $declarations) {
60 $this->moduleIndex
= self
::createModuleIndex($modules);
62 if ($declarations !== NULL) {
63 $this->declarations
= self
::cleanDeclarations($declarations);
65 $this->declarations
= NULL;
70 * Read the managed entity
72 * @return array|NULL API representation, or NULL if the entity does not exist
74 public function get($moduleName, $name) {
75 $dao = new CRM_Core_DAO_Managed();
76 $dao->module
= $moduleName;
78 if ($dao->find(TRUE)) {
80 'id' => $dao->entity_id
,
84 $result = civicrm_api3($dao->entity_type
, 'getsingle', $params);
86 catch (Exception
$e) {
87 $this->onApiError($dao->entity_type
, 'getsingle', $params, $result);
95 public function reconcile() {
96 if ($error = $this->validate($this->getDeclarations())) {
97 throw new Exception($error);
99 $this->reconcileEnabledModules();
100 $this->reconcileDisabledModules();
101 $this->reconcileUnknownModules();
105 public function reconcileEnabledModules() {
106 // Note: any thing currently declared is necessarily from
107 // an active module -- because we got it from a hook!
109 // index by moduleName,name
110 $decls = self
::createDeclarationIndex($this->moduleIndex
, $this->getDeclarations());
111 foreach ($decls as $moduleName => $todos) {
112 if (isset($this->moduleIndex
[TRUE][$moduleName])) {
113 $this->reconcileEnabledModule($this->moduleIndex
[TRUE][$moduleName], $todos);
114 } elseif (isset($this->moduleIndex
[FALSE][$moduleName])) {
115 // do nothing -- module should get swept up later
117 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
123 * Create, update, and delete entities declared by an active module
125 * @param \CRM_Core_Module|string $module string
126 * @param $todos array $name => array()
128 public function reconcileEnabledModule(CRM_Core_Module
$module, $todos) {
129 $dao = new CRM_Core_DAO_Managed();
130 $dao->module
= $module->name
;
132 while ($dao->fetch()) {
133 if (isset($todos[$dao->name
]) && $todos[$dao->name
]) {
134 // update existing entity; remove from $todos
135 $this->updateExistingEntity($dao, $todos[$dao->name
]);
136 unset($todos[$dao->name
]);
138 // remove stale entity; not in $todos
139 $this->removeStaleEntity($dao);
143 // create new entities from leftover $todos
144 foreach ($todos as $name => $todo) {
145 $this->insertNewEntity($todo);
149 public function reconcileDisabledModules() {
150 if (empty($this->moduleIndex
[FALSE])) {
154 $in = CRM_Core_DAO
::escapeStrings(array_keys($this->moduleIndex
[FALSE]));
155 $dao = new CRM_Core_DAO_Managed();
156 $dao->whereAdd("module in ($in)");
158 while ($dao->fetch()) {
159 $this->disableEntity($dao);
164 public function reconcileUnknownModules() {
165 $knownModules = array();
166 if (array_key_exists(0, $this->moduleIndex
) && is_array($this->moduleIndex
[0])) {
167 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[0]));
169 if (array_key_exists(1, $this->moduleIndex
) && is_array($this->moduleIndex
[1])) {
170 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[1]));
174 $dao = new CRM_Core_DAO_Managed();
175 if (!empty($knownModules)) {
176 $in = CRM_Core_DAO
::escapeStrings($knownModules);
177 $dao->whereAdd("module NOT IN ($in)");
180 while ($dao->fetch()) {
181 $this->removeStaleEntity($dao);
186 * Create a new entity
188 * @param array $todo entity specification (per hook_civicrm_managedEntities)
190 public function insertNewEntity($todo) {
191 $result = civicrm_api($todo['entity'], 'create', $todo['params']);
192 if ($result['is_error']) {
193 $this->onApiError($todo['entity'], 'create', $todo['params'], $result);
196 $dao = new CRM_Core_DAO_Managed();
197 $dao->module
= $todo['module'];
198 $dao->name
= $todo['name'];
199 $dao->entity_type
= $todo['entity'];
200 $dao->entity_id
= $result['id'];
201 $dao->cleanup
= CRM_Utils_Array
::value('cleanup', $todo);
206 * Update an entity which (a) is believed to exist and which (b) ought to be active.
208 * @param CRM_Core_DAO_Managed $dao
209 * @param array $todo entity specification (per hook_civicrm_managedEntities)
211 public function updateExistingEntity($dao, $todo) {
212 $policy = CRM_Utils_Array
::value('update', $todo, 'always');
213 $doUpdate = ($policy == 'always');
217 'id' => $dao->entity_id
,
218 'is_active' => 1, // FIXME: test whether is_active is valid
220 $params = array_merge($defaults, $todo['params']);
221 $result = civicrm_api($dao->entity_type
, 'create', $params);
222 if ($result['is_error']) {
223 $this->onApiError($dao->entity_type
, 'create',$params, $result);
227 if (isset($todo['cleanup'])) {
228 $dao->cleanup
= $todo['cleanup'];
234 * Update an entity which (a) is believed to exist and which (b) ought to be
237 * @param CRM_Core_DAO_Managed $dao
239 public function disableEntity($dao) {
240 // FIXME: if ($dao->entity_type supports is_active) {
242 // FIXME cascading for payproc types?
245 'id' => $dao->entity_id
,
248 $result = civicrm_api($dao->entity_type
, 'create', $params);
249 if ($result['is_error']) {
250 $this->onApiError($dao->entity_type
, 'create',$params, $result);
256 * Remove a stale entity (if policy allows)
258 * @param CRM_Core_DAO_Managed $dao
260 public function removeStaleEntity($dao) {
261 $policy = empty($dao->cleanup
) ?
'always' : $dao->cleanup
;
270 $getRefCount = civicrm_api3($dao->entity_type
, 'getrefcount', array(
272 'id' => $dao->entity_id
276 foreach ($getRefCount['values'] as $refCount) {
277 $total +
= $refCount['count'];
280 $doDelete = ($total == 0);
283 throw new \
Exception('Unrecognized cleanup policy: ' . $policy);
289 'id' => $dao->entity_id
,
291 $result = civicrm_api($dao->entity_type
, 'delete', $params);
292 if ($result['is_error']) {
293 $this->onApiError($dao->entity_type
, 'delete', $params, $result);
296 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
297 1 => array($dao->id
, 'Integer')
302 public function getDeclarations() {
303 if ($this->declarations
=== NULL) {
304 $this->declarations
= array();
305 foreach (CRM_Core_Component
::getEnabledComponents() as $component) {
306 /** @var CRM_Core_Component_Info $component */
307 $this->declarations
= array_merge($this->declarations
, $component->getManagedEntities());
309 CRM_Utils_Hook
::managed($this->declarations
);
310 $this->declarations
= self
::cleanDeclarations($this->declarations
);
312 return $this->declarations
;
318 * @return array indexed by is_active,name
320 protected static function createModuleIndex($modules) {
322 foreach ($modules as $module) {
323 $result[$module->is_active
][$module->name
] = $module;
329 * @param $moduleIndex
330 * @param $declarations
332 * @return array indexed by module,name
334 protected static function createDeclarationIndex($moduleIndex, $declarations) {
336 if (!isset($moduleIndex[TRUE])) {
339 foreach ($moduleIndex[TRUE] as $moduleName => $module) {
340 if ($module->is_active
) {
341 // need an empty array() for all active modules, even if there are no current $declarations
342 $result[$moduleName] = array();
345 foreach ($declarations as $declaration) {
346 $result[$declaration['module']][$declaration['name']] = $declaration;
352 * @param $declarations
354 * @return mixed string on error, or FALSE
356 protected static function validate($declarations) {
357 foreach ($declarations as $declare) {
358 foreach (array('name', 'module', 'entity', 'params') as $key) {
359 if (empty($declare[$key])) {
360 $str = print_r($declare, TRUE);
361 return ("Managed Entity is missing field \"$key\": $str");
364 // FIXME: validate that each 'module' is known
370 * @param $declarations
374 protected static function cleanDeclarations($declarations) {
375 foreach ($declarations as $name => &$declare) {
376 if (!array_key_exists('name', $declare)) {
377 $declare['name'] = $name;
380 return $declarations;
384 * @param string $entity
385 * @param string $action
386 * @param array $params
387 * @param array $result
391 protected function onApiError($entity, $action, $params, $result) {
392 CRM_Core_Error
::debug_var('ManagedEntities_failed', array(
398 throw new Exception('API error: ' . $result['error_message']);