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
{
11 * Get clean up options.
15 public static function getCleanupOptions() {
17 'always' => ts('Always'),
18 'never' => ts('Never'),
19 'unused' => ts('If Unused'),
25 * Array($status => array($name => CRM_Core_Module)).
27 protected $moduleIndex;
31 * List of all entity declarations.
32 * @see CRM_Utils_Hook::managed()
34 protected $declarations;
39 * @return \CRM_Core_ManagedEntities
41 public static function singleton($fresh = FALSE) {
43 if ($fresh ||
!$singleton) {
44 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module
::getAll(), NULL);
50 * Perform an asynchronous reconciliation when the transaction ends.
52 public static function scheduleReconciliation() {
53 CRM_Core_Transaction
::addCallback(
54 CRM_Core_Transaction
::PHASE_POST_COMMIT
,
56 CRM_Core_ManagedEntities
::singleton(TRUE)->reconcile();
59 'ManagedEntities::reconcile'
64 * @param array $modules
66 * @param array $declarations
67 * Per hook_civicrm_managed.
69 public function __construct($modules, $declarations) {
70 $this->moduleIndex
= self
::createModuleIndex($modules);
72 if ($declarations !== NULL) {
73 $this->declarations
= self
::cleanDeclarations($declarations);
76 $this->declarations
= NULL;
81 * Read a managed entity using APIv3.
83 * @param string $moduleName
84 * The name of the module which declared entity.
86 * The symbolic name of the entity.
88 * API representation, or NULL if the entity does not exist
90 public function get($moduleName, $name) {
91 $dao = new CRM_Core_DAO_Managed();
92 $dao->module
= $moduleName;
94 if ($dao->find(TRUE)) {
96 'id' => $dao->entity_id
,
100 $result = civicrm_api3($dao->entity_type
, 'getsingle', $params);
102 catch (Exception
$e) {
103 $this->onApiError($dao->entity_type
, 'getsingle', $params, $result);
113 * Identify any enabled/disabled modules. Add new entities, update
114 * existing entities, and remove orphaned (stale) entities.
118 public function reconcile() {
119 if ($error = $this->validate($this->getDeclarations())) {
120 throw new Exception($error);
122 $this->reconcileEnabledModules();
123 $this->reconcileDisabledModules();
124 $this->reconcileUnknownModules();
128 * For all enabled modules, add new entities, update
129 * existing entities, and remove orphaned (stale) entities.
133 public function reconcileEnabledModules() {
134 // Note: any thing currently declared is necessarily from
135 // an active module -- because we got it from a hook!
137 // index by moduleName,name
138 $decls = self
::createDeclarationIndex($this->moduleIndex
, $this->getDeclarations());
139 foreach ($decls as $moduleName => $todos) {
140 if (isset($this->moduleIndex
[TRUE][$moduleName])) {
141 $this->reconcileEnabledModule($this->moduleIndex
[TRUE][$moduleName], $todos);
143 elseif (isset($this->moduleIndex
[FALSE][$moduleName])) {
144 // do nothing -- module should get swept up later
147 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
153 * For one enabled module, add new entities, update existing entities,
154 * and remove orphaned (stale) entities.
156 * @param \CRM_Core_Module $module
157 * @param array $todos
158 * List of entities currently declared by this module.
159 * array(string $name => array $entityDef).
161 public function reconcileEnabledModule(CRM_Core_Module
$module, $todos) {
162 $dao = new CRM_Core_DAO_Managed();
163 $dao->module
= $module->name
;
165 while ($dao->fetch()) {
166 if (isset($todos[$dao->name
]) && $todos[$dao->name
]) {
167 // update existing entity; remove from $todos
168 $this->updateExistingEntity($dao, $todos[$dao->name
]);
169 unset($todos[$dao->name
]);
172 // remove stale entity; not in $todos
173 $this->removeStaleEntity($dao);
177 // create new entities from leftover $todos
178 foreach ($todos as $name => $todo) {
179 $this->insertNewEntity($todo);
184 * For all disabled modules, disable any managed entities.
186 public function reconcileDisabledModules() {
187 if (empty($this->moduleIndex
[FALSE])) {
191 $in = CRM_Core_DAO
::escapeStrings(array_keys($this->moduleIndex
[FALSE]));
192 $dao = new CRM_Core_DAO_Managed();
193 $dao->whereAdd("module in ($in)");
195 while ($dao->fetch()) {
196 $this->disableEntity($dao);
202 * Remove any orphaned (stale) entities that are linked to
205 public function reconcileUnknownModules() {
206 $knownModules = array();
207 if (array_key_exists(0, $this->moduleIndex
) && is_array($this->moduleIndex
[0])) {
208 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[0]));
210 if (array_key_exists(1, $this->moduleIndex
) && is_array($this->moduleIndex
[1])) {
211 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[1]));
214 $dao = new CRM_Core_DAO_Managed();
215 if (!empty($knownModules)) {
216 $in = CRM_Core_DAO
::escapeStrings($knownModules);
217 $dao->whereAdd("module NOT IN ($in)");
220 while ($dao->fetch()) {
221 $this->removeStaleEntity($dao);
226 * Create a new entity.
229 * Entity specification (per hook_civicrm_managedEntities).
231 public function insertNewEntity($todo) {
232 $result = civicrm_api($todo['entity'], 'create', $todo['params']);
233 if ($result['is_error']) {
234 $this->onApiError($todo['entity'], 'create', $todo['params'], $result);
237 $dao = new CRM_Core_DAO_Managed();
238 $dao->module
= $todo['module'];
239 $dao->name
= $todo['name'];
240 $dao->entity_type
= $todo['entity'];
241 $dao->entity_id
= $result['id'];
242 $dao->cleanup
= CRM_Utils_Array
::value('cleanup', $todo);
247 * Update an entity which (a) is believed to exist and which (b) ought to be active.
249 * @param CRM_Core_DAO_Managed $dao
251 * Entity specification (per hook_civicrm_managedEntities).
253 public function updateExistingEntity($dao, $todo) {
254 $policy = CRM_Utils_Array
::value('update', $todo, 'always');
255 $doUpdate = ($policy == 'always');
259 'id' => $dao->entity_id
,
260 'is_active' => 1, // FIXME: test whether is_active is valid
262 $params = array_merge($defaults, $todo['params']);
263 $result = civicrm_api($dao->entity_type
, 'create', $params);
264 if ($result['is_error']) {
265 $this->onApiError($dao->entity_type
, 'create', $params, $result);
269 if (isset($todo['cleanup'])) {
270 $dao->cleanup
= $todo['cleanup'];
276 * Update an entity which (a) is believed to exist and which (b) ought to be
279 * @param CRM_Core_DAO_Managed $dao
281 public function disableEntity($dao) {
282 // FIXME: if ($dao->entity_type supports is_active) {
284 // FIXME cascading for payproc types?
287 'id' => $dao->entity_id
,
290 $result = civicrm_api($dao->entity_type
, 'create', $params);
291 if ($result['is_error']) {
292 $this->onApiError($dao->entity_type
, 'create', $params, $result);
298 * Remove a stale entity (if policy allows).
300 * @param CRM_Core_DAO_Managed $dao
303 public function removeStaleEntity($dao) {
304 $policy = empty($dao->cleanup
) ?
'always' : $dao->cleanup
;
315 $getRefCount = civicrm_api3($dao->entity_type
, 'getrefcount', array(
317 'id' => $dao->entity_id
,
321 foreach ($getRefCount['values'] as $refCount) {
322 $total +
= $refCount['count'];
325 $doDelete = ($total == 0);
329 throw new \
Exception('Unrecognized cleanup policy: ' . $policy);
335 'id' => $dao->entity_id
,
337 $check = civicrm_api3($dao->entity_type
, 'get', $params);
338 if ((bool) $check['count']) {
339 $result = civicrm_api($dao->entity_type
, 'delete', $params);
340 if ($result['is_error']) {
341 $this->onApiError($dao->entity_type
, 'delete', $params, $result);
344 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
345 1 => array($dao->id
, 'Integer'),
356 public function getDeclarations() {
357 if ($this->declarations
=== NULL) {
358 $this->declarations
= array();
359 foreach (CRM_Core_Component
::getEnabledComponents() as $component) {
360 /** @var CRM_Core_Component_Info $component */
361 $this->declarations
= array_merge($this->declarations
, $component->getManagedEntities());
363 CRM_Utils_Hook
::managed($this->declarations
);
364 $this->declarations
= self
::cleanDeclarations($this->declarations
);
366 return $this->declarations
;
370 * @param array $modules
371 * Array<CRM_Core_Module>.
374 * indexed by is_active,name
376 protected static function createModuleIndex($modules) {
378 foreach ($modules as $module) {
379 $result[$module->is_active
][$module->name
] = $module;
385 * @param array $moduleIndex
386 * @param array $declarations
389 * indexed by module,name
391 protected static function createDeclarationIndex($moduleIndex, $declarations) {
393 if (!isset($moduleIndex[TRUE])) {
396 foreach ($moduleIndex[TRUE] as $moduleName => $module) {
397 if ($module->is_active
) {
398 // need an empty array() for all active modules, even if there are no current $declarations
399 $result[$moduleName] = array();
402 foreach ($declarations as $declaration) {
403 $result[$declaration['module']][$declaration['name']] = $declaration;
409 * @param $declarations
411 * @return string|bool
412 * string on error, or FALSE
414 protected static function validate($declarations) {
415 foreach ($declarations as $declare) {
416 foreach (array('name', 'module', 'entity', 'params') as $key) {
417 if (empty($declare[$key])) {
418 $str = print_r($declare, TRUE);
419 return ("Managed Entity is missing field \"$key\": $str");
422 // FIXME: validate that each 'module' is known
428 * @param array $declarations
432 protected static function cleanDeclarations($declarations) {
433 foreach ($declarations as $name => &$declare) {
434 if (!array_key_exists('name', $declare)) {
435 $declare['name'] = $name;
438 return $declarations;
442 * @param string $entity
443 * @param string $action
444 * @param array $params
445 * @param array $result
449 protected function onApiError($entity, $action, $params, $result) {
450 CRM_Core_Error
::debug_var('ManagedEntities_failed', array(
456 throw new Exception('API error: ' . $result['error_message']);