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