1: <?php
2: /**
3: * This file contains the implementation of the interface ISettingsManager
4: * in the class ConfigSettingsManager for management of the
5: * config settings.
6: *
7: * @package System
8: *
9: * @author Oleg Schildt
10: */
11:
12: namespace SmartFactory;
13:
14: use \SmartFactory\Interfaces\ISettingsManager;
15: use SmartFactory\Interfaces\ISettingsValidator;
16:
17: /**
18: * Class for management of the config settings stored in a config JSON file.
19: *
20: * This settings manager supports the multistep wizard like processing. That means
21: * that you do not have to put all settings in one big form and save them in one action.
22: * You can collect them over multiple steps (requests), go forward ad back, validate them
23: * on each step and finally save them after the final step.
24: *
25: * You do not need any preliminary special settings name definitions. When you introduce
26: * a new setting, just start saving and getting it.
27: *
28: * @see RuntimeSettingsManager
29: * @see UserSettingsManager
30: *
31: * @author Oleg Schildt
32: */
33: class ConfigSettingsManager implements ISettingsManager
34: {
35: /**
36: * Internal variable that holds the target file path for storing the settings data.
37: *
38: * @var ?string
39: *
40: * @author Oleg Schildt
41: */
42: protected ?string $save_path = null;
43:
44: /**
45: * Internal variable for storing the state whether the data should be encrypted
46: * before saving.
47: *
48: * @var bool
49: *
50: * @author Oleg Schildt
51: */
52: protected bool $save_encrypted = false;
53:
54: /**
55: * Internal variable for storing the salt key if the data should be encrypted
56: * before saving.
57: *
58: * @var string
59: *
60: * @author Oleg Schildt
61: */
62: protected string $salt_key = "default";
63:
64: /**
65: * Internal variable for storing the state whether the config file must exist.
66: *
67: * @var bool
68: *
69: * @author Oleg Schildt
70: */
71: protected bool $config_file_must_exist = false;
72:
73: /**
74: * Internal variable for storing the state whether the APCU should be used.
75: *
76: * @var bool
77: *
78: * @author Oleg Schildt
79: */
80: protected bool $use_apcu = false;
81:
82: /**
83: * Internal variable for storing the current context.
84: *
85: * @var string
86: *
87: * @see ConfigSettingsManager::getContext()
88: * @see ConfigSettingsManager::setContext()
89: *
90: * @author Oleg Schildt
91: */
92: protected string $context = "default";
93:
94: /**
95: * Internal variable for storing the validator.
96: *
97: * @var ?\SmartFactory\Interfaces\ISettingsValidator
98: *
99: * @see ConfigSettingsManager::getValidator()
100: * @see ConfigSettingsManager::setValidator()
101: *
102: * @author Oleg Schildt
103: */
104: protected ?\SmartFactory\Interfaces\ISettingsValidator $validator = null;
105:
106: /**
107: * Internal variable for storing the array of the settings values.
108: *
109: * @var array
110: *
111: * @author Oleg Schildt
112: */
113: protected array $settings = [];
114:
115: /**
116: * This is internal auxiliary function for converting the settings to JSON and storing it
117: * to the target file defined by the initialization.
118: *
119: * @param array &$data
120: * The array with the settings values to be saved.
121: *
122: * @return void
123: *
124: * @throws \Exception
125: * It might throw an exception in the case of any errors:
126: *
127: * - if the save path is not specified.
128: * - if the config file is not writable.
129: *
130: * @see ConfigSettingsManager::loadJSON()
131: *
132: * @author Oleg Schildt
133: */
134: protected function saveJSON(array &$data): void
135: {
136: $this->validateParameters();
137:
138: $json = array_to_json($data);
139:
140: if (!empty($this->save_encrypted)) {
141: $json = aes_256_encrypt($json, $this->salt_key);
142: }
143:
144: $semaphore_path = $this->save_path . ".lock";
145:
146: if (!is_writable(file_exists($this->save_path) ? $this->save_path : dirname($this->save_path))) {
147: throw new \Exception(sprintf("The config file '%s' ot its directory is not accesible for writing!", $this->save_path));
148: }
149:
150: $fp = fopen($semaphore_path, "w");
151: if (flock($fp, LOCK_EX)) {
152: if (file_put_contents($this->save_path, $json) === false) {
153: flock($fp, LOCK_UN);
154: fclose($fp);
155:
156: throw new \Exception(sprintf("Writing of the config file '%s' failed!", $this->save_path));
157: }
158: flock($fp, LOCK_UN);
159: }
160:
161: fclose($fp);
162:
163: if ($this->use_apcu) {
164: apcu_delete("config_settings");
165: }
166: } // saveJSON
167:
168: /**
169: * This is internal auxiliary function for loading the settings from the target file
170: * defined by the initialization.
171: *
172: * @param array &$data
173: * The target array with the settings values to be loaded.
174: *
175: * @return void
176: *
177: * @throws \Exception
178: * It might throw an exception in the case of any errors:
179: *
180: * - if the save path is not specified.
181: * - if the config file is not writable.
182: * - if the config file is not readable.
183: * - if the config file is invalid.
184: *
185: * @see ConfigSettingsManager::saveJSON()
186: *
187: * @author Oleg Schildt
188: */
189: protected function loadJSON(array &$data): void
190: {
191: if ($this->use_apcu && apcu_exists("config_settings")) {
192: $data = apcu_fetch("config_settings");
193: if (!empty($data)) {
194: return;
195: }
196: }
197:
198: $this->validateParameters();
199:
200: if (!file_exists($this->save_path) && empty($this->config_file_must_exist)) {
201: return;
202: }
203:
204: if (!file_exists($this->save_path) || !is_readable($this->save_path)) {
205: throw new \Exception(sprintf("The config file '%s' is not accesible for reading or does not exist!", $this->save_path));
206: }
207:
208: $json = null;
209:
210: $semaphore_path = $this->save_path . ".lock";
211:
212: $fp = fopen($semaphore_path, "w");
213: if (flock($fp, LOCK_EX)) {
214: $json = file_get_contents($this->save_path);
215: flock($fp, LOCK_UN);
216: }
217: fclose($fp);
218:
219: if (file_exists($semaphore_path)) {
220: unlink($semaphore_path);
221: }
222:
223: if (empty($json)) {
224: throw new \Exception(sprintf("Reading of the config file '%s' failed!", $this->save_path));
225: }
226:
227: if (!empty($this->save_encrypted)) {
228: $json = aes_256_decrypt($json, $this->salt_key);
229: }
230:
231: try {
232: json_to_array($json, $data);
233: } catch (\Throwable $ex) {
234: throw new \Exception("JSON parse error: " . $ex->getMessage());
235: }
236:
237: if ($this->use_apcu) {
238: apcu_store("config_settings", $data);
239: }
240: } // loadJSON
241:
242: /**
243: * This is internal auxiliary function for checking that the settings
244: * manager is initialized correctly.
245: *
246: * @return void
247: *
248: * @throws \Exception
249: * It might throw an exception in the case of any errors:
250: *
251: * - if the save path is not specified.
252: *
253: * @author Oleg Schildt
254: */
255: protected function validateParameters(): void
256: {
257: if (empty($this->save_path)) {
258: throw new \Exception("The 'save_path' is not specified!");
259: }
260: } // validateParameters
261:
262: /**
263: * Initializes the settings manager parameters.
264: *
265: * @param array $parameters
266: * Settings for saving and loading as an associative array in the form key => value:
267: *
268: * - $parameters["save_path"] - the target file path where the settings data should be stored.
269: * - $parameters["save_encrypted"] - if it is true, the data is encrypted before saving.
270: * - $parameters["salt_key"] - the salt key if the data should be encrypted before saving.
271: * - $parameters["config_file_must_exist"] - if this parameter is true and the config file does not exist, the loading function will fail.
272: *
273: * - $parameters["use_apcu"] - if installed, apcu can be used to cache the settings in the memory.
274: *
275: * @return void
276: *
277: * @throws \Exception
278: * It might throw an exception in the case of any system errors:
279: *
280: * - if the save path is not specified.
281: *
282: * @author Oleg Schildt
283: */
284: public function init(array $parameters): void
285: {
286: if (!empty($parameters["save_path"])) {
287: $this->save_path = $parameters["save_path"];
288: }
289:
290: if (!empty($parameters["save_encrypted"])) {
291: $this->save_encrypted = $parameters["save_encrypted"];
292: }
293:
294: if (!empty($parameters["salt_key"])) {
295: $this->salt_key = $parameters["salt_key"];
296: }
297:
298: if (!empty($parameters["config_file_must_exist"])) {
299: $this->config_file_must_exist = $parameters["config_file_must_exist"];
300: }
301:
302: if (!empty($parameters["use_apcu"])) {
303: $this->use_apcu = $parameters["use_apcu"];
304: }
305:
306: $this->validateParameters();
307: } // init
308:
309: /**
310: * Sets the validator for the settings.
311: *
312: * @param ISettingsValidator $validator
313: * The settings validator.
314: *
315: * @return void
316: *
317: * @see ConfigSettingsManager::getValidator()
318: * @see ConfigSettingsManager::validateSettings()
319: *
320: * @author Oleg Schildt
321: */
322: public function setValidator(ISettingsValidator $validator): void
323: {
324: $this->validator = $validator;
325: } // setValidator
326:
327: /**
328: * Returns the validator for the settings.
329: *
330: * @return ?\SmartFactory\Interfaces\ISettingsValidator|null
331: * Returns the validator for the settings or null if none is defined.
332: *
333: * @see ConfigSettingsManager::setValidator()
334: * @see ConfigSettingsManager::validateSettings()
335: *
336: * @author Oleg Schildt
337: */
338: public function getValidator(): ?\SmartFactory\Interfaces\ISettingsValidator
339: {
340: return $this->validator;
341: } // getValidator
342:
343: /**
344: * Sets the settings context.
345: *
346: * Settings might be edited not in one dialog,
347: * There can be several masks for different type of settings,
348: * or the settings can be configured in a wizard.
349: * In this case, only a subset of settings has to be
350: * saved and validated.
351: * To be able to write a flexible validator for the subsets,
352: * the $context is used. It can be - step1, step1, server_settings,
353: * db_connection_settings etc.
354: *
355: * @param string $context
356: * The settings context.
357: *
358: * @return void
359: *
360: * @see ConfigSettingsManager::getContext()
361: *
362: * @author Oleg Schildt
363: */
364: public function setContext(string $context = "default"): void
365: {
366: $this->context = $context;
367: } // setContext
368:
369: /**
370: * Returns the current settings context.
371: *
372: * Settings might be edited not in one dialog,
373: * There can be several masks for different type of settings,
374: * or the settings can be configured in a wizard.
375: * In this case, only a subset of settings has to be
376: * saved and validated.
377: * To be able to write a flexible validator for the subsets,
378: * the $context is used. It can be - step1, step1, server_settings,
379: * db_connection_settings etc.
380: *
381: * @return string
382: * Returns the current settings context.
383: *
384: * @see ConfigSettingsManager::setContext()
385: *
386: * @author Oleg Schildt
387: */
388: public function getContext(): string
389: {
390: return $this->context;
391: } // getContext
392:
393: /**
394: * Sets a settings parameter.
395: *
396: * @param string $name
397: * The name of the settings parameter.
398: *
399: * @param mixed $value
400: * The value of the settings parameter.
401: *
402: * @return void
403: *
404: * @throws \Exception
405: * It might throw an exception in the case of any errors:
406: *
407: * - if some parameters are missing.
408: * - if dbworker does not extend {@see \SmartFactory\DatabaseWorkers\DBWorker}.
409: * - if the query fails or if some object names are invalid.
410: * - if the config file is not readable.
411: *
412: * @see ConfigSettingsManager::getParameter()
413: * @see ConfigSettingsManager::setParameters()
414: *
415: * @author Oleg Schildt
416: */
417: public function setParameter(string $name, mixed $value): void
418: {
419: if (empty($this->settings)) {
420: $this->loadSettings();
421: }
422:
423: $this->settings[$name] = $value;
424: } // setParameter
425:
426: /**
427: * Sets settings parameters from an array.
428: *
429: * @param array $parameters
430: * Array of parameters in the form key => value.
431: *
432: * @param bool $force_creation
433: * Flag which defines whether the parameter should be created
434: * if not exists. If false, only existing parameters are updated.
435: *
436: * @return void
437: *
438: * @throws \Exception
439: * It might throw an exception in the case of any errors:
440: *
441: * - if some parameters are missing.
442: * - if dbworker does not extend {@see \SmartFactory\DatabaseWorkers\DBWorker}.
443: * - if the query fails or if some object names are invalid.
444: * - if the config file is not readable.
445: *
446: * @see ConfigSettingsManager::getParameter()
447: * @see ConfigSettingsManager::setParameter()
448: *
449: * @author Oleg Schildt
450: */
451: public function setParameters(array $parameters, bool $force_creation = false): void
452: {
453: if (empty($this->settings)) {
454: $this->loadSettings();
455: }
456:
457: foreach ($parameters as $key => $val) {
458: if (!array_key_exists($key, $this->settings) && !$force_creation) {
459: continue;
460: }
461:
462: $this->settings[$key] = $val;
463: }
464: } // setParameters
465:
466: /**
467: * Returns the value of a settings parameter.
468: *
469: * @param string $name
470: * The name of the settings parameter.
471: *
472: * @param mixed $default
473: * The default value of the settings parameter if it is not set yet.
474: * The parameter is a conformable way to pre-set a parameter
475: * to a default value if its value is not set yet.
476: *
477: * @return mixed
478: * Returns the value of the settings parameter.
479: *
480: * @throws \Exception
481: * It might throw an exception in the case of any errors:
482: *
483: * - if the save path is not specified.
484: * - if the config file is not readable.
485: *
486: * @see ConfigSettingsManager::setParameter()
487: * @see ConfigSettingsManager::setParameters()
488: *
489: * @author Oleg Schildt
490: */
491: public function getParameter(string $name, mixed $default = null): mixed
492: {
493: if (empty($this->settings)) {
494: $this->loadSettings();
495: }
496:
497: if (!isset($this->settings[$name])) {
498: return $default;
499: }
500:
501: return $this->settings[$name];
502: } // getParameter
503:
504: /**
505: * Validates the current settings values.
506: *
507: * It should be called after settings the new values of the parameters
508: * and before their saving.
509: *
510: * @return void
511: *
512: * @throws \SmartFactory\SmartException|\SmartFactory\SmartExceptionCollection
513: * It might throw an exception if the content type oк JSON data is invalid.
514: *
515: * @uses Interfaces\ISettingsValidator
516: *
517: * @see ConfigSettingsManager::getValidator()
518: * @see ConfigSettingsManager::setValidator()
519: *
520: * @author Oleg Schildt
521: */
522: public function validateSettings(): void
523: {
524: if (empty($this->validator)) {
525: return;
526: }
527:
528: $this->validator->validate($this, $this->context);
529: } // validateSettings
530:
531: /**
532: * Loads the settings from the target file.
533: *
534: * @return void
535: * Returns true if the settings have been successfully loaded, otherwise false.
536: *
537: * @throws \Exception
538: * It might throw an exception in the case of any errors:
539: *
540: * - if the save path is not specified.
541: * - if the config file is not readable.
542: * - if the config file is invalid.
543: *
544: * @see ConfigSettingsManager::saveSettings()
545: *
546: * @author Oleg Schildt
547: */
548: public function loadSettings(): void
549: {
550: $this->loadJSON($this->settings);
551: } // loadSettings
552:
553: /**
554: * Saves the settings from to the target file.
555: *
556: * @return void
557: *
558: * @throws \Exception
559: * It might throw an exception in the case of any errors:
560: *
561: * - if the save path is not specified.
562: * - if the config file is not readable.
563: * - if the config file is not writable.
564: *
565: * @see ConfigSettingsManager::loadSettings()
566: *
567: * @author Oleg Schildt
568: */
569: public function saveSettings(): void
570: {
571: if (empty($this->settings)) {
572: $this->loadSettings();
573: }
574:
575: $this->saveJSON($this->settings);
576: } // saveSettings
577: } // ConfigSettingsManager