Source Code: SmartFactory/ConfigSettingsManager.php

  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