PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.19
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.19
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / vendor_prefixed / sentry / sentry / src / OptionsResolver.php

OptionsResolver.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.19, at vendor_prefixed/sentry/sentry/src/OptionsResolver.php

314 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WCPOS\Vendor\Sentry;
5
6 use WCPOS\Vendor\Psr\Log\LoggerInterface;
7 use WCPOS\Vendor\Sentry\Util\Arr;
8 /**
9 * A container that declares defaults and allows validation and normalization in a central place.
10 * When a value fails validation, it will keep the current value (or fall back to the default value if there is no
11 * current value) and emit debug logs.
12 *
13 * Supports a nested config with arbitrary number of layers.
14 *
15 * To set validation on nested values, it's possible to use a . (dot) syntax, similar to how JSON can be traversed.
16 * For example, using 'foo.bar' will refer to
17 * 'foo' => [
18 * 'bar' => 'test'
19 * ]
20 *
21 * Dot syntax is generally available to set validators and normalizers, while defaults are specified using
22 * the real array shape
23 *
24 * @internal
25 */
26 class OptionsResolver
27 {
28 /**
29 * Contains all default values and also acts as a kind of schema.
30 * Only values present in the defaults can be overwritten.
31 *
32 * @var array<string, mixed>
33 */
34 private $defaults = [];
35 /**
36 * List of all allowed types for a path. Stored using dot syntax.
37 *
38 * @var array<string, string[]>
39 */
40 private $allowedTypes = [];
41 /**
42 * List of valid values or a validation callback for each option path.
43 * Stored using dot syntax.
44 *
45 * @var array<string, mixed[]|callable|bool|float|int|string|null>
46 */
47 private $allowedValues = [];
48 /**
49 * Stores normalizers for each option path. Stored using dot syntax.
50 *
51 * @var array<string, callable>
52 */
53 private $normalizers = [];
54 /**
55 * @param array<string, mixed> $defaults
56 */
57 public function setDefaults(array $defaults) : void
58 {
59 $this->defaults = $this->processDefaults($defaults, '');
60 }
61 /**
62 * @param mixed $value
63 */
64 public function setDefault(string $name, $value) : void
65 {
66 $this->defaults[$name] = $this->processDefaults([$name => $value], '')[$name];
67 }
68 /**
69 * @param mixed $types
70 */
71 public function setAllowedTypes(string $name, $types) : void
72 {
73 if (\is_string($types)) {
74 $this->allowedTypes[$name] = [$types];
75 return;
76 }
77 if (!\is_array($types)) {
78 throw new \InvalidArgumentException('Allowed types must be a string or an array of strings.');
79 }
80 $typeSpecs = [];
81 foreach (\array_keys($types) as $key) {
82 if (!\is_string($types[$key])) {
83 throw new \InvalidArgumentException('Allowed types must be strings.');
84 }
85 $typeSpecs[] = $types[$key];
86 }
87 $this->allowedTypes[$name] = $typeSpecs;
88 }
89 /**
90 * @param mixed[]|callable|bool|float|int|string|null $values
91 */
92 public function setAllowedValues(string $path, $values) : void
93 {
94 $this->allowedValues[$path] = $values;
95 }
96 public function setNormalizer(string $path, callable $normalizer) : void
97 {
98 $this->normalizers[$path] = $normalizer;
99 }
100 /**
101 * Resolves the passed options against the configured defaults.
102 * If a value does not have a default value, it will be ignored.
103 * If a value is invalid but has a default, it will fall back to using the default value.
104 *
105 * If a value doesn't exist or is invalid, a DEBUG log is generated using the configured logger.
106 *
107 * @param array<string, mixed> $options
108 *
109 * @return array<string, mixed>
110 */
111 public function resolve(array $options = [], ?LoggerInterface $logger = null) : array
112 {
113 return \array_merge($this->defaults, $this->resolveOnly($options, [], $logger));
114 }
115 /**
116 * Resolves only the options passed as $override and all nested keys that belong to it.
117 *
118 * @param array<string, mixed> $override
119 * @param array<string, mixed> $options
120 *
121 * @return array<string, mixed>
122 */
123 public function resolveOnly(array $override = [], array $options = [], ?LoggerInterface $logger = null) : array
124 {
125 return $this->applyOptions(\array_intersect_key($options, $override), $this->defaults, $override, '', $logger);
126 }
127 /**
128 * @param array<string, mixed> $defaults
129 *
130 * @return array<string, mixed>
131 */
132 private function processDefaults(array $defaults, string $parentPath) : array
133 {
134 /** @mago-ignore analysis:mixed-assignment */
135 foreach ($defaults as $option => $value) {
136 $path = $parentPath === '' ? $option : $parentPath . '.' . $option;
137 /** @mago-ignore analysis:mixed-assignment */
138 [$isValid, $processed] = $this->normalizeAndValidate($path, $value);
139 if (!$isValid) {
140 $defaults[$option] = null;
141 } elseif (!Arr::isAssociative($processed)) {
142 $defaults[$option] = $processed;
143 } else {
144 /** @var array<string, mixed> $processed */
145 $defaults[$option] = $this->processDefaults($processed, $path);
146 }
147 }
148 return $defaults;
149 }
150 /**
151 * @param array<string, mixed> $resolved
152 * @param array<string, mixed> $defaults
153 * @param array<string, mixed> $options
154 *
155 * @return array<string, mixed>
156 */
157 private function applyOptions(array $resolved, array $defaults, array $options, string $parentPath, ?LoggerInterface $logger) : array
158 {
159 /** @mago-ignore analysis:mixed-assignment */
160 foreach ($options as $option => $value) {
161 $path = $parentPath === '' ? $option : $parentPath . '.' . $option;
162 if (!\array_key_exists($option, $defaults)) {
163 if ($logger !== null) {
164 $logger->debug(\sprintf('Option "%s" does not exist and will be ignored', $path));
165 }
166 continue;
167 }
168 /** @mago-ignore analysis:mixed-assignment */
169 $default = $defaults[$option];
170 $isBranch = Arr::isAssociative($default);
171 /** @mago-ignore analysis:mixed-assignment */
172 [$isValid, $value] = $this->normalizeAndValidate($path, $value);
173 // If the value is invalid or the value is not in the correct shape, keep the current value if one exists.
174 // Otherwise, fall back to the default.
175 // For example, we expected to receive a nested value, but we got a scalar
176 if (!$isValid || $isBranch && !\is_array($value)) {
177 if ($logger !== null) {
178 $logger->debug(\sprintf('Invalid value for option "%s". The value has been ignored.', $path));
179 }
180 if (!\array_key_exists($option, $resolved)) {
181 $resolved[$option] = $default;
182 }
183 continue;
184 }
185 if (!$isBranch) {
186 $resolved[$option] = $value;
187 continue;
188 }
189 $base = $resolved[$option] ?? null;
190 if (!\is_array($base)) {
191 $base = $default;
192 }
193 /** @var array<string, mixed> $default */
194 /** @var array<string, mixed> $base */
195 /** @var array<string, mixed> $value */
196 $resolved[$option] = $this->applyOptions($base, $default, $value, $path, $logger);
197 }
198 return $resolved;
199 }
200 /**
201 * Normalizes and validates a value for a given path.
202 *
203 * @param mixed $value
204 *
205 * @return array{0: bool, 1: mixed} [isValid, normalizedValue]
206 */
207 private function normalizeAndValidate(string $name, $value) : array
208 {
209 if (!$this->validateType($name, $value)) {
210 return [\false, $value];
211 }
212 if (!$this->validateValue($name, $value)) {
213 return [\false, $value];
214 }
215 // If there's no normalizer for this path, or normalization is a no-op, skip re-validation
216 $normalizer = $this->normalizers[$name] ?? null;
217 if ($normalizer === null) {
218 return [\true, $value];
219 }
220 // Normalize, then validate again only if the value actually changed
221 /** @mago-ignore analysis:mixed-assignment */
222 $normalized = $normalizer($value);
223 if ($normalized === $value) {
224 return [\true, $value];
225 }
226 if (!$this->validateType($name, $normalized)) {
227 return [\false, $normalized];
228 }
229 if (!$this->validateValue($name, $normalized)) {
230 return [\false, $normalized];
231 }
232 return [\true, $normalized];
233 }
234 /**
235 * @param mixed $value
236 */
237 private function validateType(string $name, $value) : bool
238 {
239 $allowedTypes = $this->allowedTypes[$name] ?? null;
240 if ($allowedTypes === null) {
241 return \true;
242 }
243 foreach ($allowedTypes as $typeSpec) {
244 if ($this->valueMatchesType($value, $typeSpec)) {
245 return \true;
246 }
247 }
248 return \false;
249 }
250 /**
251 * Checks whether a value matches a given type specification.
252 * Supports built-ins, FQCNs/interfaces and typed arrays like "string[]" or Foo\Bar\Baz[].
253 *
254 * @param mixed $value
255 */
256 private function valueMatchesType($value, string $typeSpec) : bool
257 {
258 if (\substr($typeSpec, -2) === '[]') {
259 $elementType = \substr($typeSpec, 0, -2);
260 if (!\is_array($value)) {
261 return \false;
262 }
263 foreach (\array_keys($value) as $key) {
264 if (!$this->valueMatchesType($value[$key], $elementType)) {
265 return \false;
266 }
267 }
268 return \true;
269 }
270 switch ($typeSpec) {
271 case 'string':
272 return \is_string($value);
273 case 'int':
274 case 'integer':
275 return \is_int($value);
276 case 'float':
277 case 'double':
278 return \is_float($value);
279 case 'boolean':
280 case 'bool':
281 return \is_bool($value);
282 case 'array':
283 return \is_array($value);
284 case 'object':
285 return \is_object($value);
286 case 'callable':
287 return \is_callable($value);
288 case 'null':
289 return $value === null;
290 }
291 if (\is_object($value)) {
292 return $value instanceof $typeSpec;
293 }
294 return \false;
295 }
296 /**
297 * @param mixed $value
298 */
299 private function validateValue(string $name, $value) : bool
300 {
301 $allowedValue = $this->allowedValues[$name] ?? null;
302 if ($allowedValue === null) {
303 return \true;
304 }
305 if (\is_callable($allowedValue)) {
306 return $allowedValue($value) === \true;
307 }
308 if (!\is_array($allowedValue)) {
309 return $value === $allowedValue;
310 }
311 return \in_array($value, $allowedValue, \true);
312 }
313 }
314