Merge pull request #3004 from sgladstone/master
[civicrm-core.git] / CRM / Core / ManagedEntities.php
CommitLineData
6a488035
TO
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 */
8class 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();
3bd831aa
TO
26 foreach (CRM_Core_Component::getEnabledComponents() as $component) {
27 $declarations = array_merge($declarations, $component->getManagedEntities());
28 }
6a488035
TO
29 CRM_Utils_Hook::managed($declarations);
30 $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module::getAll(), $declarations);
31 }
32 return $singleton;
33 }
34
35 /**
36 * @param $modules array CRM_Core_Module
37 * @param $declarations array per hook_civicrm_managed
38 */
39 public function __construct($modules, $declarations) {
40 $this->moduleIndex = self::createModuleIndex($modules);
41 $this->declarations = self::cleanDeclarations($declarations);
42 }
43
44 /**
45 * Read the managed entity
46 */
47 public function get($moduleName, $name) {
48 $dao = new CRM_Core_DAO_Managed();
49 $dao->module = $moduleName;
50 $dao->name = $name;
51 if ($dao->find(TRUE)) {
52 $params = array(
6a488035
TO
53 'id' => $dao->entity_id,
54 );
637ea2cf
E
55 try {
56 $result = civicrm_api3($dao->entity_type, 'getsingle', $params);
57 }
58 catch (Exception $e) {
6a488035 59 $this->onApiError($params, $result);
6a488035 60 }
637ea2cf 61 return $result;
6a488035
TO
62 } else {
63 return NULL;
64 }
65 }
66
67 public function reconcile() {
68 if ($error = $this->validate($this->declarations)) {
69 throw new Exception($error);
70 }
71 $this->reconcileEnabledModules();
72 $this->reconcileDisabledModules();
73 $this->reconcileUnknownModules();
74 }
75
76
77 public function reconcileEnabledModules() {
78 // Note: any thing currently declared is necessarily from
79 // an active module -- because we got it from a hook!
80
81 // index by moduleName,name
82 $decls = self::createDeclarationIndex($this->moduleIndex, $this->declarations);
83 foreach ($decls as $moduleName => $todos) {
84 if (isset($this->moduleIndex[TRUE][$moduleName])) {
85 $this->reconcileEnabledModule($this->moduleIndex[TRUE][$moduleName], $todos);
86 } elseif (isset($this->moduleIndex[FALSE][$moduleName])) {
87 // do nothing -- module should get swept up later
88 } else {
89 throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
90 }
91 }
92 }
93
94 /**
95 * Create, update, and delete entities declared by an active module
96 *
97 * @param $module string
98 * @param $todos array $name => array()
99 */
100 public function reconcileEnabledModule(CRM_Core_Module $module, $todos) {
101 $dao = new CRM_Core_DAO_Managed();
102 $dao->module = $module->name;
103 $dao->find();
104 while ($dao->fetch()) {
3bd831aa 105 if (isset($todos[$dao->name]) && $todos[$dao->name]) {
6a488035
TO
106 // update existing entity; remove from $todos
107 $defaults = array('id' => $dao->entity_id, 'is_active' => 1); // FIXME: test whether is_active is valid
108 $params = array_merge($defaults, $todos[$dao->name]['params']);
109 $result = civicrm_api($dao->entity_type, 'create', $params);
110 if ($result['is_error']) {
111 $this->onApiError($params, $result);
112 }
113
114 unset($todos[$dao->name]);
115 } else {
116 // remove stale entity; not in $todos
117 $params = array(
118 'version' => 3,
119 'id' => $dao->entity_id,
120 );
121 $result = civicrm_api($dao->entity_type, 'delete', $params);
122 if ($result['is_error']) {
123 $this->onApiError($params, $result);
124 }
125
126 CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
127 1 => array($dao->id, 'Integer')
128 ));
129 }
130 }
131
132 // create new entities from leftover $todos
133 foreach ($todos as $name => $todo) {
134 $result = civicrm_api($todo['entity'], 'create', $todo['params']);
135 if ($result['is_error']) {
136 $this->onApiError($todo['params'], $result);
137 }
138
139 $dao = new CRM_Core_DAO_Managed();
140 $dao->module = $todo['module'];
141 $dao->name = $todo['name'];
142 $dao->entity_type = $todo['entity'];
143 $dao->entity_id = $result['id'];
144 $dao->save();
145 }
146 }
147
148 public function reconcileDisabledModules() {
149 if (empty($this->moduleIndex[FALSE])) {
150 return;
151 }
152
153 $in = CRM_Core_DAO::escapeStrings(array_keys($this->moduleIndex[FALSE]));
154 $dao = new CRM_Core_DAO_Managed();
155 $dao->whereAdd("module in ($in)");
156 $dao->find();
157 while ($dao->fetch()) {
158 // FIXME: if ($dao->entity_type supports is_active) {
159 if (TRUE) {
160 // FIXME cascading for payproc types?
161 $params = array(
162 'version' => 3,
163 'id' => $dao->entity_id,
164 'is_active' => 0,
165 );
166 $result = civicrm_api($dao->entity_type, 'create', $params);
167 if ($result['is_error']) {
168 $this->onApiError($params, $result);
169 }
170 }
171 }
172 }
173
174 public function reconcileUnknownModules() {
175 $knownModules = array();
176 if (array_key_exists(0, $this->moduleIndex) && is_array($this->moduleIndex[0])) {
177 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex[0]));
178 }
179 if (array_key_exists(1, $this->moduleIndex) && is_array($this->moduleIndex[1])) {
180 $knownModules = array_merge($knownModules, array_keys($this->moduleIndex[1]));
181
182 }
183
184 $dao = new CRM_Core_DAO_Managed();
185 if (!empty($knownModules)) {
186 $in = CRM_Core_DAO::escapeStrings($knownModules);
187 $dao->whereAdd("module NOT IN ($in)");
188 }
189 $dao->find();
190 while ($dao->fetch()) {
191 $params = array(
192 'version' => 3,
193 'id' => $dao->entity_id,
194 );
195 $result = civicrm_api($dao->entity_type, 'delete', $params);
196 if ($result['is_error']) {
197 $this->onApiError($params, $result);
198 }
199
200 CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
201 1 => array($dao->id, 'Integer')
202 ));
203 }
204 }
205
206 /**
207 * @return array indexed by is_active,name
208 */
209 protected static function createModuleIndex($modules) {
210 $result = array();
211 foreach ($modules as $module) {
212 $result[$module->is_active][$module->name] = $module;
213 }
214 return $result;
215 }
216
217 /**
218 * @return array indexed by module,name
219 */
220 protected static function createDeclarationIndex($moduleIndex, $declarations) {
221 $result = array();
222 if (!isset($moduleIndex[TRUE])) {
223 return $result;
224 }
225 foreach ($moduleIndex[TRUE] as $moduleName => $module) {
226 if ($module->is_active) {
227 // need an empty array() for all active modules, even if there are no current $declarations
228 $result[$moduleName] = array();
229 }
230 }
231 foreach ($declarations as $declaration) {
232 $result[$declaration['module']][$declaration['name']] = $declaration;
233 }
234 return $result;
235 }
236
237 /**
238 * @return mixed string on error, or FALSE
239 */
240 protected static function validate($declarations) {
241 foreach ($declarations as $declare) {
242 foreach (array('name', 'module', 'entity', 'params') as $key) {
243 if (empty($declare[$key])) {
244 $str = print_r($declare, TRUE);
245 return ("Managed Entity is missing field \"$key\": $str");
246 }
247 }
248 // FIXME: validate that each 'module' is known
249 }
250 return FALSE;
251 }
252
253 protected static function cleanDeclarations($declarations) {
254 foreach ($declarations as $name => &$declare) {
255 if (!array_key_exists('name', $declare)) {
256 $declare['name'] = $name;
257 }
258 }
259 return $declarations;
260 }
261
262 protected function onApiError($params, $result) {
263 CRM_Core_Error::debug_var('ManagedEntities_failed', array(
264 'params' => $params,
265 'result' => $result,
266 ));
267 throw new Exception('API error: ' . $result['error_message']);
cbb7c7e0 268 }
6a488035
TO
269}
270