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.
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.
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
21 use Wikimedia\Assert\Assert
;
22 use Wikimedia\Message\MessageParam
;
23 use Wikimedia\Message\MessageSpecifier
;
24 use Wikimedia\Message\MessageValue
;
27 * Generic operation result class
28 * Has warning/error list, boolean status and arbitrary value
30 * "Good" means the operation was completed with no warnings or errors.
32 * "OK" means the operation was partially or wholly completed.
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.
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.
44 * This class accepts any MessageSpecifier objects. The use of Message objects
45 * should be avoided when serializability is needed. Use MessageValue in that
52 class StatusValue
implements Stringable
{
56 * @internal Only for use by Status. Use {@link self::isOK()} or {@link self::setOK()}.
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).
66 protected $errors = [];
71 /** @var bool[] Map of (key => bool) to indicate success of each part of batch operations */
74 /** @var int Counter for batch operations */
75 public $successCount = 0;
77 /** @var int Counter for batch operations */
78 public $failCount = 0;
80 /** @var mixed arbitrary extra data about the operation */
84 * Factory function for fatal errors
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()
92 public static function newFatal( $message, ...$parameters ) {
93 $result = new static();
94 $result->fatal( $message, ...$parameters );
99 * Factory function for good results
101 * @param mixed|null $value
104 public static function newGood( $value = null ) {
105 $result = new static();
106 $result->value
= $value;
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
115 * 0 => object(StatusValue) # the StatusValue with error messages, only
116 * 1 => object(StatusValue) # The StatusValue with warning messages, only
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() );
127 foreach ( $this->errors
as $item ) {
128 if ( $item['type'] === 'warning' ) {
129 $warningsOnlyStatusValue->errors
[] = $item;
131 $errorsOnlyStatusValue->errors
[] = $item;
135 return [ $errorsOnlyStatusValue, $warningsOnlyStatusValue ];
139 * Returns whether the operation completed and didn't have any error or
144 public function isGood() {
145 return $this->ok
&& !$this->errors
;
149 * Returns whether the operation completed
153 public function isOK() {
160 public function getValue() {
165 * Get the list of errors
167 * Each error is a (message:string or MessageSpecifier,params:array) map
169 * @deprecated since 1.43 Use `->getMessages()` instead
171 * @phan-return array{type:'warning'|'error', message:string|MessageSpecifier, params:array}[]
173 public function getErrors() {
174 return $this->errors
;
178 * Change operation status
183 public function setOK( $ok ) {
189 * Change operation result
191 * @param bool $ok Whether the operation completed
192 * @param mixed|null $value
195 public function setResult( $ok, $value = null ) {
196 $this->ok
= (bool)$ok;
197 $this->value
= $value;
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:
205 * - type: 'error' or 'warning'
206 * - message: a string (message key) or MessageSpecifier
207 * - params: an array of string parameters
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.
214 * @param array $newError
215 * @phan-param array{type:'warning'|'error', message:string|MessageSpecifier, params:array} $newError
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();
227 foreach ( $this->errors
as [ 'type' => &$type, 'message' => $key, 'params' => $params ] ) {
228 if ( $key instanceof MessageSpecifier
) {
229 $params = $key->getParams();
230 $key = $key->getKey();
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' ) {
243 $this->errors
[] = $newError;
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()
257 public function warning( $message, ...$parameters ) {
258 return $this->addError( [
260 'message' => $message,
261 'params' => $parameters
266 * Add an error, do not set fatal flag
267 * This can be used for non-fatal errors
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()
275 public function error( $message, ...$parameters ) {
276 return $this->addError( [
278 'message' => $message,
279 'params' => $parameters
284 * Add an error and set OK to false, indicating that the operation
285 * as a whole was fatal
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()
293 public function fatal( $message, ...$parameters ) {
295 return $this->error( $message, ...$parameters );
299 * Merge another status object into this one
301 * @param StatusValue $other
302 * @param bool $overwriteValue Whether to override the "value" member
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" );
309 $this->statusData ??
= $other->statusData
;
312 foreach ( $other->errors
as $error ) {
313 $this->addError( $error );
315 $this->ok
= $this->ok
&& $other->ok
;
316 if ( $overwriteValue ) {
317 $this->value
= $other->value
;
319 $this->successCount +
= $other->successCount
;
320 $this->failCount +
= $other->failCount
;
326 * Returns a list of status messages of the given type
328 * Each entry is a map of:
329 * - message: string message key or MessageSpecifier
330 * - params: array list of parameters
332 * @deprecated since 1.43 Use `->getMessages( $type )` instead
333 * @param string $type
335 * @phan-return array{type:'warning'|'error', message:string|MessageSpecifier, params:array}[]
337 public function getErrorsByType( $type ) {
339 foreach ( $this->errors
as $error ) {
340 if ( $error['type'] === $type ) {
349 * Returns a list of error messages, optionally only those of the given type
351 * If the `warning()` or `error()` method was called with a MessageSpecifier object,
352 * this method is guaranteed to return the same object.
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[]
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'" );
363 foreach ( $this->errors
as $error ) {
364 if ( $type === null ||
$error['type'] === $type ) {
365 [ 'message' => $key, 'params' => $params ] = $error;
366 if ( $key instanceof MessageSpecifier
) {
369 $result[] = new MessageValue( $key, $params );
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).
381 * @param string $message Message key to search for
384 public function hasMessage( string $message ) {
385 foreach ( $this->errors
as [ 'message' => $key ] ) {
386 if ( ( $key instanceof MessageSpecifier
&& $key->getKey() === $message ) ||
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).
400 * @param string ...$messages Message keys to search for
403 public function hasMessagesExcept( string ...$messages ) {
404 foreach ( $this->errors
as [ 'message' => $key ] ) {
405 if ( $key instanceof MessageSpecifier
) {
406 $key = $key->getKey();
408 if ( !in_array( $key, $messages, true ) ) {
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.
420 * Any message using the same key will be replaced (ignoring the message parameters).
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.
426 public function replaceMessage( string $source, $dest ) {
429 foreach ( $this->errors
as [ 'message' => &$message, 'params' => &$params ] ) {
430 if ( $message === $source ||
431 ( $message instanceof MessageSpecifier
&& $message->getKey() === $source )
434 if ( $dest instanceof MessageSpecifier
) {
435 // 'params' will be ignored now, so remove them from the internal array
446 * Returns a string representation of the status for debugging.
447 * This is fairly verbose and may change without notice.
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";
456 $errorcount = "no errors detected";
458 if ( $this->value
!== null ) {
459 $valstr = get_debug_type( $this->value
) . " value set";
461 $valstr = "no value set";
463 $out = sprintf( "<%s, %s, %s>",
468 if ( count( $this->errors
) > 0 ) {
469 $hdr = sprintf( "+-%'-8s-+-%'-25s-+-%'-36s-+\n", "", "", "" );
471 foreach ( $this->errors
as [ 'type' => $type, 'message' => $key, 'params' => $params ] ) {
472 if ( $key instanceof MessageSpecifier
) {
473 $params = $key->getParams();
474 $key = $key->getKey();
477 $keyChunks = mb_str_split( $key, 25 );
478 $paramsChunks = mb_str_split( $this->flattenParams( $params, " | " ), 36 );
480 // array_map(null,...) is like Python's zip()
481 foreach ( array_map( null, [ $type ], $keyChunks, $paramsChunks )
482 as [ $typeChunk, $keyChunk, $paramsChunk ]
484 $out .= sprintf( "| %-8s | %-25s | %-36s |\n",
498 * @param array $params Message parameters
499 * @param string $joiner
501 * @return string String representation
503 private function flattenParams( array $params, string $joiner = ', ' ): string {
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
) {
516 $ret[] = mb_strlen( $r ) > 100 ?
mb_substr( $r, 0, 99 ) . "..." : $r;
518 return implode( $joiner, $ret );
522 * Returns a list of status messages of the given type (or all if false)
524 * @internal Only for use by Status.
526 * @param string|bool $type
529 protected function getStatusArray( $type = false ) {
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() ];
537 $result[] = [ $error['message'], ...$error['params'] ];