PluginProbe
Metricool – Social media and site statistics / 2.1.1
Metricool – Social media and site statistics v2.1.1
2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
← All changes | app/Features/UserSettings/Fields/Field.php +272 -0 2.0.1 → 2.1.1 View file →
@@ -1,0 +1,272 @@
1 +<?php
2 +
3 +declare(strict_types=1);
4 +
5 +namespace Metricool\Features\UserSettings\Fields;
6 +
7 +use Metricool\Features\UserSettings\Exceptions\ValidatorFailedException;
8 +use Metricool\Features\UserSettings\Storage\AbstractStorage;
9 +use Metricool\Support\Validation\Rules\AbstractRule;
10 +use Metricool\Support\Validation\Validator;
11 +
12 +class Field
13 +{
14 + /**
15 + * Field identifier. Used as key when registering and as default setting name.
16 + */
17 + public string $name;
18 +
19 + /**
20 + * Field data type (boolean|integer|float|string|array|object). Used for
21 + * casting and validation.
22 + */
23 + public string $type;
24 +
25 + /**
26 + * Section name within settings UI. Used for grouping fields in the UI.
27 + */
28 + public ?string $section;
29 +
30 + /**
31 + * Storage key name (e.g. `default`). Used to select the storage implementation.
32 + */
33 + public string $storageName;
34 +
35 + /**
36 + * Setting name for persistence; if null, the field {@see $name} is used.
37 + */
38 + public ?string $settingName;
39 +
40 + /**
41 + * @var mixed Default value returned when no stored value exists.
42 + */
43 + public $defaultValue;
44 +
45 + /**
46 + * @var mixed Current value set via setValue(); if null, value is read from
47 + * storage.
48 + */
49 + public $value = null;
50 +
51 + /**
52 + * Storage instance used to update/read the field value.
53 + */
54 + public AbstractStorage $storage;
55 +
56 + /**
57 + * Validation rules applied to this field during validate(). Configured
58 + * with the 'validators' key in config/user_settings.php, either as rule
59 + * strings like 'required' or as {@see AbstractRule} instances.
60 + * @var array<string|AbstractRule>
61 + */
62 + protected array $validators = [];
63 +
64 + public function __construct(string $name)
65 + {
66 + $this->name = $name;
67 + }
68 +
69 + /**
70 + * Gets the name of the field. If no {@see $settingName} is set, this value
71 + * is used as the setting name.
72 + */
73 + public function getName(): string
74 + {
75 + return $this->name;
76 + }
77 +
78 + /**
79 + * Gets the type of the field
80 + */
81 + public function getType(): string
82 + {
83 + return $this->type;
84 + }
85 +
86 + /**
87 + * Gets the section name of this field
88 + */
89 + public function getSection(): ?string
90 + {
91 + return $this->section;
92 + }
93 +
94 + /**
95 + * Gets the setting name of the field
96 + */
97 + public function getSettingName(): string
98 + {
99 + return $this->settingName ?? $this->name;
100 + }
101 +
102 + /**
103 + * Gets the storage name of the field
104 + */
105 + public function getStorageName(): string
106 + {
107 + return $this->storageName;
108 + }
109 +
110 + /**
111 + * Set the storage object for this field
112 + */
113 + public function setStorage(AbstractStorage $storage): void
114 + {
115 + $this->storage = $storage;
116 + }
117 +
118 + /**
119 + * Gets the storage object for this field
120 + */
121 + public function getStorage(): AbstractStorage
122 + {
123 + return $this->storage;
124 + }
125 +
126 + /** @return mixed */
127 + public function getDefaultValue()
128 + {
129 + return $this->defaultValue;
130 + }
131 +
132 + /**
133 + * Sets the value of the field after validating it
134 + * @param mixed $value
135 + * @param \WP_REST_Request $request Pass the request object for
136 + * context-aware validation
137 + * @throws \LogicException when storage is not set by developer
138 + * @throws ValidatorFailedException when validation fails
139 + */
140 + public function setValue($value, \WP_REST_Request $request): void
141 + {
142 + if (empty($this->storage)) {
143 + throw new \LogicException('Storage not set for field: ' . esc_html($this->name) . '. First call setStorage() before setValue().');
144 + }
145 +
146 + $this->validate($value, $request);
147 + $this->value = $value;
148 + }
149 +
150 + /**
151 + * Retrieves the value of the field from storage but only if {@see setValue}
152 + * was not called before. If it was, the set value is returned. Method
153 + * returns default value when no value is found in storage.
154 + * @return mixed
155 + * @throws \LogicException when storage is not set by developer
156 + * @throws \Exception
157 + */
158 + public function getValue()
159 + {
160 + if (!empty($this->value)) {
161 + return $this->castValue($this->value);
162 + }
163 +
164 + if (empty($this->storage)) {
165 + throw new \LogicException('Storage not set for field: ' . esc_html($this->name) . '. First call setStorage() before getValue().');
166 + }
167 +
168 + $value = $this->storage->get($this->getSettingName());
169 +
170 + return is_null($value) ? $this->getDefaultValue() : $this->castValue($value);
171 + }
172 +
173 + /**
174 + * Casts the value to the type of the field
175 + * @param mixed $value
176 + * @return mixed
177 + */
178 + protected function castValue($value)
179 + {
180 + if ($this->isBoolean()) {
181 + return (bool) $value;
182 + }
183 +
184 + if ($this->isInteger()) {
185 + return (int) $value;
186 + }
187 +
188 + if ($this->isFloat()) {
189 + return (float) $value;
190 + }
191 +
192 + if ($this->isString()) {
193 + return (string) $value;
194 + }
195 +
196 + if ($this->isArray()) {
197 + return (array) $value;
198 + }
199 +
200 + if ($this->isObject()) {
201 + return (object) $value;
202 + }
203 +
204 + return $value;
205 + }
206 +
207 + /**
208 + * Validates the value of the field against this field's validators using
209 + * the {@see Validator}. The request params are passed along as validation
210 + * data for context-aware rules like requiredIf.
211 + * @param mixed $value
212 + * @throws ValidatorFailedException
213 + */
214 + public function validate($value, \WP_REST_Request $request): void
215 + {
216 + $data = $request->get_params();
217 + $data[$this->getName()] = $value;
218 +
219 + $validator = Validator::make($data, [$this->getName() => $this->validators]);
220 +
221 + if ($validator->fails()) {
222 + $messages = $validator->errors()[$this->getName()] ?? [];
223 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Messages are returned as JSON
224 + throw new ValidatorFailedException(implode(' ', $messages));
225 + }
226 + }
227 +
228 + /**
229 + * Apply the configuration array to the field
230 + */
231 + public function applyConfig(array $config = []): self
232 + {
233 + $this->type = $config['type'] ?? 'string';
234 + $this->section = $config['section'] ?? '';
235 + $this->storageName = $config['storage'] ?? 'default';
236 + $this->settingName = $config['settingName'] ?? null;
237 + $this->defaultValue = $config['defaultValue'] ?? null;
238 + $this->validators = $config['validators'] ?? [];
239 +
240 + return $this;
241 + }
242 +
243 + public function isBoolean(): bool
244 + {
245 + return $this->type === 'boolean';
246 + }
247 +
248 + public function isInteger(): bool
249 + {
250 + return $this->type === 'integer';
251 + }
252 +
253 + public function isFloat(): bool
254 + {
255 + return $this->type === 'float';
256 + }
257 +
258 + public function isString(): bool
259 + {
260 + return $this->type === 'string';
261 + }
262 +
263 + public function isArray(): bool
264 + {
265 + return $this->type === 'array';
266 + }
267 +
268 + public function isObject(): bool
269 + {
270 + return $this->type === 'object';
271 + }
272 +}