PluginProbe
Metricool – Social media and site statistics / trunk
Metricool – Social media and site statistics vtrunk
2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
metricool / app / Support / Validation / Validator.php

Validator.php in Metricool – Social media and site statistics trunk, at app/Support/Validation/Validator.php

222 lines 6.3 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
5 namespace Metricool\Support\Validation;
6
7 use Metricool\Exceptions\ValidationException;
8 use Metricool\Support\Validation\Rules\AbstractRule;
9 use Metricool\Support\Validation\Exceptions\RuleFailedException;
10
11 /**
12 * Laravel styled validator for validating request data. Each rule is a class
13 * in the Rules namespace, resolved from the rule string by the
14 * {@see RuleFactory}.
15 *
16 * Usage:
17 *
18 * // Throws a ValidationException on failure, returns the validated data on success
19 * $validated = Validator::validate($request->get_params(), [
20 * 'email' => 'required|email',
21 * 'password' => 'required|string|min:8',
22 * 'terms' => 'accepted',
23 * 'marketing' => 'boolean',
24 * ]);
25 *
26 * // Or inspect the result manually
27 * $validator = Validator::make($request->get_params(), ['email' => 'required|email']);
28 * if ($validator->fails()) {
29 * $errors = $validator->errors();
30 * }
31 *
32 * Rules may also be given as {@see AbstractRule} instances or class names,
33 * e.g. ['email' => ['required', new EmailRule()]] or
34 * ['email' => ['required', EmailRule::class]].
35 *
36 * Supported rules: required, requiredIf:otherField,value, email, url, string,
37 * boolean, accepted, numeric, integer, array, min:x, max:x, in:a,b,c,
38 * confirm:otherField
39 *
40 * The confirm rule checks that the field matches the value of another field,
41 * e.g. "passwordConfirmation" => "confirm:password". When no field is given
42 * it falls back to "{field}_confirmation".
43 */
44 class Validator
45 {
46 private array $data;
47 private array $rules;
48 private ?array $errors = null;
49
50 private function __construct(array $data, array $rules)
51 {
52 $this->data = $data;
53 $this->rules = $rules;
54 }
55
56 /**
57 * Create a new validator instance for the given data and rules.
58 */
59 public static function make(array $data, array $rules): self
60 {
61 return new self($data, $rules);
62 }
63
64 /**
65 * Validate the given data against the rules and return the validated data.
66 * @throws ValidationException when validation fails
67 */
68 public static function validate(array $data, array $rules): array
69 {
70 return self::make($data, $rules)->validated();
71 }
72
73 /**
74 * Check if the validation passes.
75 */
76 public function passes(): bool
77 {
78 return empty($this->errors());
79 }
80
81 /**
82 * Check if the validation fails.
83 */
84 public function fails(): bool
85 {
86 return !$this->passes();
87 }
88
89 /**
90 * Get the validation errors, keyed by field name.
91 */
92 public function errors(): array
93 {
94 return $this->errors ??= $this->collectErrors();
95 }
96
97 /**
98 * Get the validated data, containing only the fields that have rules.
99 * @throws ValidationException when validation fails
100 */
101 public function validated(): array
102 {
103 if ($this->fails()) {
104 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Errors are returned as JSON
105 throw ValidationException::withErrors($this->errors());
106 }
107
108 $validated = [];
109 foreach (array_keys($this->rules) as $field) {
110 if (array_key_exists($field, $this->data)) {
111 $validated[$field] = $this->data[$field];
112 }
113 }
114
115 return $validated;
116 }
117
118 /**
119 * Run the validation and return the errors, keyed by field name.
120 */
121 private function collectErrors(): array
122 {
123 $errors = [];
124
125 foreach ($this->rules as $field => $rules) {
126 $fieldErrors = $this->validateField($field, $this->parseRules($rules));
127
128 if (!empty($fieldErrors)) {
129 $errors[$field] = $fieldErrors;
130 }
131 }
132
133 return $errors;
134 }
135
136 /**
137 * Normalize a rule definition into an array of {@see AbstractRule}
138 * instances. Rule strings are resolved with the {@see RuleFactory}.
139 * @param string|array $rules
140 * @return AbstractRule[]
141 */
142 private function parseRules($rules): array
143 {
144 $rules = is_string($rules) ? explode('|', $rules) : (array) $rules;
145
146 return array_map(function ($rule) {
147 return $rule instanceof AbstractRule ? $rule : RuleFactory::createFromConfig($rule);
148 }, $rules);
149 }
150
151 /**
152 * Validate a single field against its rules. Required rules run first and stops
153 * the validation of the field when they fail.
154 *
155 * Optional rules are run after the required rules and only when the field is not empty.
156 * This prevents seeing redundant errors for a single field, for example:
157 * this field is required and this field is too short.
158 *
159 * @param AbstractRule[] $rules
160 * @return string[] the error messages for the field
161 */
162 private function validateField(string $field, array $rules): array
163 {
164 $value = $this->data[$field] ?? null;
165 $optionalRules = [];
166 $errors = [];
167
168 foreach ($rules as $rule) {
169 if (!$rule->isRequired()) {
170 $optionalRules[] = $rule;
171 continue;
172 }
173
174 $error = $this->applyRule($field, $value, $rule);
175
176 if ($error !== null) {
177 return [$error];
178 }
179 }
180
181 if (!self::isEmptyValue($value)) {
182 foreach ($optionalRules as $rule) {
183 $error = $this->applyRule($field, $value, $rule);
184
185 if ($error !== null) {
186 $errors[] = $error;
187 }
188 }
189 }
190
191 return $errors;
192 }
193
194 /**
195 * Apply a single rule to a field value.
196 * @param mixed $value
197 * @return string|null the error message when the rule fails, null when it passes
198 */
199 private function applyRule(string $field, $value, AbstractRule $rule): ?string
200 {
201 try {
202 $rule->validate($field, $value, $this->data);
203 } catch (RuleFailedException $e) {
204 return $e->getMessage();
205 }
206
207 return null;
208 }
209
210 /**
211 * Determine if a value counts as empty. Booleans are never considered
212 * empty.
213 * @param mixed $value
214 */
215 public static function isEmptyValue($value): bool
216 {
217 return $value === null
218 || (is_string($value) && trim($value) === '')
219 || (is_array($value) && empty($value));
220 }
221 }
222