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 * @var array($status => array($name => CRM_Core_Module))
15 * @var array per hook_civicrm_managed
22 public static function singleton($fresh = FALSE) {
24 if ($fresh ||
!$singleton) {
25 $declarations = array();
26 foreach (CRM_Core_Component
::getEnabledComponents() as $component) {
27 /** @var CRM_Core_Component_Info $component */
28 $declarations = array_merge($declarations, $component->getManagedEntities());
30 CRM_Utils_Hook
::managed($declarations);
31 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module
::getAll(), $declarations);
37 * @param array $modules CRM_Core_Module
38 * @param array $declarations per hook_civicrm_managed
40 public function __construct($modules, $declarations) {
41 $this->moduleIndex
= self
::createModuleIndex($modules);
42 $this->declarations
= self
::cleanDeclarations($declarations);
46 * Read the managed entity
48 public function get($moduleName, $name) {
49 $dao = new CRM_Core_DAO_Managed();
50 $dao->module
= $moduleName;
52 if ($dao->find(TRUE)) {
54 'id' => $dao->entity_id
,
58 $result = civicrm_api3($dao->entity_type
, 'getsingle', $params);
60 catch (Exception
$e) {
61 $this->onApiError($params, $result);
69 public function reconcile() {
70 if ($error = $this->validate($this->declarations
)) {
71 throw new Exception($error);
73 $this->reconcileEnabledModules();
74 $this->reconcileDisabledModules();
75 $this->reconcileUnknownModules();
79 public function reconcileEnabledModules() {
80 // Note: any thing currently declared is necessarily from
81 // an active module -- because we got it from a hook!
83 // index by moduleName,name
84 $decls = self
::createDeclarationIndex($this->moduleIndex
, $this->declarations
);
85 foreach ($decls as $moduleName => $todos) {
86 if (isset($this->moduleIndex
[TRUE][$moduleName])) {
87 $this->reconcileEnabledModule($this->moduleIndex
[TRUE][$moduleName], $todos);
88 } elseif (isset($this->moduleIndex
[FALSE][$moduleName])) {
89 // do nothing -- module should get swept up later
91 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
97 * Create, update, and delete entities declared by an active module
99 * @param \CRM_Core_Module|string $module string
100 * @param $todos array $name => array()
102 public function reconcileEnabledModule(CRM_Core_Module
$module, $todos) {
103 $dao = new CRM_Core_DAO_Managed();
104 $dao->module
= $module->name
;
106 while ($dao->fetch()) {
107 if (isset($todos[$dao->name
]) && $todos[$dao->name
]) {
108 // update existing entity; remove from $todos
109 $this->updateExistingEntity($dao, $todos[$dao->name
]);
110 unset($todos[$dao->name
]);
112 // remove stale entity; not in $todos
113 $this->removeStaleEntity($dao);
117 // create new entities from leftover $todos
118 foreach ($todos as $name => $todo) {
119 $this->insertNewEntity($todo);
123 public function reconcileDisabledModules() {
124 if (empty($this->moduleIndex
[FALSE])) {
128 $in = CRM_Core_DAO
::escapeStrings(array_keys($this->moduleIndex
[FALSE]));
129 $dao = new CRM_Core_DAO_Managed();
130 $dao->whereAdd("module in ($in)");
132 while ($dao->fetch()) {
133 $this->disableEntity($dao);
138 public function reconcileUnknownModules() {
139 $knownModules = array();
140 if (array_key_exists(0, $this->moduleIndex
) && is_array($this->moduleIndex
[0])) {
141 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[0]));
143 if (array_key_exists(1, $this->moduleIndex
) && is_array($this->moduleIndex
[1])) {
144 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex
[1]));
148 $dao = new CRM_Core_DAO_Managed();
149 if (!empty($knownModules)) {
150 $in = CRM_Core_DAO
::escapeStrings($knownModules);
151 $dao->whereAdd("module NOT IN ($in)");
154 while ($dao->fetch()) {
155 $this->removeStaleEntity($dao);
160 * Create a new entity
162 * @param array $todo entity specification (per hook_civicrm_managedEntities)
164 public function insertNewEntity($todo) {
165 $result = civicrm_api($todo['entity'], 'create', $todo['params']);
166 if ($result['is_error']) {
167 $this->onApiError($todo['params'], $result);
170 $dao = new CRM_Core_DAO_Managed();
171 $dao->module
= $todo['module'];
172 $dao->name
= $todo['name'];
173 $dao->entity_type
= $todo['entity'];
174 $dao->entity_id
= $result['id'];
179 * Update an entity which (a) is believed to exist and which (b) ought to be active.
181 * @param CRM_Core_DAO_Managed $dao
182 * @param array $todo entity specification (per hook_civicrm_managedEntities)
184 public function updateExistingEntity($dao, $todo) {
185 $policy = CRM_Utils_Array
::value('update', $todo, 'always');
186 $doUpdate = ($policy == 'always');
190 'id' => $dao->entity_id
,
191 'is_active' => 1, // FIXME: test whether is_active is valid
193 $params = array_merge($defaults, $todo['params']);
194 $result = civicrm_api($dao->entity_type
, 'create', $params);
195 if ($result['is_error']) {
196 $this->onApiError($params, $result);
202 * Update an entity which (a) is believed to exist and which (b) ought to be
205 * @param CRM_Core_DAO_Managed $dao
207 public function disableEntity($dao) {
208 // FIXME: if ($dao->entity_type supports is_active) {
210 // FIXME cascading for payproc types?
213 'id' => $dao->entity_id
,
216 $result = civicrm_api($dao->entity_type
, 'create', $params);
217 if ($result['is_error']) {
218 $this->onApiError($params, $result);
224 * Remove a stale entity (if policy allows)
226 * @param CRM_Core_DAO_Managed $dao
228 public function removeStaleEntity($dao) {
231 'id' => $dao->entity_id
,
233 $result = civicrm_api($dao->entity_type
, 'delete', $params);
234 if ($result['is_error']) {
235 $this->onApiError($params, $result);
238 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
239 1 => array($dao->id
, 'Integer')
246 * @return array indexed by is_active,name
248 protected static function createModuleIndex($modules) {
250 foreach ($modules as $module) {
251 $result[$module->is_active
][$module->name
] = $module;
257 * @param $moduleIndex
258 * @param $declarations
260 * @return array indexed by module,name
262 protected static function createDeclarationIndex($moduleIndex, $declarations) {
264 if (!isset($moduleIndex[TRUE])) {
267 foreach ($moduleIndex[TRUE] as $moduleName => $module) {
268 if ($module->is_active
) {
269 // need an empty array() for all active modules, even if there are no current $declarations
270 $result[$moduleName] = array();
273 foreach ($declarations as $declaration) {
274 $result[$declaration['module']][$declaration['name']] = $declaration;
280 * @param $declarations
282 * @return mixed string on error, or FALSE
284 protected static function validate($declarations) {
285 foreach ($declarations as $declare) {
286 foreach (array('name', 'module', 'entity', 'params') as $key) {
287 if (empty($declare[$key])) {
288 $str = print_r($declare, TRUE);
289 return ("Managed Entity is missing field \"$key\": $str");
292 // FIXME: validate that each 'module' is known
298 * @param $declarations
302 protected static function cleanDeclarations($declarations) {
303 foreach ($declarations as $name => &$declare) {
304 if (!array_key_exists('name', $declare)) {
305 $declare['name'] = $name;
308 return $declarations;
317 protected function onApiError($params, $result) {
318 CRM_Core_Error
::debug_var('ManagedEntities_failed', array(
322 throw new Exception('API error: ' . $result['error_message']);