Merge pull request #16299 from mattwire/scheduledjobs_doc
[civicrm-core.git] / Civi / Core / SettingsBag.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Core;
13
14 /**
15 * Class SettingsBag
16 * @package Civi\Core
17 *
18 * Read and write settings for a given domain (or contact).
19 *
20 * If the target entity does not already have a value for the setting, then
21 * the defaults will be used. If mandatory values are provided, they will
22 * override any defaults or custom settings.
23 *
24 * It's expected that the SettingsBag will have O(50-250) settings -- and that
25 * we'll load the full bag on many page requests. Consequently, we don't
26 * want the full metadata (help text and version history and HTML widgets)
27 * for all 250 settings, but we do need the default values.
28 *
29 * This class is not usually instantiated directly. Instead, use SettingsManager
30 * or Civi::settings().
31 *
32 * @see \Civi::settings()
33 * @see SettingsManagerTest
34 */
35 class SettingsBag {
36
37 protected $domainId;
38
39 protected $contactId;
40
41 /**
42 * @var array
43 * Array(string $settingName => mixed $value).
44 */
45 protected $defaults;
46
47 /**
48 * @var array
49 * Array(string $settingName => mixed $value).
50 */
51 protected $mandatory;
52
53 /**
54 * The result of combining default values, mandatory
55 * values, and user values.
56 *
57 * @var array|null
58 * Array(string $settingName => mixed $value).
59 */
60 protected $combined;
61
62 /**
63 * @var array
64 */
65 protected $values;
66
67 /**
68 * @param int $domainId
69 * The domain for which we want settings.
70 * @param int|null $contactId
71 * The contact for which we want settings. Use NULL for domain settings.
72 */
73 public function __construct($domainId, $contactId) {
74 $this->domainId = $domainId;
75 $this->contactId = $contactId;
76 $this->values = [];
77 $this->combined = NULL;
78 }
79
80 /**
81 * Set/replace the default values.
82 *
83 * @param array $defaults
84 * Array(string $settingName => mixed $value).
85 * @return SettingsBag
86 */
87 public function loadDefaults($defaults) {
88 $this->defaults = $defaults;
89 $this->combined = NULL;
90 return $this;
91 }
92
93 /**
94 * Set/replace the mandatory values.
95 *
96 * @param array $mandatory
97 * Array(string $settingName => mixed $value).
98 * @return SettingsBag
99 */
100 public function loadMandatory($mandatory) {
101 $this->mandatory = $mandatory;
102 $this->combined = NULL;
103 return $this;
104 }
105
106 /**
107 * Load all explicit settings that apply to this domain or contact.
108 *
109 * @return SettingsBag
110 */
111 public function loadValues() {
112 // Note: Don't use DAO child classes. They require fields() which require
113 // translations -- which are keyed off settings!
114
115 $this->values = [];
116 $this->combined = NULL;
117
118 // Ordinarily, we just load values from `civicrm_setting`. But upgrades require care.
119 // In v4.0 and earlier, all values were stored in `civicrm_domain.config_backend`.
120 // In v4.1-v4.6, values were split between `civicrm_domain` and `civicrm_setting`.
121 // In v4.7+, all values are stored in `civicrm_setting`.
122 // Whenever a value is available in civicrm_setting, it will take precedence.
123
124 $isUpgradeMode = \CRM_Core_Config::isUpgradeMode();
125
126 if ($isUpgradeMode && empty($this->contactId) && \CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_domain', 'config_backend', FALSE)) {
127 $config_backend = \CRM_Core_DAO::singleValueQuery('SELECT config_backend FROM civicrm_domain WHERE id = %1',
128 [1 => [$this->domainId, 'Positive']]);
129 $oldSettings = \CRM_Upgrade_Incremental_php_FourSeven::convertBackendToSettings($this->domainId, $config_backend);
130 \CRM_Utils_Array::extend($this->values, $oldSettings);
131 }
132
133 // Normal case. Aside: Short-circuit prevents unnecessary query.
134 if (!$isUpgradeMode || \CRM_Core_DAO::checkTableExists('civicrm_setting')) {
135 $dao = \CRM_Core_DAO::executeQuery($this->createQuery()->toSQL());
136 while ($dao->fetch()) {
137 $this->values[$dao->name] = ($dao->value !== NULL) ? \CRM_Utils_String::unserialize($dao->value) : NULL;
138 }
139 }
140
141 return $this;
142 }
143
144 /**
145 * Add a batch of settings. Save them.
146 *
147 * @param array $settings
148 * Array(string $settingName => mixed $settingValue).
149 * @return SettingsBag
150 */
151 public function add(array $settings) {
152 foreach ($settings as $key => $value) {
153 $this->set($key, $value);
154 }
155 return $this;
156 }
157
158 /**
159 * Get a list of all effective settings.
160 *
161 * @return array
162 * Array(string $settingName => mixed $settingValue).
163 */
164 public function all() {
165 if ($this->combined === NULL) {
166 $this->combined = $this->combine(
167 [$this->defaults, $this->values, $this->mandatory]
168 );
169 }
170 return $this->combined;
171 }
172
173 /**
174 * Determine the effective value.
175 *
176 * @param string $key
177 * @return mixed
178 */
179 public function get($key) {
180 $all = $this->all();
181 return isset($all[$key]) ? $all[$key] : NULL;
182 }
183
184 /**
185 * Determine the default value of a setting.
186 *
187 * @param string $key
188 * The simple name of the setting.
189 * @return mixed|NULL
190 */
191 public function getDefault($key) {
192 return isset($this->defaults[$key]) ? $this->defaults[$key] : NULL;
193 }
194
195 /**
196 * Determine the explicitly designated value, regardless of
197 * any default or mandatory values.
198 *
199 * @param string $key
200 * The simple name of the setting.
201 * @return mixed|NULL
202 */
203 public function getExplicit($key) {
204 return (isset($this->values[$key]) ? $this->values[$key] : NULL);
205 }
206
207 /**
208 * Determine the mandatory value of a setting.
209 *
210 * @param string $key
211 * The simple name of the setting.
212 * @return mixed|NULL
213 */
214 public function getMandatory($key) {
215 return isset($this->mandatory[$key]) ? $this->mandatory[$key] : NULL;
216 }
217
218 /**
219 * Determine if the entity has explicitly designated a value.
220 *
221 * Note that get() may still return other values based on
222 * mandatory values or defaults.
223 *
224 * @param string $key
225 * The simple name of the setting.
226 * @return bool
227 */
228 public function hasExplict($key) {
229 // NULL means no designated value.
230 return isset($this->values[$key]);
231 }
232
233 /**
234 * Removes any explicit settings. This restores the default.
235 *
236 * @param string $key
237 * The simple name of the setting.
238 * @return SettingsBag
239 */
240 public function revert($key) {
241 // It might be better to DELETE (to avoid long-term leaks),
242 // but setting NULL is simpler for now.
243 return $this->set($key, NULL);
244 }
245
246 /**
247 * Add a single setting. Save it.
248 *
249 * @param string $key
250 * The simple name of the setting.
251 * @param mixed $value
252 * The new, explicit value of the setting.
253 * @return SettingsBag
254 */
255 public function set($key, $value) {
256 $this->setDb($key, $value);
257 $this->values[$key] = $value;
258 $this->combined = NULL;
259 return $this;
260 }
261
262 /**
263 * @return \CRM_Utils_SQL_Select
264 */
265 protected function createQuery() {
266 $select = \CRM_Utils_SQL_Select::from('civicrm_setting')
267 ->select('id, name, value, domain_id, contact_id, is_domain, component_id, created_date, created_id')
268 ->where('domain_id = #id', [
269 'id' => $this->domainId,
270 ]);
271 if ($this->contactId === NULL) {
272 $select->where('is_domain = 1');
273 }
274 else {
275 $select->where('contact_id = #id', [
276 'id' => $this->contactId,
277 ]);
278 $select->where('is_domain = 0');
279 }
280 return $select;
281 }
282
283 /**
284 * Combine a series of arrays, excluding any
285 * null values. Later values override earlier
286 * values.
287 *
288 * @param array $arrays
289 * List of arrays to combine.
290 * @return array
291 */
292 protected function combine($arrays) {
293 $combined = [];
294 foreach ($arrays as $array) {
295 foreach ($array as $k => $v) {
296 if ($v !== NULL) {
297 $combined[$k] = $v;
298 }
299 }
300 }
301 return $combined;
302 }
303
304 /**
305 * Update the DB record for this setting.
306 *
307 * @param string $name
308 * The simple name of the setting.
309 * @param mixed $value
310 * The new value of the setting.
311 */
312 protected function setDb($name, $value) {
313 $fields = [];
314 $fieldsToSet = \CRM_Core_BAO_Setting::validateSettingsInput([$name => $value], $fields);
315 //We haven't traditionally validated inputs to setItem, so this breaks things.
316 //foreach ($fieldsToSet as $settingField => &$settingValue) {
317 // self::validateSetting($settingValue, $fields['values'][$settingField]);
318 //}
319
320 $metadata = $fields['values'][$name];
321
322 $dao = new \CRM_Core_DAO_Setting();
323 $dao->name = $name;
324 $dao->domain_id = $this->domainId;
325 if ($this->contactId) {
326 $dao->contact_id = $this->contactId;
327 $dao->is_domain = 0;
328 }
329 else {
330 $dao->is_domain = 1;
331 }
332 $dao->find(TRUE);
333
334 // Call 'on_change' listeners. It would be nice to only fire when there's
335 // a genuine change in the data. However, PHP developers have mixed
336 // expectations about whether 0, '0', '', NULL, and FALSE represent the same
337 // value, so there's no universal way to determine if a change is genuine.
338 if (isset($metadata['on_change'])) {
339 foreach ($metadata['on_change'] as $callback) {
340 call_user_func(
341 \Civi\Core\Resolver::singleton()->get($callback),
342 \CRM_Utils_String::unserialize($dao->value),
343 $value,
344 $metadata,
345 $this->domainId
346 );
347 }
348 }
349
350 if (!is_array($value) && \CRM_Utils_System::isNull($value)) {
351 $dao->value = 'null';
352 }
353 else {
354 $dao->value = serialize($value);
355 }
356
357 if (!isset(\Civi::$statics[__CLASS__]['upgradeMode'])) {
358 \Civi::$statics[__CLASS__]['upgradeMode'] = \CRM_Core_Config::isUpgradeMode();
359 }
360 if (\Civi::$statics[__CLASS__]['upgradeMode'] && \CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_setting', 'group_name')) {
361 $dao->group_name = 'placeholder';
362 }
363
364 $dao->created_date = \CRM_Utils_Time::getTime('YmdHis');
365
366 $session = \CRM_Core_Session::singleton();
367 if (\CRM_Contact_BAO_Contact_Utils::isContactId($session->get('userID'))) {
368 $dao->created_id = $session->get('userID');
369 }
370
371 if ($dao->id) {
372 $dao->save();
373 }
374 else {
375 // Cannot use $dao->save(); in upgrade mode (eg WP + Civi 4.4=>4.7), the DAO will refuse
376 // to save the field `group_name`, which is required in older schema.
377 \CRM_Core_DAO::executeQuery(\CRM_Utils_SQL_Insert::dao($dao)->toSQL());
378 }
379 }
380
381 }