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 public static function singleton($fresh = FALSE) {
33 if ($fresh ||
!$singleton) {
34 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module
::getAll(), NULL);
40 * @param array $modules CRM_Core_Module
41 * @param array $declarations per hook_civicrm_managed
43 public function __construct($modules, $declarations) {
44 $this->moduleIndex
= self
::createModuleIndex($modules);
46 if ($declarations !== NULL) {
47 $this->declarations
= self
::cleanDeclarations($declarations);
49 $this->declarations
= NULL;
54 * Read the managed entity
56 * @return array|NULL API representation, or NULL if the entity does not exist
58 public function get($moduleName, $name) {
59 $dao = new CRM_Core_DAO_Managed();
60 $dao->module
= $moduleName;
62 if ($dao->find(TRUE)) {
64 'id' => $dao->entity_id
,
68 $result = civicrm_api3($dao->entity_type
, 'getsingle', $params);
70 catch (Exception
$e) {
71 $this->onApiError($dao->entity_type
, 'getsingle', $params, $result);
79 public function reconcile() {
80 if ($error = $this->validate($this->getDeclarations())) {
81 throw new Exception($error);
83 $this->reconcileEnabledModules();
84 $this->reconcileDisabledModules();
85 $this->reconcileUnknownModules();
89 public function reconcileEnabledModules() {
90 // Note: any thing currently declared is necessarily from
91 // an active module -- because we got it from a hook!
93 // index by moduleName,name
94 $decls = self
::createDeclarationIndex($this->moduleIndex
, $this->getDeclarations());
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
101 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
107 * Create, update, and delete entities declared by an active module
109 * @param \CRM_Core_Module|string $module string
110 * @param $todos array $name => array()
112 public function reconcileEnabledModule(CRM_Core_Module
$module, $todos) {
113 $dao = new CRM_Core_DAO_Managed();
114 $dao->module
= $module->name
;
116 while ($dao->fetch()) {
117 if (isset($todos[$dao->name
]) && $todos[$dao->name
]) {
118 // update existing entity; remove from $todos
119 $this->updateExistingEntity($dao, $todos[$dao->name
]);
120 unset($todos[$dao->name
]);
122 // remove stale entity; not in $todos
123 $this->removeStaleEntity($dao);
127 // create new entities from leftover $todos
128 foreach ($todos as $name => $todo) {
129 $this->insertNewEntity($todo);
133 public function reconcileDisabledModules() {
134 if (empty($this->moduleIndex
[FALSE])) {
138 $in = CRM_Core_DAO
::escapeStrings(array_keys($this->moduleIndex
[FALSE]));
139 $dao = new CRM_Core_DAO_Managed();
140 $dao->whereAdd("module in ($in)");
142 while ($dao->fetch()) {
143 $this->disableEntity($dao);
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]));
153 if (array_key_exists(1, $this->moduleIndex
) && is_array($this->moduleIndex
[1])) {
154 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[1]));
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)");
164 while ($dao->fetch()) {
165 $this->removeStaleEntity($dao);
170 * Create a new entity
172 * @param array $todo entity specification (per hook_civicrm_managedEntities)
174 public function insertNewEntity($todo) {
175 $result = civicrm_api($todo['entity'], 'create', $todo['params']);
176 if ($result['is_error']) {
177 $this->onApiError($todo['entity'], 'create', $todo['params'], $result);
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'];
185 $dao->cleanup
= CRM_Utils_Array
::value('cleanup', $todo);
190 * Update an entity which (a) is believed to exist and which (b) ought to be active.
192 * @param CRM_Core_DAO_Managed $dao
193 * @param array $todo entity specification (per hook_civicrm_managedEntities)
195 public function updateExistingEntity($dao, $todo) {
196 $policy = CRM_Utils_Array
::value('update', $todo, 'always');
197 $doUpdate = ($policy == 'always');
201 'id' => $dao->entity_id
,
202 'is_active' => 1, // FIXME: test whether is_active is valid
204 $params = array_merge($defaults, $todo['params']);
205 $result = civicrm_api($dao->entity_type
, 'create', $params);
206 if ($result['is_error']) {
207 $this->onApiError($dao->entity_type
, 'create',$params, $result);
211 if (isset($todo['cleanup'])) {
212 $dao->cleanup
= $todo['cleanup'];
218 * Update an entity which (a) is believed to exist and which (b) ought to be
221 * @param CRM_Core_DAO_Managed $dao
223 public function disableEntity($dao) {
224 // FIXME: if ($dao->entity_type supports is_active) {
226 // FIXME cascading for payproc types?
229 'id' => $dao->entity_id
,
232 $result = civicrm_api($dao->entity_type
, 'create', $params);
233 if ($result['is_error']) {
234 $this->onApiError($dao->entity_type
, 'create',$params, $result);
240 * Remove a stale entity (if policy allows)
242 * @param CRM_Core_DAO_Managed $dao
244 public function removeStaleEntity($dao) {
245 $policy = empty($dao->cleanup
) ?
'always' : $dao->cleanup
;
254 $getRefCount = civicrm_api3($dao->entity_type
, 'getrefcount', array(
256 'id' => $dao->entity_id
260 foreach ($getRefCount['values'] as $refCount) {
261 $total +
= $refCount['count'];
264 $doDelete = ($total == 0);
267 throw new \
Exception('Unrecognized cleanup policy: ' . $policy);
273 'id' => $dao->entity_id
,
275 $result = civicrm_api($dao->entity_type
, 'delete', $params);
276 if ($result['is_error']) {
277 $this->onApiError($dao->entity_type
, 'delete', $params, $result);
280 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
281 1 => array($dao->id
, 'Integer')
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());
293 CRM_Utils_Hook
::managed($this->declarations
);
294 $this->declarations
= self
::cleanDeclarations($this->declarations
);
296 return $this->declarations
;
302 * @return array indexed by is_active,name
304 protected static function createModuleIndex($modules) {
306 foreach ($modules as $module) {
307 $result[$module->is_active
][$module->name
] = $module;
313 * @param $moduleIndex
314 * @param $declarations
316 * @return array indexed by module,name
318 protected static function createDeclarationIndex($moduleIndex, $declarations) {
320 if (!isset($moduleIndex[TRUE])) {
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();
329 foreach ($declarations as $declaration) {
330 $result[$declaration['module']][$declaration['name']] = $declaration;
336 * @param $declarations
338 * @return mixed string on error, or FALSE
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");
348 // FIXME: validate that each 'module' is known
354 * @param $declarations
358 protected static function cleanDeclarations($declarations) {
359 foreach ($declarations as $name => &$declare) {
360 if (!array_key_exists('name', $declare)) {
361 $declare['name'] = $name;
364 return $declarations;
368 * @param string $entity
369 * @param string $action
370 * @param array $params
371 * @param array $result
375 protected function onApiError($entity, $action, $params, $result) {
376 CRM_Core_Error
::debug_var('ManagedEntities_failed', array(
382 throw new Exception('API error: ' . $result['error_message']);