FormatsMessages.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. <?php
  2. namespace Illuminate\Validation\Concerns;
  3. use Closure;
  4. use Illuminate\Support\Arr;
  5. use Illuminate\Support\Number;
  6. use Illuminate\Support\Str;
  7. use Symfony\Component\HttpFoundation\File\File;
  8. use Symfony\Component\HttpFoundation\File\UploadedFile;
  9. trait FormatsMessages
  10. {
  11. use ReplacesAttributes;
  12. /**
  13. * Get the validation message for an attribute and rule.
  14. *
  15. * @param string $attribute
  16. * @param string $rule
  17. * @return string
  18. */
  19. protected function getMessage($attribute, $rule)
  20. {
  21. $attributeWithPlaceholders = $attribute;
  22. $attribute = $this->replacePlaceholderInString($attribute);
  23. $inlineMessage = $this->getInlineMessage($attribute, $rule);
  24. // First we will retrieve the custom message for the validation rule if one
  25. // exists. If a custom validation message is being used we'll return the
  26. // custom message, otherwise we'll keep searching for a valid message.
  27. if (! is_null($inlineMessage)) {
  28. return $inlineMessage;
  29. }
  30. $lowerRule = Str::snake($rule);
  31. $customKey = "validation.custom.{$attribute}.{$lowerRule}";
  32. $customMessage = $this->getCustomMessageFromTranslator(
  33. in_array($rule, $this->sizeRules)
  34. ? [$customKey.".{$this->getAttributeType($attribute)}", $customKey]
  35. : $customKey
  36. );
  37. // First we check for a custom defined validation message for the attribute
  38. // and rule. This allows the developer to specify specific messages for
  39. // only some attributes and rules that need to get specially formed.
  40. if ($customMessage !== $customKey) {
  41. return $customMessage;
  42. }
  43. // If the rule being validated is a "size" rule, we will need to gather the
  44. // specific error message for the type of attribute being validated such
  45. // as a number, file or string which all have different message types.
  46. elseif (in_array($rule, $this->sizeRules)) {
  47. return $this->getSizeMessage($attributeWithPlaceholders, $rule);
  48. }
  49. // Finally, if no developer specified messages have been set, and no other
  50. // special messages apply for this rule, we will just pull the default
  51. // messages out of the translator service for this validation rule.
  52. $key = "validation.{$lowerRule}";
  53. if ($key !== ($value = $this->translator->get($key))) {
  54. return $value;
  55. }
  56. return $this->getFromLocalArray(
  57. $attribute, $lowerRule, $this->fallbackMessages
  58. ) ?: $key;
  59. }
  60. /**
  61. * Get the proper inline error message for standard and size rules.
  62. *
  63. * @param string $attribute
  64. * @param string $rule
  65. * @return string|null
  66. */
  67. protected function getInlineMessage($attribute, $rule)
  68. {
  69. $inlineEntry = $this->getFromLocalArray($attribute, Str::snake($rule));
  70. return is_array($inlineEntry) && in_array($rule, $this->sizeRules)
  71. ? ($inlineEntry[$this->getAttributeType($attribute)] ?? null)
  72. : $inlineEntry;
  73. }
  74. /**
  75. * Get the inline message for a rule if it exists.
  76. *
  77. * @param string $attribute
  78. * @param string $lowerRule
  79. * @param array|null $source
  80. * @return string|null
  81. */
  82. protected function getFromLocalArray($attribute, $lowerRule, $source = null)
  83. {
  84. $source = $source ?: $this->customMessages;
  85. $keys = ["{$attribute}.{$lowerRule}", $lowerRule, $attribute];
  86. if ($this->getAttributeType($attribute) !== 'file') {
  87. $shortRule = "{$attribute}.".Str::snake(class_basename($lowerRule));
  88. if (! in_array($shortRule, $keys)) {
  89. $keys[] = $shortRule;
  90. }
  91. }
  92. // First we will check for a custom message for an attribute specific rule
  93. // message for the fields, then we will check for a general custom line
  94. // that is not attribute specific. If we find either we'll return it.
  95. foreach ($keys as $key) {
  96. foreach (array_keys($source) as $sourceKey) {
  97. if (str_contains($sourceKey, '*')) {
  98. $pattern = str_replace('\*', '([^.]*)', preg_quote($sourceKey, '#'));
  99. if (preg_match('#^'.$pattern.'\z#u', $key) === 1) {
  100. $message = $source[$sourceKey];
  101. if (is_array($message)) {
  102. return $message[$lowerRule] ?? null;
  103. }
  104. return $message;
  105. }
  106. continue;
  107. }
  108. if (Str::is($sourceKey, $key)) {
  109. $message = $source[$sourceKey];
  110. if ($sourceKey === $attribute && is_array($message)) {
  111. return $message[$lowerRule] ?? null;
  112. }
  113. return $message;
  114. }
  115. }
  116. }
  117. }
  118. /**
  119. * Get the custom error message from the translator.
  120. *
  121. * @param array|string $keys
  122. * @return string
  123. */
  124. protected function getCustomMessageFromTranslator($keys)
  125. {
  126. foreach (Arr::wrap($keys) as $key) {
  127. if (($message = $this->translator->get($key)) !== $key) {
  128. return $message;
  129. }
  130. // If an exact match was not found for the key, we will collapse all of these
  131. // messages and loop through them and try to find a wildcard match for the
  132. // given key. Otherwise, we will simply return the key's value back out.
  133. $shortKey = preg_replace(
  134. '/^validation\.custom\./', '', $key
  135. );
  136. $message = $this->getWildcardCustomMessages(Arr::dot(
  137. (array) $this->translator->get('validation.custom')
  138. ), $shortKey, $key);
  139. if ($message !== $key) {
  140. return $message;
  141. }
  142. }
  143. return Arr::last(Arr::wrap($keys));
  144. }
  145. /**
  146. * Check the given messages for a wildcard key.
  147. *
  148. * @param array $messages
  149. * @param string $search
  150. * @param string $default
  151. * @return string
  152. */
  153. protected function getWildcardCustomMessages($messages, $search, $default)
  154. {
  155. foreach ($messages as $key => $message) {
  156. if ($search === $key || (Str::contains($key, ['*']) && Str::is($key, $search))) {
  157. return $message;
  158. }
  159. }
  160. return $default;
  161. }
  162. /**
  163. * Get the proper error message for an attribute and size rule.
  164. *
  165. * @param string $attribute
  166. * @param string $rule
  167. * @return string
  168. */
  169. protected function getSizeMessage($attribute, $rule)
  170. {
  171. $lowerRule = Str::snake($rule);
  172. // There are three different types of size validations. The attribute may be
  173. // either a number, file, or string so we will check a few things to know
  174. // which type of value it is and return the correct line for that type.
  175. $type = $this->getAttributeType($attribute);
  176. $key = "validation.{$lowerRule}.{$type}";
  177. return $this->translator->get($key);
  178. }
  179. /**
  180. * Get the data type of the given attribute.
  181. *
  182. * @param string $attribute
  183. * @return string
  184. */
  185. protected function getAttributeType($attribute)
  186. {
  187. // We assume that the attributes present in the file array are files so that
  188. // means that if the attribute does not have a numeric rule and the files
  189. // list doesn't have it we'll just consider it a string by elimination.
  190. return match (true) {
  191. $this->hasRule($attribute, $this->numericRules) => 'numeric',
  192. $this->hasRule($attribute, ['Array', 'List']) => 'array',
  193. $this->getValue($attribute) instanceof UploadedFile,
  194. $this->getValue($attribute) instanceof File => 'file',
  195. default => 'string',
  196. };
  197. }
  198. /**
  199. * Replace all error message place-holders with actual values.
  200. *
  201. * @param string $message
  202. * @param string $attribute
  203. * @param string $rule
  204. * @param array $parameters
  205. * @return string
  206. */
  207. public function makeReplacements($message, $attribute, $rule, $parameters)
  208. {
  209. $message = $this->replaceAttributePlaceholder(
  210. $message, $this->getDisplayableAttribute($attribute)
  211. );
  212. $message = $this->replaceInputPlaceholder($message, $attribute);
  213. $message = $this->replaceIndexPlaceholder($message, $attribute);
  214. $message = $this->replacePositionPlaceholder($message, $attribute);
  215. $message = $this->replaceOrdinalPositionPlaceholder($message, $attribute);
  216. if (isset($this->replacers[Str::snake($rule)])) {
  217. return $this->callReplacer($message, $attribute, Str::snake($rule), $parameters, $this);
  218. } elseif (method_exists($this, $replacer = "replace{$rule}")) {
  219. return $this->$replacer($message, $attribute, $rule, $parameters);
  220. }
  221. return $message;
  222. }
  223. /**
  224. * Get the displayable name of the attribute.
  225. *
  226. * @param string $attribute
  227. * @return string
  228. */
  229. public function getDisplayableAttribute($attribute)
  230. {
  231. $primaryAttribute = $this->getPrimaryAttribute($attribute);
  232. $expectedAttributes = $attribute != $primaryAttribute
  233. ? [$attribute, $primaryAttribute]
  234. : [$attribute];
  235. foreach ($expectedAttributes as $name) {
  236. // The developer may dynamically specify the array of custom attributes on this
  237. // validator instance. If the attribute exists in this array it is used over
  238. // the other ways of pulling the attribute name for this given attributes.
  239. if ($inlineAttribute = $this->getAttributeFromLocalArray($name)) {
  240. return $inlineAttribute;
  241. }
  242. // We allow for a developer to specify language lines for any attribute in this
  243. // application, which allows flexibility for displaying a unique displayable
  244. // version of the attribute name instead of the name used in an HTTP POST.
  245. if ($translatedAttribute = $this->getAttributeFromTranslations($name)) {
  246. return $translatedAttribute;
  247. }
  248. }
  249. // When no language line has been specified for the attribute and it is also
  250. // an implicit attribute we will display the raw attribute's name and not
  251. // modify it with any of these replacements before we display the name.
  252. if (isset($this->implicitAttributes[$primaryAttribute])) {
  253. return ($formatter = $this->implicitAttributesFormatter)
  254. ? $formatter($attribute)
  255. : $attribute;
  256. }
  257. return str_replace('_', ' ', Str::snake($attribute));
  258. }
  259. /**
  260. * Get the given attribute from the attribute translations.
  261. *
  262. * @param string $name
  263. * @return string|null
  264. */
  265. protected function getAttributeFromTranslations($name)
  266. {
  267. if (! is_array($attributes = $this->translator->get('validation.attributes'))) {
  268. return null;
  269. }
  270. return $this->getAttributeFromLocalArray($name, Arr::dot($attributes));
  271. }
  272. /**
  273. * Get the custom name for an attribute if it exists in the given array.
  274. *
  275. * @param string $attribute
  276. * @param array|null $source
  277. * @return string|null
  278. */
  279. protected function getAttributeFromLocalArray($attribute, $source = null)
  280. {
  281. $source = $source ?: $this->customAttributes;
  282. if (isset($source[$attribute])) {
  283. return $source[$attribute];
  284. }
  285. foreach (array_keys($source) as $sourceKey) {
  286. if (str_contains($sourceKey, '*')) {
  287. $pattern = str_replace('\*', '([^.]*)', preg_quote($sourceKey, '#'));
  288. if (preg_match('#^'.$pattern.'\z#u', $attribute) === 1) {
  289. return $source[$sourceKey];
  290. }
  291. }
  292. }
  293. }
  294. /**
  295. * Replace the :attribute placeholder in the given message.
  296. *
  297. * @param string $message
  298. * @param string $value
  299. * @return string
  300. */
  301. protected function replaceAttributePlaceholder($message, $value)
  302. {
  303. return str_replace(
  304. [':attribute', ':ATTRIBUTE', ':Attribute'],
  305. [$value, Str::upper($value), Str::ucfirst($value)],
  306. $message
  307. );
  308. }
  309. /**
  310. * Replace the :index placeholder in the given message.
  311. *
  312. * @param string $message
  313. * @param string $attribute
  314. * @return string
  315. */
  316. protected function replaceIndexPlaceholder($message, $attribute)
  317. {
  318. return $this->replaceIndexOrPositionPlaceholder(
  319. $message, $attribute, 'index'
  320. );
  321. }
  322. /**
  323. * Replace the :position placeholder in the given message.
  324. *
  325. * @param string $message
  326. * @param string $attribute
  327. * @return string
  328. */
  329. protected function replacePositionPlaceholder($message, $attribute)
  330. {
  331. return $this->replaceIndexOrPositionPlaceholder(
  332. $message, $attribute, 'position', fn ($segment) => $segment + 1
  333. );
  334. }
  335. /**
  336. * Replace the :ordinal-position placeholder in the given message.
  337. *
  338. * @param string $message
  339. * @param string $attribute
  340. * @return string
  341. */
  342. protected function replaceOrdinalPositionPlaceholder($message, $attribute)
  343. {
  344. if (! extension_loaded('intl')) {
  345. return $message;
  346. }
  347. return $this->replaceIndexOrPositionPlaceholder(
  348. $message, $attribute, 'ordinal-position', fn ($segment) => Number::ordinal($segment + 1)
  349. );
  350. }
  351. /**
  352. * Replace the :index or :position placeholder in the given message.
  353. *
  354. * @param string $message
  355. * @param string $attribute
  356. * @param string $placeholder
  357. * @param \Closure|null $modifier
  358. * @return string
  359. */
  360. protected function replaceIndexOrPositionPlaceholder($message, $attribute, $placeholder, ?Closure $modifier = null)
  361. {
  362. if (! str_contains(strtolower($message), ':'.$placeholder) &&
  363. ! str_contains(strtolower($message), '-'.$placeholder)) {
  364. return $message;
  365. }
  366. $segments = explode('.', $attribute);
  367. $modifier ??= fn ($value) => $value;
  368. $numericIndex = 1;
  369. foreach ($segments as $segment) {
  370. if (is_numeric($segment)) {
  371. if ($numericIndex === 1) {
  372. $message = str_ireplace(':'.$placeholder, $modifier((int) $segment), $message);
  373. }
  374. $message = str_ireplace(
  375. ':'.$this->numberToIndexOrPositionWord($numericIndex).'-'.$placeholder,
  376. $modifier((int) $segment),
  377. $message
  378. );
  379. $numericIndex++;
  380. }
  381. }
  382. return $message;
  383. }
  384. /**
  385. * Get the word for a index or position segment.
  386. *
  387. * @param int $value
  388. * @return string
  389. */
  390. protected function numberToIndexOrPositionWord(int $value)
  391. {
  392. return [
  393. 1 => 'first',
  394. 2 => 'second',
  395. 3 => 'third',
  396. 4 => 'fourth',
  397. 5 => 'fifth',
  398. 6 => 'sixth',
  399. 7 => 'seventh',
  400. 8 => 'eighth',
  401. 9 => 'ninth',
  402. 10 => 'tenth',
  403. ][(int) $value] ?? 'other';
  404. }
  405. /**
  406. * Replace the :input placeholder in the given message.
  407. *
  408. * @param string $message
  409. * @param string $attribute
  410. * @return string
  411. */
  412. protected function replaceInputPlaceholder($message, $attribute)
  413. {
  414. if (! str_contains($message, ':input')) {
  415. return $message;
  416. }
  417. $actualValue = $this->getValue($attribute);
  418. if (is_scalar($actualValue) || is_null($actualValue)) {
  419. $message = str_replace(':input', $this->getDisplayableValue($attribute, $actualValue), $message);
  420. }
  421. return $message;
  422. }
  423. /**
  424. * Get the displayable name of the value.
  425. *
  426. * @param string $attribute
  427. * @param mixed $value
  428. * @return string
  429. */
  430. public function getDisplayableValue($attribute, $value)
  431. {
  432. if (isset($this->customValues[$attribute][$value])) {
  433. return $this->customValues[$attribute][$value];
  434. }
  435. if (is_array($value)) {
  436. return 'array';
  437. }
  438. $key = "validation.values.{$attribute}.{$value}";
  439. if (($line = $this->translator->get($key)) !== $key) {
  440. return $line;
  441. }
  442. if (is_bool($value)) {
  443. return $value ? 'true' : 'false';
  444. }
  445. if (is_null($value)) {
  446. return 'empty';
  447. }
  448. return (string) $value;
  449. }
  450. /**
  451. * Transform an array of attributes to their displayable form.
  452. *
  453. * @param array $values
  454. * @return array
  455. */
  456. protected function getAttributeList(array $values)
  457. {
  458. $attributes = [];
  459. // For each attribute in the list we will simply get its displayable form as
  460. // this is convenient when replacing lists of parameters like some of the
  461. // replacement functions do when formatting out the validation message.
  462. foreach ($values as $key => $value) {
  463. $attributes[$key] = $this->getDisplayableAttribute($value);
  464. }
  465. return $attributes;
  466. }
  467. /**
  468. * Call a custom validator message replacer.
  469. *
  470. * @param string $message
  471. * @param string $attribute
  472. * @param string $rule
  473. * @param array $parameters
  474. * @param \Illuminate\Validation\Validator $validator
  475. * @return string|null
  476. */
  477. protected function callReplacer($message, $attribute, $rule, $parameters, $validator)
  478. {
  479. $callback = $this->replacers[$rule];
  480. if ($callback instanceof Closure) {
  481. return $callback(...func_get_args());
  482. } elseif (is_string($callback)) {
  483. return $this->callClassBasedReplacer($callback, $message, $attribute, $rule, $parameters, $validator);
  484. }
  485. }
  486. /**
  487. * Call a class based validator message replacer.
  488. *
  489. * @param string $callback
  490. * @param string $message
  491. * @param string $attribute
  492. * @param string $rule
  493. * @param array $parameters
  494. * @param \Illuminate\Validation\Validator $validator
  495. * @return string
  496. */
  497. protected function callClassBasedReplacer($callback, $message, $attribute, $rule, $parameters, $validator)
  498. {
  499. [$class, $method] = Str::parseCallback($callback, 'replace');
  500. return $this->container->make($class)->{$method}(...array_slice(func_get_args(), 1));
  501. }
  502. }