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