NestedValidationException.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <?php
  2. /*
  3. * Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
  4. * SPDX-License-Identifier: MIT
  5. */
  6. declare(strict_types=1);
  7. namespace Respect\Validation\Exceptions;
  8. use IteratorAggregate;
  9. use RecursiveIteratorIterator;
  10. use SplObjectStorage;
  11. use function array_shift;
  12. use function count;
  13. use function current;
  14. use function implode;
  15. use function is_array;
  16. use function spl_object_hash;
  17. use function sprintf;
  18. use function str_repeat;
  19. use const PHP_EOL;
  20. /**
  21. * Exception for nested validations.
  22. *
  23. * This exception allows to have exceptions inside itself and providers methods
  24. * to handle them and to retrieve nested messages based on itself and its
  25. * children.
  26. *
  27. * @author Alexandre Gomes Gaigalas <alganet@gmail.com>
  28. * @author Henrique Moody <henriquemoody@gmail.com>
  29. * @author Jonathan Stewmon <jstewmon@rmn.com>
  30. * @author Wojciech Frącz <fraczwojciech@gmail.com>
  31. */
  32. class NestedValidationException extends ValidationException implements IteratorAggregate
  33. {
  34. /**
  35. * @var ValidationException[]
  36. */
  37. private $exceptions = [];
  38. /**
  39. * Returns the exceptions that are children of the exception.
  40. *
  41. * @return ValidationException[]
  42. */
  43. public function getChildren(): array
  44. {
  45. return $this->exceptions;
  46. }
  47. /**
  48. * Adds a child to the exception.
  49. */
  50. public function addChild(ValidationException $exception): self
  51. {
  52. $this->exceptions[spl_object_hash($exception)] = $exception;
  53. return $this;
  54. }
  55. /**
  56. * Adds children to the exception.
  57. *
  58. * @param ValidationException[] $exceptions
  59. */
  60. public function addChildren(array $exceptions): self
  61. {
  62. foreach ($exceptions as $exception) {
  63. $this->addChild($exception);
  64. }
  65. return $this;
  66. }
  67. /**
  68. * @return SplObjectStorage<ValidationException, int>
  69. */
  70. public function getIterator(): SplObjectStorage
  71. {
  72. /** @var SplObjectStorage<ValidationException, int> */
  73. $childrenExceptions = new SplObjectStorage();
  74. $recursiveIteratorIterator = $this->getRecursiveIterator();
  75. $lastDepth = 0;
  76. $lastDepthOriginal = 0;
  77. $knownDepths = [];
  78. foreach ($recursiveIteratorIterator as $childException) {
  79. if ($this->isOmissible($childException)) {
  80. continue;
  81. }
  82. $currentDepth = $lastDepth;
  83. $currentDepthOriginal = $recursiveIteratorIterator->getDepth() + 1;
  84. if (isset($knownDepths[$currentDepthOriginal])) {
  85. $currentDepth = $knownDepths[$currentDepthOriginal];
  86. } elseif ($currentDepthOriginal > $lastDepthOriginal) {
  87. ++$currentDepth;
  88. }
  89. if (!isset($knownDepths[$currentDepthOriginal])) {
  90. $knownDepths[$currentDepthOriginal] = $currentDepth;
  91. }
  92. $lastDepth = $currentDepth;
  93. $lastDepthOriginal = $currentDepthOriginal;
  94. $childrenExceptions->attach($childException, $currentDepth);
  95. }
  96. return $childrenExceptions;
  97. }
  98. /**
  99. * Returns a key->value array with all the messages of the exception.
  100. *
  101. * In this array the "keys" are the ids of the exceptions (defined name or
  102. * name of the rule) and the values are the message.
  103. *
  104. * Once templates are passed it overwrites the templates of the given
  105. * messages.
  106. *
  107. * @param string[]|string[][] $templates
  108. *
  109. * @return string[]
  110. */
  111. public function getMessages(array $templates = []): array
  112. {
  113. $messages = [$this->getId() => $this->renderMessage($this, $templates)];
  114. foreach ($this->getChildren() as $exception) {
  115. $id = $exception->getId();
  116. if (!$exception instanceof self) {
  117. $messages[$id] = $this->renderMessage(
  118. $exception,
  119. $this->findTemplates($templates, $this->getId())
  120. );
  121. continue;
  122. }
  123. $messages[$id] = $exception->getMessages($this->findTemplates($templates, $id, $this->getId()));
  124. if (count($messages[$id]) > 1) {
  125. continue;
  126. }
  127. $messages[$id] = current($messages[$exception->getId()]);
  128. }
  129. if (count($messages) > 1) {
  130. unset($messages[$this->getId()]);
  131. }
  132. return $messages;
  133. }
  134. /**
  135. * Returns a string with all the messages of the exception.
  136. */
  137. public function getFullMessage(): string
  138. {
  139. $messages = [];
  140. $leveler = 1;
  141. if (!$this->isOmissible($this)) {
  142. $leveler = 0;
  143. $messages[] = sprintf('- %s', $this->getMessage());
  144. }
  145. $exceptions = $this->getIterator();
  146. /** @var ValidationException $exception */
  147. foreach ($exceptions as $exception) {
  148. $messages[] = sprintf(
  149. '%s- %s',
  150. str_repeat(' ', (int) ($exceptions[$exception] - $leveler) * 2),
  151. $exception->getMessage()
  152. );
  153. }
  154. return implode(PHP_EOL, $messages);
  155. }
  156. /**
  157. * @param string[] $templates
  158. */
  159. protected function renderMessage(ValidationException $exception, array $templates): string
  160. {
  161. if (isset($templates[$exception->getId()])) {
  162. $exception->updateTemplate($templates[$exception->getId()]);
  163. }
  164. return $exception->getMessage();
  165. }
  166. /**
  167. * @param string[] $templates
  168. * @param mixed ...$ids
  169. *
  170. * @return string[]
  171. */
  172. protected function findTemplates(array $templates, ...$ids): array
  173. {
  174. while (count($ids) > 0) {
  175. $id = array_shift($ids);
  176. if (!isset($templates[$id])) {
  177. continue;
  178. }
  179. if (!is_array($templates[$id])) {
  180. continue;
  181. }
  182. $templates = $templates[$id];
  183. }
  184. return $templates;
  185. }
  186. /**
  187. * @return RecursiveIteratorIterator<RecursiveExceptionIterator>
  188. */
  189. private function getRecursiveIterator(): RecursiveIteratorIterator
  190. {
  191. return new RecursiveIteratorIterator(
  192. new RecursiveExceptionIterator($this),
  193. RecursiveIteratorIterator::SELF_FIRST
  194. );
  195. }
  196. private function isOmissible(Exception $exception): bool
  197. {
  198. if (!$exception instanceof self) {
  199. return false;
  200. }
  201. if (count($exception->getChildren()) !== 1) {
  202. return false;
  203. }
  204. /** @var ValidationException $childException */
  205. $childException = current($exception->getChildren());
  206. if ($childException->getMessage() === $exception->getMessage()) {
  207. return true;
  208. }
  209. if ($exception->hasCustomTemplate()) {
  210. return $childException->hasCustomTemplate();
  211. }
  212. return !$childException instanceof NonOmissibleException;
  213. }
  214. }