1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Extbase\Error; 19 20/** 21 * Result object for operations dealing with objects, such as the Property Mapper or the Validators. 22 */ 23class Result 24{ 25 /** 26 * @var Error[] 27 */ 28 protected $errors = []; 29 30 /** 31 * Caches the existence of errors 32 * @var bool 33 */ 34 protected $errorsExist = false; 35 36 /** 37 * @var Warning[] 38 */ 39 protected $warnings = []; 40 41 /** 42 * Caches the existence of warning 43 * @var bool 44 */ 45 protected $warningsExist = false; 46 47 /** 48 * @var Notice[] 49 */ 50 protected $notices = []; 51 52 /** 53 * Caches the existence of notices 54 * @var bool 55 */ 56 protected $noticesExist = false; 57 58 /** 59 * The result objects for the sub properties 60 * 61 * @var Result[] 62 */ 63 protected $propertyResults = []; 64 65 /** 66 * @var Result 67 */ 68 protected $parent; 69 70 /** 71 * Injects the parent result and propagates the 72 * cached error states upwards 73 * 74 * @param Result $parent 75 */ 76 public function setParent(Result $parent): void 77 { 78 if ($this->parent !== $parent) { 79 $this->parent = $parent; 80 if ($this->hasErrors()) { 81 $parent->setErrorsExist(); 82 } 83 if ($this->hasWarnings()) { 84 $parent->setWarningsExist(); 85 } 86 if ($this->hasNotices()) { 87 $parent->setNoticesExist(); 88 } 89 } 90 } 91 92 /** 93 * Add an error to the current Result object 94 * 95 * @param Error $error 96 */ 97 public function addError(Error $error): void 98 { 99 $this->errors[] = $error; 100 $this->setErrorsExist(); 101 } 102 103 /** 104 * Add a warning to the current Result object 105 * 106 * @param Warning $warning 107 */ 108 public function addWarning(Warning $warning): void 109 { 110 $this->warnings[] = $warning; 111 $this->setWarningsExist(); 112 } 113 114 /** 115 * Add a notice to the current Result object 116 * 117 * @param Notice $notice 118 */ 119 public function addNotice(Notice $notice): void 120 { 121 $this->notices[] = $notice; 122 $this->setNoticesExist(); 123 } 124 125 /** 126 * Get all errors in the current Result object (non-recursive) 127 * 128 * @return Error[] 129 */ 130 public function getErrors(): array 131 { 132 return $this->errors; 133 } 134 135 /** 136 * Get all warnings in the current Result object (non-recursive) 137 * 138 * @return Warning[] 139 */ 140 public function getWarnings(): array 141 { 142 return $this->warnings; 143 } 144 145 /** 146 * Get all notices in the current Result object (non-recursive) 147 * 148 * @return Notice[] 149 */ 150 public function getNotices(): array 151 { 152 return $this->notices; 153 } 154 155 /** 156 * Get the first error object of the current Result object (non-recursive) 157 * 158 * @return bool|Error 159 */ 160 public function getFirstError() 161 { 162 reset($this->errors); 163 return current($this->errors); 164 } 165 166 /** 167 * Get the first warning object of the current Result object (non-recursive) 168 * 169 * @return bool|Warning 170 */ 171 public function getFirstWarning() 172 { 173 reset($this->warnings); 174 return current($this->warnings); 175 } 176 177 /** 178 * Get the first notice object of the current Result object (non-recursive) 179 * 180 * @return bool|Notice 181 */ 182 public function getFirstNotice() 183 { 184 reset($this->notices); 185 return current($this->notices); 186 } 187 188 /** 189 * Return a Result object for the given property path. This is 190 * a fluent interface, so you will probably use it like: 191 * $result->forProperty('foo.bar')->getErrors() -- to get all errors 192 * for property "foo.bar" 193 * 194 * @param string|null $propertyPath 195 * @return Result 196 */ 197 public function forProperty(?string $propertyPath): Result 198 { 199 if ($propertyPath === '' || $propertyPath === null) { 200 return $this; 201 } 202 if (strpos($propertyPath, '.') !== false) { 203 return $this->recurseThroughResult(explode('.', $propertyPath)); 204 } 205 if (!isset($this->propertyResults[$propertyPath])) { 206 $this->propertyResults[$propertyPath] = new self(); 207 $this->propertyResults[$propertyPath]->setParent($this); 208 } 209 return $this->propertyResults[$propertyPath]; 210 } 211 212 /** 213 * @todo: consider making this method protected as it will and should not be called from an outside scope 214 * 215 * @param array $pathSegments 216 * @return Result 217 * 218 * @internal only to be used within Extbase, not part of TYPO3 Core API. 219 */ 220 public function recurseThroughResult(array $pathSegments): Result 221 { 222 if (count($pathSegments) === 0) { 223 return $this; 224 } 225 226 $propertyName = array_shift($pathSegments); 227 228 if (!isset($this->propertyResults[$propertyName])) { 229 $this->propertyResults[$propertyName] = new self(); 230 $this->propertyResults[$propertyName]->setParent($this); 231 } 232 233 return $this->propertyResults[$propertyName]->recurseThroughResult($pathSegments); 234 } 235 236 /** 237 * Sets the error cache to TRUE and propagates the information 238 * upwards the Result-Object Tree 239 */ 240 protected function setErrorsExist(): void 241 { 242 $this->errorsExist = true; 243 if ($this->parent !== null) { 244 $this->parent->setErrorsExist(); 245 } 246 } 247 248 /** 249 * Sets the warning cache to TRUE and propagates the information 250 * upwards the Result-Object Tree 251 */ 252 protected function setWarningsExist(): void 253 { 254 $this->warningsExist = true; 255 if ($this->parent !== null) { 256 $this->parent->setWarningsExist(); 257 } 258 } 259 260 /** 261 * Sets the notices cache to TRUE and propagates the information 262 * upwards the Result-Object Tree 263 */ 264 protected function setNoticesExist(): void 265 { 266 $this->noticesExist = true; 267 if ($this->parent !== null) { 268 $this->parent->setNoticesExist(); 269 } 270 } 271 272 /** 273 * Does the current Result object have Notices, Errors or Warnings? (Recursively) 274 * 275 * @return bool 276 */ 277 public function hasMessages(): bool 278 { 279 return $this->errorsExist || $this->noticesExist || $this->warningsExist; 280 } 281 282 /** 283 * Clears the result 284 */ 285 public function clear(): void 286 { 287 $this->errors = []; 288 $this->notices = []; 289 $this->warnings = []; 290 291 $this->warningsExist = false; 292 $this->noticesExist = false; 293 $this->errorsExist = false; 294 295 $this->propertyResults = []; 296 } 297 298 /** 299 * Does the current Result object have Errors? (Recursively) 300 * 301 * @return bool 302 */ 303 public function hasErrors(): bool 304 { 305 if (count($this->errors) > 0) { 306 return true; 307 } 308 309 foreach ($this->propertyResults as $subResult) { 310 if ($subResult->hasErrors()) { 311 return true; 312 } 313 } 314 315 return false; 316 } 317 318 /** 319 * Does the current Result object have Warnings? (Recursively) 320 * 321 * @return bool 322 */ 323 public function hasWarnings(): bool 324 { 325 if (count($this->warnings) > 0) { 326 return true; 327 } 328 329 foreach ($this->propertyResults as $subResult) { 330 if ($subResult->hasWarnings()) { 331 return true; 332 } 333 } 334 335 return false; 336 } 337 338 /** 339 * Does the current Result object have Notices? (Recursively) 340 * 341 * @return bool 342 */ 343 public function hasNotices(): bool 344 { 345 if (count($this->notices) > 0) { 346 return true; 347 } 348 349 foreach ($this->propertyResults as $subResult) { 350 if ($subResult->hasNotices()) { 351 return true; 352 } 353 } 354 355 return false; 356 } 357 358 /** 359 * Get a list of all Error objects recursively. The result is an array, 360 * where the key is the property path where the error occurred, and the 361 * value is a list of all errors (stored as array) 362 * 363 * @return array<string,array<Error>> 364 */ 365 public function getFlattenedErrors(): array 366 { 367 $result = []; 368 $this->flattenErrorTree($result, []); 369 return $result; 370 } 371 372 /** 373 * Get a list of all Warning objects recursively. The result is an array, 374 * where the key is the property path where the warning occurred, and the 375 * value is a list of all warnings (stored as array) 376 * 377 * @return array<string,array<Warning>> 378 */ 379 public function getFlattenedWarnings(): array 380 { 381 $result = []; 382 $this->flattenWarningsTree($result, []); 383 return $result; 384 } 385 386 /** 387 * Get a list of all Notice objects recursively. The result is an array, 388 * where the key is the property path where the notice occurred, and the 389 * value is a list of all notices (stored as array) 390 * 391 * @return array<string,array<Notice>> 392 */ 393 public function getFlattenedNotices(): array 394 { 395 $result = []; 396 $this->flattenNoticesTree($result, []); 397 return $result; 398 } 399 400 /** 401 * @param array $result 402 * @param array $level 403 */ 404 protected function flattenErrorTree(array &$result, array $level): void 405 { 406 if (count($this->errors) > 0) { 407 $result[implode('.', $level)] = $this->errors; 408 } 409 foreach ($this->propertyResults as $subPropertyName => $subResult) { 410 $level[] = $subPropertyName; 411 $subResult->flattenErrorTree($result, $level); 412 array_pop($level); 413 } 414 } 415 416 /** 417 * @param array $result 418 * @param array $level 419 */ 420 protected function flattenWarningsTree(array &$result, array $level): void 421 { 422 if (count($this->warnings) > 0) { 423 $result[implode('.', $level)] = $this->warnings; 424 } 425 foreach ($this->propertyResults as $subPropertyName => $subResult) { 426 $level[] = $subPropertyName; 427 $subResult->flattenWarningsTree($result, $level); 428 array_pop($level); 429 } 430 } 431 432 /** 433 * @param array $result 434 * @param array $level 435 */ 436 protected function flattenNoticesTree(array &$result, array $level): void 437 { 438 if (count($this->notices) > 0) { 439 $result[implode('.', $level)] = $this->notices; 440 } 441 foreach ($this->propertyResults as $subPropertyName => $subResult) { 442 $level[] = $subPropertyName; 443 $subResult->flattenNoticesTree($result, $level); 444 array_pop($level); 445 } 446 } 447 448 /** 449 * Merge the given Result object into this one. 450 * 451 * @param Result $otherResult 452 */ 453 public function merge(Result $otherResult): void 454 { 455 if ($otherResult->errorsExist) { 456 $this->mergeProperty($otherResult, 'getErrors', 'addError'); 457 } 458 if ($otherResult->warningsExist) { 459 $this->mergeProperty($otherResult, 'getWarnings', 'addWarning'); 460 } 461 if ($otherResult->noticesExist) { 462 $this->mergeProperty($otherResult, 'getNotices', 'addNotice'); 463 } 464 465 foreach ($otherResult->getSubResults() as $subPropertyName => $subResult) { 466 /** @var Result $subResult */ 467 if (array_key_exists($subPropertyName, $this->propertyResults) && $this->propertyResults[$subPropertyName]->hasMessages()) { 468 $this->forProperty($subPropertyName)->merge($subResult); 469 } else { 470 $this->propertyResults[$subPropertyName] = $subResult; 471 $subResult->setParent($this); 472 } 473 } 474 } 475 476 /** 477 * Merge a single property from the other result object. 478 * 479 * @param Result $otherResult 480 * @param string $getterName 481 * @param string $adderName 482 */ 483 protected function mergeProperty(Result $otherResult, string $getterName, string $adderName): void 484 { 485 $getter = [$otherResult, $getterName]; 486 $adder = [$this, $adderName]; 487 488 if (!is_callable($getter) || !is_callable($adder)) { 489 return; 490 } 491 492 foreach ($getter() as $messageInOtherResult) { 493 $adder($messageInOtherResult); 494 } 495 } 496 497 /** 498 * Get a list of all sub Result objects available. 499 * 500 * @return Result[] 501 */ 502 public function getSubResults(): array 503 { 504 return $this->propertyResults; 505 } 506} 507