CRM-14478 - ManagedEntities - Add support for 'update' policy ('always' or 'never')
[civicrm-core.git] / CRM / Core / ManagedEntities.php
1 <?php
2
3 /**
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.
7 */
8 class CRM_Core_ManagedEntities {
9 /**
10 * @var array($status => array($name => CRM_Core_Module))
11 */
12 public $moduleIndex;
13
14 /**
15 * @var array per hook_civicrm_managed
16 */
17 public $declarations;
18
19 /**
20 * Get an instance
21 */
22 public static function singleton($fresh = FALSE) {
23 static $singleton;
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());
29 }
30 CRM_Utils_Hook::managed($declarations);
31 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module::getAll(), $declarations);
32 }
33 return $singleton;
34 }
35
36 /**
37 * @param array $modules CRM_Core_Module
38 * @param array $declarations per hook_civicrm_managed
39 */
40 public function __construct($modules, $declarations) {
41 $this->moduleIndex = self::createModuleIndex($modules);
42 $this->declarations = self::cleanDeclarations($declarations);
43 }
44
45 /**
46 * Read the managed entity
47 */
48 public function get($moduleName, $name) {
49 $dao = new CRM_Core_DAO_Managed();
50 $dao->module = $moduleName;
51 $dao->name = $name;
52 if ($dao->find(TRUE)) {
53 $params = array(
54 'id' => $dao->entity_id,
55 );
56 $result = NULL;
57 try {
58 $result = civicrm_api3($dao->entity_type, 'getsingle', $params);
59 }
60 catch (Exception $e) {
61 $this->onApiError($params, $result);
62 }
63 return $result;
64 } else {
65 return NULL;
66 }
67 }
68
69 public function reconcile() {
70 if ($error = $this->validate($this->declarations)) {
71 throw new Exception($error);
72 }
73 $this->reconcileEnabledModules();
74 $this->reconcileDisabledModules();
75 $this->reconcileUnknownModules();
76 }
77
78
79 public function reconcileEnabledModules() {
80 // Note: any thing currently declared is necessarily from
81 // an active module -- because we got it from a hook!
82
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
90 } else {
91 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
92 }
93 }
94 }
95
96 /**
97 * Create, update, and delete entities declared by an active module
98 *
99 * @param \CRM_Core_Module|string $module string
100 * @param $todos array $name => array()
101 */
102 public function reconcileEnabledModule(CRM_Core_Module $module, $todos) {
103 $dao = new CRM_Core_DAO_Managed();
104 $dao->module = $module->name;
105 $dao->find();
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]);
111 } else {
112 // remove stale entity; not in $todos
113 $this->removeStaleEntity($dao);
114 }
115 }
116
117 // create new entities from leftover $todos
118 foreach ($todos as $name => $todo) {
119 $this->insertNewEntity($todo);
120 }
121 }
122
123 public function reconcileDisabledModules() {
124 if (empty($this->moduleIndex[FALSE])) {
125 return;
126 }
127
128 $in = CRM_Core_DAO::escapeStrings(array_keys($this->moduleIndex[FALSE]));
129 $dao = new CRM_Core_DAO_Managed();
130 $dao->whereAdd("module in ($in)");
131 $dao->find();
132 while ($dao->fetch()) {
133 $this->disableEntity($dao);
134
135 }
136 }
137
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]));
142 }
143 if (array_key_exists(1, $this->moduleIndex) && is_array($this->moduleIndex[1])) {
144 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex[1]));
145
146 }
147
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)");
152 }
153 $dao->find();
154 while ($dao->fetch()) {
155 $this->removeStaleEntity($dao);
156 }
157 }
158
159 /**
160 * Create a new entity
161 *
162 * @param array $todo entity specification (per hook_civicrm_managedEntities)
163 */
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);
168 }
169
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'];
175 $dao->save();
176 }
177
178 /**
179 * Update an entity which (a) is believed to exist and which (b) ought to be active.
180 *
181 * @param CRM_Core_DAO_Managed $dao
182 * @param array $todo entity specification (per hook_civicrm_managedEntities)
183 */
184 public function updateExistingEntity($dao, $todo) {
185 $policy = CRM_Utils_Array::value('update', $todo, 'always');
186 $doUpdate = ($policy == 'always');
187
188 if ($doUpdate) {
189 $defaults = array(
190 'id' => $dao->entity_id,
191 'is_active' => 1, // FIXME: test whether is_active is valid
192 );
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);
197 }
198 }
199 }
200
201 /**
202 * Update an entity which (a) is believed to exist and which (b) ought to be
203 * inactive.
204 *
205 * @param CRM_Core_DAO_Managed $dao
206 */
207 public function disableEntity($dao) {
208 // FIXME: if ($dao->entity_type supports is_active) {
209 if (TRUE) {
210 // FIXME cascading for payproc types?
211 $params = array(
212 'version' => 3,
213 'id' => $dao->entity_id,
214 'is_active' => 0,
215 );
216 $result = civicrm_api($dao->entity_type, 'create', $params);
217 if ($result['is_error']) {
218 $this->onApiError($params, $result);
219 }
220 }
221 }
222
223 /**
224 * Remove a stale entity (if policy allows)
225 *
226 * @param CRM_Core_DAO_Managed $dao
227 */
228 public function removeStaleEntity($dao) {
229 $params = array(
230 'version' => 3,
231 'id' => $dao->entity_id,
232 );
233 $result = civicrm_api($dao->entity_type, 'delete', $params);
234 if ($result['is_error']) {
235 $this->onApiError($params, $result);
236 }
237
238 CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
239 1 => array($dao->id, 'Integer')
240 ));
241 }
242
243 /**
244 * @param $modules
245 *
246 * @return array indexed by is_active,name
247 */
248 protected static function createModuleIndex($modules) {
249 $result = array();
250 foreach ($modules as $module) {
251 $result[$module->is_active][$module->name] = $module;
252 }
253 return $result;
254 }
255
256 /**
257 * @param $moduleIndex
258 * @param $declarations
259 *
260 * @return array indexed by module,name
261 */
262 protected static function createDeclarationIndex($moduleIndex, $declarations) {
263 $result = array();
264 if (!isset($moduleIndex[TRUE])) {
265 return $result;
266 }
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();
271 }
272 }
273 foreach ($declarations as $declaration) {
274 $result[$declaration['module']][$declaration['name']] = $declaration;
275 }
276 return $result;
277 }
278
279 /**
280 * @param $declarations
281 *
282 * @return mixed string on error, or FALSE
283 */
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");
290 }
291 }
292 // FIXME: validate that each 'module' is known
293 }
294 return FALSE;
295 }
296
297 /**
298 * @param $declarations
299 *
300 * @return mixed
301 */
302 protected static function cleanDeclarations($declarations) {
303 foreach ($declarations as $name => &$declare) {
304 if (!array_key_exists('name', $declare)) {
305 $declare['name'] = $name;
306 }
307 }
308 return $declarations;
309 }
310
311 /**
312 * @param $params
313 * @param $result
314 *
315 * @throws Exception
316 */
317 protected function onApiError($params, $result) {
318 CRM_Core_Error::debug_var('ManagedEntities_failed', array(
319 'params' => $params,
320 'result' => $result,
321 ));
322 throw new Exception('API error: ' . $result['error_message']);
323 }
324 }
325