| 1 |
<?php |
| 2 |
|
| 3 |
declare(strict_types=1); |
| 4 |
|
| 5 |
namespace Metricool\Features\UserSettings; |
| 6 |
|
| 7 |
use Metricool\Features\UserSettings\Exceptions\StorageSubmitException; |
| 8 |
use Metricool\Features\UserSettings\Exceptions\ValidationFailedExceptions; |
| 9 |
use Metricool\Features\UserSettings\Exceptions\ValidatorFailedException; |
| 10 |
use Metricool\Features\UserSettings\Fields\Field; |
| 11 |
use Metricool\Support\Helpers\Collection; |
| 12 |
|
| 13 |
/** |
| 14 |
* This service is responsible for storing and retrieving user settings. |
| 15 |
*/ |
| 16 |
class UserSettingsService |
| 17 |
{ |
| 18 |
/** |
| 19 |
* The fields from config/user_settings.php converted to {@see Field} |
| 20 |
* instances grouped in a Collection |
| 21 |
* @var Collection|Field[] |
| 22 |
*/ |
| 23 |
public Collection $fields; |
| 24 |
|
| 25 |
/** |
| 26 |
* Property is used to keep track of unique field storages when storing |
| 27 |
* settings, so we can submit each storage only once. |
| 28 |
*/ |
| 29 |
private array $uniqueFieldStorages = []; |
| 30 |
|
| 31 |
public function __construct(Collection $fields) |
| 32 |
{ |
| 33 |
$this->fields = $fields; |
| 34 |
} |
| 35 |
|
| 36 |
public function getSettings(): Collection |
| 37 |
{ |
| 38 |
return $this->fields; |
| 39 |
} |
| 40 |
|
| 41 |
/** |
| 42 |
* Return an array with keys and values of all the settings, optionally |
| 43 |
* filtered by section. |
| 44 |
* @throws \Exception |
| 45 |
*/ |
| 46 |
public function getSettingsResponse(?string $section = null): array |
| 47 |
{ |
| 48 |
$response = new UserSettingsResponse($this->fields); |
| 49 |
|
| 50 |
if (!empty($section)) { |
| 51 |
$response->setSection($section); |
| 52 |
} |
| 53 |
|
| 54 |
return $response->parse()->get(); |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* Validate and update settings and return their updated values. If any |
| 59 |
* field storage is submittable, it will be submitted after all fields are |
| 60 |
* validated. |
| 61 |
* |
| 62 |
* @throws ValidationFailedExceptions with all the validation errors when validation fails |
| 63 |
* @throws StorageSubmitException when it fails to store data to it's storage |
| 64 |
*/ |
| 65 |
public function storeSettings(array $requestData, \WP_REST_Request $request): Collection |
| 66 |
{ |
| 67 |
$validationErrors = []; |
| 68 |
$userSettings = $this->fields->whereIn('name', array_keys($requestData)); |
| 69 |
|
| 70 |
foreach ($userSettings as &$field) { |
| 71 |
$value = $requestData[$field->getName()]; |
| 72 |
|
| 73 |
try { |
| 74 |
$field->setValue($value, $request); |
| 75 |
} catch (ValidatorFailedException $e) { |
| 76 |
$validationErrors[$field->getName()] = $e; |
| 77 |
continue; |
| 78 |
} |
| 79 |
|
| 80 |
// Store the validated setting for later submission |
| 81 |
$this->storeValidatedSetting($field, $value); |
| 82 |
} |
| 83 |
|
| 84 |
// After all fields are validated, save the settings through their |
| 85 |
// storages |
| 86 |
$this->saveValidatedSettings(); |
| 87 |
|
| 88 |
// Still throw validation errors if any, this should not block storage |
| 89 |
// submission for valid fields |
| 90 |
if (count($validationErrors) > 0) { |
| 91 |
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- $validationErrors contains ValidatorFailedException objects with pre-escaped messages. |
| 92 |
throw new ValidationFailedExceptions($validationErrors); |
| 93 |
} |
| 94 |
|
| 95 |
return $userSettings; |
| 96 |
} |
| 97 |
|
| 98 |
/** |
| 99 |
* Store the validated value into the field's storage and keep track of |
| 100 |
* unique storages to submit later with {@see saveSettings()} |
| 101 |
* @param mixed $value |
| 102 |
*/ |
| 103 |
private function storeValidatedSetting(Field $field, $value): void |
| 104 |
{ |
| 105 |
$field->storage->set($field->getSettingName(), $value); |
| 106 |
|
| 107 |
// Remember the unique storages to submit later with saveSettings() |
| 108 |
if (empty($this->uniqueFieldStorages[$field->getStorageName()])) { |
| 109 |
$this->uniqueFieldStorages[$field->getStorageName()] = $field->getStorage(); |
| 110 |
} |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Save all updated unique storages that were mapped in |
| 115 |
* {@see storeValidatedSetting}. The method collects any errors and throws a |
| 116 |
* single exception if any storage submission fails. |
| 117 |
* |
| 118 |
* @throws StorageSubmitException when any storage submission fails - is |
| 119 |
* caught in {@see UserSettingsEndpoint} and the message we return here is |
| 120 |
* only shown to the user has WP_DEBUG set to true. |
| 121 |
*/ |
| 122 |
private function saveValidatedSettings(): void |
| 123 |
{ |
| 124 |
$requestErrors = []; |
| 125 |
foreach ($this->uniqueFieldStorages as $storage) { |
| 126 |
try { |
| 127 |
$storage->save(); |
| 128 |
} catch (\Exception $e) { |
| 129 |
$requestErrors[$storage->name] = $e->getMessage(); |
| 130 |
continue; // So we can try to submit other storages |
| 131 |
} |
| 132 |
} |
| 133 |
|
| 134 |
if (count($requestErrors) > 0) { |
| 135 |
$exception = new StorageSubmitException(__('Something went wrong while submitting the settings!', 'metricool')); |
| 136 |
$exception->setErrors($requestErrors); |
| 137 |
throw $exception; |
| 138 |
} |
| 139 |
} |
| 140 |
} |
| 141 |
|