Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.26% |
142 / 146 |
|
86.36% |
19 / 22 |
CRAP | |
0.00% |
0 / 1 |
StatusValue | |
97.26% |
142 / 146 |
|
86.36% |
19 / 22 |
73 | |
0.00% |
0 / 1 |
newFatal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newGood | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
splitByErrorType | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isGood | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
isOK | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOK | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setResult | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
addError | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 | |||
warning | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
error | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
fatal | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
merge | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
getErrorsByType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getMessages | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
7.05 | |||
hasMessage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
hasMessagesExcept | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
replaceMessage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
__toString | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
8 | |||
flattenParams | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
6.22 | |||
getStatusArray | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use Wikimedia\Assert\Assert; |
22 | use Wikimedia\Message\MessageParam; |
23 | use Wikimedia\Message\MessageSpecifier; |
24 | use Wikimedia\Message\MessageValue; |
25 | |
26 | /** |
27 | * Generic operation result class |
28 | * Has warning/error list, boolean status and arbitrary value |
29 | * |
30 | * "Good" means the operation was completed with no warnings or errors. |
31 | * |
32 | * "OK" means the operation was partially or wholly completed. |
33 | * |
34 | * An operation which is not OK should have errors so that the user can be |
35 | * informed as to what went wrong. Calling the fatal() function sets an error |
36 | * message and simultaneously switches off the OK flag. |
37 | * |
38 | * The recommended pattern for functions returning StatusValue objects is |
39 | * to return a StatusValue unconditionally, both on success and on failure |
40 | * (similarly to Option, Maybe, Promise etc. objects in other languages) -- |
41 | * so that the developer of the calling code is reminded that the function |
42 | * can fail, and so that a lack of error-handling will be explicit. |
43 | * |
44 | * This class accepts any MessageSpecifier objects. The use of Message objects |
45 | * should be avoided when serializability is needed. Use MessageValue in that |
46 | * case instead. |
47 | * |
48 | * @newable |
49 | * @stable to extend |
50 | * @since 1.25 |
51 | */ |
52 | class StatusValue implements Stringable { |
53 | |
54 | /** |
55 | * @var bool |
56 | * @internal Only for use by Status. Use {@link self::isOK()} or {@link self::setOK()}. |
57 | */ |
58 | protected $ok = true; |
59 | |
60 | /** |
61 | * @var array[] |
62 | * @internal Only for use by Status. Use {@link self::getErrors()} (get full list), |
63 | * {@link self::splitByErrorType()} (get errors/warnings), or |
64 | * {@link self::fatal()}, {@link self::error()} or {@link self::warning()} (add error/warning). |
65 | */ |
66 | protected $errors = []; |
67 | |
68 | /** @var mixed */ |
69 | public $value; |
70 | |
71 | /** @var bool[] Map of (key => bool) to indicate success of each part of batch operations */ |
72 | public $success = []; |
73 | |
74 | /** @var int Counter for batch operations */ |
75 | public $successCount = 0; |
76 | |
77 | /** @var int Counter for batch operations */ |
78 | public $failCount = 0; |
79 | |
80 | /** @var mixed arbitrary extra data about the operation */ |
81 | public $statusData; |
82 | |
83 | /** |
84 | * Factory function for fatal errors |
85 | * |
86 | * @param string|MessageSpecifier $message Message key or object |
87 | * @phpcs:ignore Generic.Files.LineLength |
88 | * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$parameters |
89 | * See Message::params() |
90 | * @return static |
91 | */ |
92 | public static function newFatal( $message, ...$parameters ) { |
93 | $result = new static(); |
94 | $result->fatal( $message, ...$parameters ); |
95 | return $result; |
96 | } |
97 | |
98 | /** |
99 | * Factory function for good results |
100 | * |
101 | * @param mixed|null $value |
102 | * @return static |
103 | */ |
104 | public static function newGood( $value = null ) { |
105 | $result = new static(); |
106 | $result->value = $value; |
107 | return $result; |
108 | } |
109 | |
110 | /** |
111 | * Splits this StatusValue object into two new StatusValue objects, one which contains only |
112 | * the error messages, and one that contains the warnings, only. The returned array is |
113 | * defined as: |
114 | * [ |
115 | * 0 => object(StatusValue) # the StatusValue with error messages, only |
116 | * 1 => object(StatusValue) # The StatusValue with warning messages, only |
117 | * ] |
118 | * |
119 | * @return static[] |
120 | */ |
121 | public function splitByErrorType() { |
122 | $errorsOnlyStatusValue = static::newGood(); |
123 | $warningsOnlyStatusValue = static::newGood(); |
124 | $warningsOnlyStatusValue->setResult( true, $this->getValue() ); |
125 | $errorsOnlyStatusValue->setResult( $this->isOK(), $this->getValue() ); |
126 | |
127 | foreach ( $this->errors as $item ) { |
128 | if ( $item['type'] === 'warning' ) { |
129 | $warningsOnlyStatusValue->errors[] = $item; |
130 | } else { |
131 | $errorsOnlyStatusValue->errors[] = $item; |
132 | } |
133 | } |
134 | |
135 | return [ $errorsOnlyStatusValue, $warningsOnlyStatusValue ]; |
136 | } |
137 | |
138 | /** |
139 | * Returns whether the operation completed and didn't have any error or |
140 | * warnings |
141 | * |
142 | * @return bool |
143 | */ |
144 | public function isGood() { |
145 | return $this->ok && !$this->errors; |
146 | } |
147 | |
148 | /** |
149 | * Returns whether the operation completed |
150 | * |
151 | * @return bool |
152 | */ |
153 | public function isOK() { |
154 | return $this->ok; |
155 | } |
156 | |
157 | /** |
158 | * @return mixed |
159 | */ |
160 | public function getValue() { |
161 | return $this->value; |
162 | } |
163 | |
164 | /** |
165 | * Get the list of errors |
166 | * |
167 | * Each error is a (message:string or MessageSpecifier,params:array) map |
168 | * |
169 | * @deprecated since 1.43 Use `->getMessages()` instead |
170 | * @return array[] |
171 | * @phan-return array{type:'warning'|'error', message:string|MessageSpecifier, params:array}[] |
172 | */ |
173 | public function getErrors() { |
174 | return $this->errors; |
175 | } |
176 | |
177 | /** |
178 | * Change operation status |
179 | * |
180 | * @param bool $ok |
181 | * @return $this |
182 | */ |
183 | public function setOK( $ok ) { |
184 | $this->ok = $ok; |
185 | return $this; |
186 | } |
187 | |
188 | /** |
189 | * Change operation result |
190 | * |
191 | * @param bool $ok Whether the operation completed |
192 | * @param mixed|null $value |
193 | * @return $this |
194 | */ |
195 | public function setResult( $ok, $value = null ) { |
196 | $this->ok = (bool)$ok; |
197 | $this->value = $value; |
198 | return $this; |
199 | } |
200 | |
201 | /** |
202 | * Add a new error to the error array ($this->errors) if that error is not already in the |
203 | * error array. Each error is passed as an array with the following fields: |
204 | * |
205 | * - type: 'error' or 'warning' |
206 | * - message: a string (message key) or MessageSpecifier |
207 | * - params: an array of string parameters |
208 | * |
209 | * If the new error is of type 'error' and it matches an existing error of type 'warning', |
210 | * the existing error is upgraded to type 'error'. An error provided as a MessageSpecifier |
211 | * will successfully match an error provided as the same string message key and array of |
212 | * parameters as separate array elements. |
213 | * |
214 | * @param array $newError |
215 | * @phan-param array{type:'warning'|'error', message:string|MessageSpecifier, params:array} $newError |
216 | * @return $this |
217 | */ |
218 | private function addError( array $newError ) { |
219 | [ 'type' => $newType, 'message' => $newKey, 'params' => $newParams ] = $newError; |
220 | if ( $newKey instanceof MessageSpecifier ) { |
221 | Assert::parameter( $newParams === [], |
222 | '$parameters', "must be empty when using a MessageSpecifier" ); |
223 | $newParams = $newKey->getParams(); |
224 | $newKey = $newKey->getKey(); |
225 | } |
226 | |
227 | foreach ( $this->errors as [ 'type' => &$type, 'message' => $key, 'params' => $params ] ) { |
228 | if ( $key instanceof MessageSpecifier ) { |
229 | $params = $key->getParams(); |
230 | $key = $key->getKey(); |
231 | } |
232 | |
233 | // This uses loose equality as we must support equality between MessageParam objects |
234 | // (e.g. ScalarParam), including when they are created separate and not by-ref equal. |
235 | if ( $newKey === $key && $newParams == $params ) { |
236 | if ( $type === 'warning' && $newType === 'error' ) { |
237 | $type = 'error'; |
238 | } |
239 | return $this; |
240 | } |
241 | } |
242 | |
243 | $this->errors[] = $newError; |
244 | |
245 | return $this; |
246 | } |
247 | |
248 | /** |
249 | * Add a new warning |
250 | * |
251 | * @param string|MessageSpecifier $message Message key or object |
252 | * @phpcs:ignore Generic.Files.LineLength |
253 | * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$parameters |
254 | * See Message::params() |
255 | * @return $this |
256 | */ |
257 | public function warning( $message, ...$parameters ) { |
258 | return $this->addError( [ |
259 | 'type' => 'warning', |
260 | 'message' => $message, |
261 | 'params' => $parameters |
262 | ] ); |
263 | } |
264 | |
265 | /** |
266 | * Add an error, do not set fatal flag |
267 | * This can be used for non-fatal errors |
268 | * |
269 | * @param string|MessageSpecifier $message Message key or object |
270 | * @phpcs:ignore Generic.Files.LineLength |
271 | * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$parameters |
272 | * See Message::params() |
273 | * @return $this |
274 | */ |
275 | public function error( $message, ...$parameters ) { |
276 | return $this->addError( [ |
277 | 'type' => 'error', |
278 | 'message' => $message, |
279 | 'params' => $parameters |
280 | ] ); |
281 | } |
282 | |
283 | /** |
284 | * Add an error and set OK to false, indicating that the operation |
285 | * as a whole was fatal |
286 | * |
287 | * @param string|MessageSpecifier $message Message key or object |
288 | * @phpcs:ignore Generic.Files.LineLength |
289 | * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$parameters |
290 | * See Message::params() |
291 | * @return $this |
292 | */ |
293 | public function fatal( $message, ...$parameters ) { |
294 | $this->ok = false; |
295 | return $this->error( $message, ...$parameters ); |
296 | } |
297 | |
298 | /** |
299 | * Merge another status object into this one |
300 | * |
301 | * @param StatusValue $other |
302 | * @param bool $overwriteValue Whether to override the "value" member |
303 | * @return $this |
304 | */ |
305 | public function merge( $other, $overwriteValue = false ) { |
306 | if ( $this->statusData !== null && $other->statusData !== null ) { |
307 | throw new RuntimeException( "Status cannot be merged, because they both have \$statusData" ); |
308 | } else { |
309 | $this->statusData ??= $other->statusData; |
310 | } |
311 | |
312 | foreach ( $other->errors as $error ) { |
313 | $this->addError( $error ); |
314 | } |
315 | $this->ok = $this->ok && $other->ok; |
316 | if ( $overwriteValue ) { |
317 | $this->value = $other->value; |
318 | } |
319 | $this->successCount += $other->successCount; |
320 | $this->failCount += $other->failCount; |
321 | |
322 | return $this; |
323 | } |
324 | |
325 | /** |
326 | * Returns a list of status messages of the given type |
327 | * |
328 | * Each entry is a map of: |
329 | * - message: string message key or MessageSpecifier |
330 | * - params: array list of parameters |
331 | * |
332 | * @deprecated since 1.43 Use `->getMessages( $type )` instead |
333 | * @param string $type |
334 | * @return array[] |
335 | * @phan-return array{type:'warning'|'error', message:string|MessageSpecifier, params:array}[] |
336 | */ |
337 | public function getErrorsByType( $type ) { |
338 | $result = []; |
339 | foreach ( $this->errors as $error ) { |
340 | if ( $error['type'] === $type ) { |
341 | $result[] = $error; |
342 | } |
343 | } |
344 | |
345 | return $result; |
346 | } |
347 | |
348 | /** |
349 | * Returns a list of error messages, optionally only those of the given type |
350 | * |
351 | * If the `warning()` or `error()` method was called with a MessageSpecifier object, |
352 | * this method is guaranteed to return the same object. |
353 | * |
354 | * @since 1.43 |
355 | * @param ?string $type If provided, only return messages of the type 'warning' or 'error' |
356 | * @phan-param null|'warning'|'error' $type |
357 | * @return MessageSpecifier[] |
358 | */ |
359 | public function getMessages( ?string $type = null ): array { |
360 | Assert::parameter( $type === null || $type === 'warning' || $type === 'error', |
361 | '$type', "must be null, 'warning', or 'error'" ); |
362 | $result = []; |
363 | foreach ( $this->errors as $error ) { |
364 | if ( $type === null || $error['type'] === $type ) { |
365 | [ 'message' => $key, 'params' => $params ] = $error; |
366 | if ( $key instanceof MessageSpecifier ) { |
367 | $result[] = $key; |
368 | } else { |
369 | $result[] = new MessageValue( $key, $params ); |
370 | } |
371 | } |
372 | } |
373 | |
374 | return $result; |
375 | } |
376 | |
377 | /** |
378 | * Returns true if the specified message is present as a warning or error. |
379 | * Any message using the same key will be found (ignoring the message parameters). |
380 | * |
381 | * @param string $message Message key to search for |
382 | * @return bool |
383 | */ |
384 | public function hasMessage( string $message ) { |
385 | foreach ( $this->errors as [ 'message' => $key ] ) { |
386 | if ( ( $key instanceof MessageSpecifier && $key->getKey() === $message ) || |
387 | $key === $message |
388 | ) { |
389 | return true; |
390 | } |
391 | } |
392 | |
393 | return false; |
394 | } |
395 | |
396 | /** |
397 | * Returns true if any other message than the specified ones is present as a warning or error. |
398 | * Any messages using the same keys will be found (ignoring the message parameters). |
399 | * |
400 | * @param string ...$messages Message keys to search for |
401 | * @return bool |
402 | */ |
403 | public function hasMessagesExcept( string ...$messages ) { |
404 | foreach ( $this->errors as [ 'message' => $key ] ) { |
405 | if ( $key instanceof MessageSpecifier ) { |
406 | $key = $key->getKey(); |
407 | } |
408 | if ( !in_array( $key, $messages, true ) ) { |
409 | return true; |
410 | } |
411 | } |
412 | |
413 | return false; |
414 | } |
415 | |
416 | /** |
417 | * If the specified source message exists, replace it with the specified |
418 | * destination message, but keep the same parameters as in the original error. |
419 | * |
420 | * Any message using the same key will be replaced (ignoring the message parameters). |
421 | * |
422 | * @param string $source Message key to search for |
423 | * @param MessageSpecifier|string $dest Replacement message key or object |
424 | * @return bool Return true if the replacement was done, false otherwise. |
425 | */ |
426 | public function replaceMessage( string $source, $dest ) { |
427 | $replaced = false; |
428 | |
429 | foreach ( $this->errors as [ 'message' => &$message, 'params' => &$params ] ) { |
430 | if ( $message === $source || |
431 | ( $message instanceof MessageSpecifier && $message->getKey() === $source ) |
432 | ) { |
433 | $message = $dest; |
434 | if ( $dest instanceof MessageSpecifier ) { |
435 | // 'params' will be ignored now, so remove them from the internal array |
436 | $params = []; |
437 | } |
438 | $replaced = true; |
439 | } |
440 | } |
441 | |
442 | return $replaced; |
443 | } |
444 | |
445 | /** |
446 | * Returns a string representation of the status for debugging. |
447 | * This is fairly verbose and may change without notice. |
448 | * |
449 | * @return string |
450 | */ |
451 | public function __toString() { |
452 | $status = $this->isOK() ? "OK" : "Error"; |
453 | if ( count( $this->errors ) ) { |
454 | $errorcount = "collected " . ( count( $this->errors ) ) . " message(s) on the way"; |
455 | } else { |
456 | $errorcount = "no errors detected"; |
457 | } |
458 | if ( $this->value !== null ) { |
459 | $valstr = get_debug_type( $this->value ) . " value set"; |
460 | } else { |
461 | $valstr = "no value set"; |
462 | } |
463 | $out = sprintf( "<%s, %s, %s>", |
464 | $status, |
465 | $errorcount, |
466 | $valstr |
467 | ); |
468 | if ( count( $this->errors ) > 0 ) { |
469 | $hdr = sprintf( "+-%'-8s-+-%'-25s-+-%'-36s-+\n", "", "", "" ); |
470 | $out .= "\n" . $hdr; |
471 | foreach ( $this->errors as [ 'type' => $type, 'message' => $key, 'params' => $params ] ) { |
472 | if ( $key instanceof MessageSpecifier ) { |
473 | $params = $key->getParams(); |
474 | $key = $key->getKey(); |
475 | } |
476 | |
477 | $keyChunks = mb_str_split( $key, 25 ); |
478 | $paramsChunks = mb_str_split( $this->flattenParams( $params, " | " ), 36 ); |
479 | |
480 | // array_map(null,...) is like Python's zip() |
481 | foreach ( array_map( null, [ $type ], $keyChunks, $paramsChunks ) |
482 | as [ $typeChunk, $keyChunk, $paramsChunk ] |
483 | ) { |
484 | $out .= sprintf( "| %-8s | %-25s | %-36s |\n", |
485 | $typeChunk, |
486 | $keyChunk, |
487 | $paramsChunk |
488 | ); |
489 | } |
490 | } |
491 | $out .= $hdr; |
492 | } |
493 | |
494 | return $out; |
495 | } |
496 | |
497 | /** |
498 | * @param array $params Message parameters |
499 | * @param string $joiner |
500 | * |
501 | * @return string String representation |
502 | */ |
503 | private function flattenParams( array $params, string $joiner = ', ' ): string { |
504 | $ret = []; |
505 | foreach ( $params as $p ) { |
506 | if ( is_array( $p ) ) { |
507 | $r = '[ ' . self::flattenParams( $p ) . ' ]'; |
508 | } elseif ( $p instanceof MessageSpecifier ) { |
509 | $r = '{ ' . $p->getKey() . ': ' . self::flattenParams( $p->getParams() ) . ' }'; |
510 | } elseif ( $p instanceof MessageParam ) { |
511 | $r = $p->dump(); |
512 | } else { |
513 | $r = (string)$p; |
514 | } |
515 | |
516 | $ret[] = mb_strlen( $r ) > 100 ? mb_substr( $r, 0, 99 ) . "..." : $r; |
517 | } |
518 | return implode( $joiner, $ret ); |
519 | } |
520 | |
521 | /** |
522 | * Returns a list of status messages of the given type (or all if false) |
523 | * |
524 | * @internal Only for use by Status. |
525 | * |
526 | * @param string|bool $type |
527 | * @return array[] |
528 | */ |
529 | protected function getStatusArray( $type = false ) { |
530 | $result = []; |
531 | |
532 | foreach ( $this->getErrors() as $error ) { |
533 | if ( !$type || $error['type'] === $type ) { |
534 | if ( $error['message'] instanceof MessageSpecifier ) { |
535 | $result[] = [ $error['message']->getKey(), ...$error['message']->getParams() ]; |
536 | } else { |
537 | $result[] = [ $error['message'], ...$error['params'] ]; |
538 | } |
539 | } |
540 | } |
541 | |
542 | return $result; |
543 | } |
544 | } |