Merge pull request #17892 from demeritcowboy/tests-outputreport
[civicrm-core.git] / Civi / Core / SettingsBag.php
CommitLineData
3a84c0ab
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
41498ac5 4 | Copyright CiviCRM LLC. All rights reserved. |
3a84c0ab 5 | |
41498ac5
TO
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 |
3a84c0ab
TO
9 +--------------------------------------------------------------------+
10 */
11
12namespace 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 */
35class 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 *
cc101011 57 * @var array|null
3a84c0ab
TO
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.
e97c66ff 70 * @param int|null $contactId
3a84c0ab 71 * The contact for which we want settings. Use NULL for domain settings.
5dbaf8de
TO
72 */
73 public function __construct($domainId, $contactId) {
74 $this->domainId = $domainId;
75 $this->contactId = $contactId;
c64f69d9 76 $this->values = [];
5dbaf8de
TO
77 $this->combined = NULL;
78 }
79
80 /**
81 * Set/replace the default values.
82 *
3a84c0ab
TO
83 * @param array $defaults
84 * Array(string $settingName => mixed $value).
4b350175 85 * @return SettingsBag
5dbaf8de
TO
86 */
87 public function loadDefaults($defaults) {
88 $this->defaults = $defaults;
5dbaf8de
TO
89 $this->combined = NULL;
90 return $this;
91 }
92
93 /**
94 * Set/replace the mandatory values.
95 *
3a84c0ab
TO
96 * @param array $mandatory
97 * Array(string $settingName => mixed $value).
4b350175 98 * @return SettingsBag
3a84c0ab 99 */
5dbaf8de 100 public function loadMandatory($mandatory) {
3a84c0ab
TO
101 $this->mandatory = $mandatory;
102 $this->combined = NULL;
5dbaf8de 103 return $this;
3a84c0ab
TO
104 }
105
106 /**
5dbaf8de 107 * Load all explicit settings that apply to this domain or contact.
3a84c0ab 108 *
4b350175 109 * @return SettingsBag
3a84c0ab 110 */
5dbaf8de 111 public function loadValues() {
f806379b 112 // Note: Don't use DAO child classes. They require fields() which require
5dbaf8de 113 // translations -- which are keyed off settings!
f806379b 114
c64f69d9 115 $this->values = [];
3a84c0ab 116 $this->combined = NULL;
f806379b
TO
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
eed7e803 126 if ($isUpgradeMode && empty($this->contactId) && \CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_domain', 'config_backend', FALSE)) {
f806379b 127 $config_backend = \CRM_Core_DAO::singleValueQuery('SELECT config_backend FROM civicrm_domain WHERE id = %1',
c64f69d9 128 [1 => [$this->domainId, 'Positive']]);
f806379b
TO
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()) {
f24846d5 137 $this->values[$dao->name] = ($dao->value !== NULL) ? \CRM_Utils_String::unserialize($dao->value) : NULL;
f806379b
TO
138 }
139 }
140
3a84c0ab
TO
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).
4b350175 149 * @return SettingsBag
3a84c0ab
TO
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(
c64f69d9 167 [$this->defaults, $this->values, $this->mandatory]
3a84c0ab 168 );
8f2a141a
TO
169 // computeVirtual() depends on completion of preceding pass.
170 $this->combined = $this->combine(
171 [$this->combined, $this->computeVirtual()]
172 );
3a84c0ab
TO
173 }
174 return $this->combined;
175 }
176
177 /**
178 * Determine the effective value.
179 *
180 * @param string $key
181 * @return mixed
182 */
183 public function get($key) {
184 $all = $this->all();
2e1f50d6 185 return $all[$key] ?? NULL;
3a84c0ab
TO
186 }
187
1a6ba7d4
TO
188 /**
189 * Determine the default value of a setting.
190 *
191 * @param string $key
192 * The simple name of the setting.
193 * @return mixed|NULL
194 */
195 public function getDefault($key) {
2e1f50d6 196 return $this->defaults[$key] ?? NULL;
1a6ba7d4
TO
197 }
198
3a84c0ab
TO
199 /**
200 * Determine the explicitly designated value, regardless of
201 * any default or mandatory values.
202 *
203 * @param string $key
1a6ba7d4
TO
204 * The simple name of the setting.
205 * @return mixed|NULL
3a84c0ab
TO
206 */
207 public function getExplicit($key) {
2e1f50d6 208 return ($this->values[$key] ?? NULL);
3a84c0ab
TO
209 }
210
1a6ba7d4
TO
211 /**
212 * Determine the mandatory value of a setting.
213 *
214 * @param string $key
215 * The simple name of the setting.
216 * @return mixed|NULL
217 */
218 public function getMandatory($key) {
2e1f50d6 219 return $this->mandatory[$key] ?? NULL;
1a6ba7d4
TO
220 }
221
3a84c0ab
TO
222 /**
223 * Determine if the entity has explicitly designated a value.
224 *
225 * Note that get() may still return other values based on
226 * mandatory values or defaults.
227 *
228 * @param string $key
1a6ba7d4 229 * The simple name of the setting.
3a84c0ab
TO
230 * @return bool
231 */
232 public function hasExplict($key) {
233 // NULL means no designated value.
234 return isset($this->values[$key]);
235 }
236
237 /**
238 * Removes any explicit settings. This restores the default.
239 *
240 * @param string $key
1a6ba7d4 241 * The simple name of the setting.
4b350175 242 * @return SettingsBag
3a84c0ab
TO
243 */
244 public function revert($key) {
245 // It might be better to DELETE (to avoid long-term leaks),
246 // but setting NULL is simpler for now.
247 return $this->set($key, NULL);
248 }
249
250 /**
251 * Add a single setting. Save it.
252 *
253 * @param string $key
1a6ba7d4 254 * The simple name of the setting.
3a84c0ab 255 * @param mixed $value
1a6ba7d4 256 * The new, explicit value of the setting.
4b350175 257 * @return SettingsBag
3a84c0ab
TO
258 */
259 public function set($key, $value) {
8f2a141a 260 if ($this->updateVirtual($key, $value)) {
ff6f993e 261 return $this;
262 }
3a84c0ab
TO
263 $this->setDb($key, $value);
264 $this->values[$key] = $value;
265 $this->combined = NULL;
266 return $this;
267 }
268
ff6f993e 269 /**
8f2a141a
TO
270 * Update a virtualized/deprecated setting.
271 *
ff6f993e 272 * Temporary handling for phasing out contribution_invoice_settings.
273 *
274 * Until we have transitioned we need to handle setting & retrieving
275 * contribution_invoice_settings.
276 *
277 * Once removed from core we will add deprecation notices & then remove this.
278 *
279 * https://lab.civicrm.org/dev/core/issues/1558
280 *
8f2a141a 281 * @param string $key
ff6f993e 282 * @param array $value
8f2a141a
TO
283 * @return bool
284 * TRUE if $key is a virtualized setting. FALSE if it is a normal setting.
ff6f993e 285 */
8f2a141a
TO
286 public function updateVirtual($key, $value) {
287 if ($key === 'contribution_invoice_settings') {
288 foreach (SettingsBag::getContributionInvoiceSettingKeys() as $possibleKeyName => $settingName) {
289 $keyValue = $value[$possibleKeyName] ?? '';
84d52986
TO
290 if ($possibleKeyName === 'invoicing' && is_array($keyValue)) {
291 $keyValue = $keyValue['invoicing'];
292 }
8f2a141a
TO
293 $this->set($settingName, $keyValue);
294 }
295 return TRUE;
ff6f993e 296 }
8f2a141a 297 return FALSE;
ff6f993e 298 }
299
300 /**
8f2a141a 301 * Determine the values of any virtual/computed settings.
ff6f993e 302 *
303 * @return array
304 */
8f2a141a 305 public function computeVirtual() {
ff6f993e 306 $contributionSettings = [];
307 foreach (SettingsBag::getContributionInvoiceSettingKeys() as $keyName => $settingName) {
84d52986
TO
308 switch ($keyName) {
309 case 'invoicing':
310 $contributionSettings[$keyName] = $this->get($settingName) ? [$keyName => 1] : 0;
311 break;
312
313 default:
314 $contributionSettings[$keyName] = $this->get($settingName);
315 break;
316 }
ff6f993e 317 }
8f2a141a 318 return ['contribution_invoice_settings' => $contributionSettings];
ff6f993e 319 }
320
3a84c0ab 321 /**
5dbaf8de 322 * @return \CRM_Utils_SQL_Select
3a84c0ab 323 */
5dbaf8de
TO
324 protected function createQuery() {
325 $select = \CRM_Utils_SQL_Select::from('civicrm_setting')
f806379b 326 ->select('id, name, value, domain_id, contact_id, is_domain, component_id, created_date, created_id')
c64f69d9 327 ->where('domain_id = #id', [
5dbaf8de 328 'id' => $this->domainId,
c64f69d9 329 ]);
3a84c0ab 330 if ($this->contactId === NULL) {
5dbaf8de 331 $select->where('is_domain = 1');
3a84c0ab
TO
332 }
333 else {
c64f69d9 334 $select->where('contact_id = #id', [
5dbaf8de 335 'id' => $this->contactId,
c64f69d9 336 ]);
5dbaf8de 337 $select->where('is_domain = 0');
3a84c0ab 338 }
5dbaf8de 339 return $select;
3a84c0ab
TO
340 }
341
342 /**
343 * Combine a series of arrays, excluding any
344 * null values. Later values override earlier
345 * values.
346 *
1a6ba7d4
TO
347 * @param array $arrays
348 * List of arrays to combine.
3a84c0ab
TO
349 * @return array
350 */
351 protected function combine($arrays) {
c64f69d9 352 $combined = [];
3a84c0ab
TO
353 foreach ($arrays as $array) {
354 foreach ($array as $k => $v) {
355 if ($v !== NULL) {
356 $combined[$k] = $v;
357 }
358 }
359 }
360 return $combined;
361 }
362
363 /**
055f6c27
TO
364 * Update the DB record for this setting.
365 *
1a6ba7d4
TO
366 * @param string $name
367 * The simple name of the setting.
368 * @param mixed $value
369 * The new value of the setting.
3a84c0ab
TO
370 */
371 protected function setDb($name, $value) {
c64f69d9
CW
372 $fields = [];
373 $fieldsToSet = \CRM_Core_BAO_Setting::validateSettingsInput([$name => $value], $fields);
3a84c0ab
TO
374 //We haven't traditionally validated inputs to setItem, so this breaks things.
375 //foreach ($fieldsToSet as $settingField => &$settingValue) {
376 // self::validateSetting($settingValue, $fields['values'][$settingField]);
377 //}
055f6c27
TO
378
379 $metadata = $fields['values'][$name];
380
381 $dao = new \CRM_Core_DAO_Setting();
382 $dao->name = $name;
383 $dao->domain_id = $this->domainId;
384 if ($this->contactId) {
385 $dao->contact_id = $this->contactId;
386 $dao->is_domain = 0;
387 }
388 else {
389 $dao->is_domain = 1;
390 }
391 $dao->find(TRUE);
055f6c27 392
38e5457e
TO
393 // Call 'on_change' listeners. It would be nice to only fire when there's
394 // a genuine change in the data. However, PHP developers have mixed
395 // expectations about whether 0, '0', '', NULL, and FALSE represent the same
396 // value, so there's no universal way to determine if a change is genuine.
397 if (isset($metadata['on_change'])) {
055f6c27
TO
398 foreach ($metadata['on_change'] as $callback) {
399 call_user_func(
400 \Civi\Core\Resolver::singleton()->get($callback),
f24846d5 401 \CRM_Utils_String::unserialize($dao->value),
055f6c27
TO
402 $value,
403 $metadata,
404 $this->domainId
405 );
406 }
407 }
408
bf322c68 409 if (!is_array($value) && \CRM_Utils_System::isNull($value)) {
055f6c27
TO
410 $dao->value = 'null';
411 }
412 else {
413 $dao->value = serialize($value);
414 }
415
8ff759c4
C
416 if (!isset(\Civi::$statics[__CLASS__]['upgradeMode'])) {
417 \Civi::$statics[__CLASS__]['upgradeMode'] = \CRM_Core_Config::isUpgradeMode();
418 }
eed7e803 419 if (\Civi::$statics[__CLASS__]['upgradeMode'] && \CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_setting', 'group_name')) {
8ff759c4
C
420 $dao->group_name = 'placeholder';
421 }
422
9ece35cc 423 $dao->created_date = \CRM_Utils_Time::getTime('YmdHis');
055f6c27
TO
424
425 $session = \CRM_Core_Session::singleton();
426 if (\CRM_Contact_BAO_Contact_Utils::isContactId($session->get('userID'))) {
427 $dao->created_id = $session->get('userID');
428 }
429
867a532b
TO
430 if ($dao->id) {
431 $dao->save();
432 }
433 else {
434 // Cannot use $dao->save(); in upgrade mode (eg WP + Civi 4.4=>4.7), the DAO will refuse
435 // to save the field `group_name`, which is required in older schema.
436 \CRM_Core_DAO::executeQuery(\CRM_Utils_SQL_Insert::dao($dao)->toSQL());
437 }
3a84c0ab
TO
438 }
439
ff6f993e 440 /**
441 * @return array
442 */
443 public static function getContributionInvoiceSettingKeys(): array {
444 $convertedKeys = [
445 'credit_notes_prefix' => 'credit_notes_prefix',
446 'invoice_prefix' => 'invoice_prefix',
447 'due_date' => 'invoice_due_date',
448 'due_date_period' => 'invoice_due_date_period',
449 'notes' => 'invoice_notes',
450 'is_email_pdf' => 'invoice_is_email_pdf',
451 'tax_term' => 'tax_term',
452 'tax_display_settings' => 'tax_display_settings',
453 'invoicing' => 'invoicing',
454 ];
455 return $convertedKeys;
456 }
457
3a84c0ab 458}