Validator.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. namespace Libs;
  3. /**
  4. * Validador de payloads de entrada.
  5. *
  6. * Padroniza as validações de input em todos os controllers, evitando
  7. * casting solto e verificações manuais espalhadas. Uso encadeado:
  8. *
  9. * $validator = (new Validator($body))
  10. * ->required('email')->email('email')
  11. * ->required('password')->minLength('password', 8);
  12. *
  13. * if ($validator->fails()) {
  14. * return Payload::fail($validator->firstError(), [], 'E_VALIDATE', 400);
  15. * }
  16. *
  17. * Regras só são aplicadas se o campo não já tiver acumulado erro, evitando
  18. * mensagens redundantes para o mesmo campo.
  19. */
  20. final class Validator
  21. {
  22. private array $data;
  23. /** @var array<string, string> erro por campo (primeiro erro encontrado) */
  24. private array $errors = [];
  25. public function __construct(array $data)
  26. {
  27. $this->data = $data;
  28. }
  29. public function required(string $field, ?string $label = null): self
  30. {
  31. if ($this->hasError($field)) {
  32. return $this;
  33. }
  34. $value = $this->data[$field] ?? null;
  35. if ($value === null || (is_string($value) && trim($value) === '') || $value === []) {
  36. $this->addError($field, sprintf('%s is required', $label ?? $field));
  37. }
  38. return $this;
  39. }
  40. public function email(string $field, ?string $label = null): self
  41. {
  42. if ($this->hasError($field) || !isset($this->data[$field])) {
  43. return $this;
  44. }
  45. $value = trim((string) $this->data[$field]);
  46. if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
  47. $this->addError($field, sprintf('%s must be a valid email', $label ?? $field));
  48. }
  49. return $this;
  50. }
  51. /**
  52. * Telefone aceita apenas dígitos (após remover espaços, +, -, () ),
  53. * com comprimento entre $min e $max.
  54. */
  55. public function phone(string $field, int $min = 8, int $max = 15, ?string $label = null): self
  56. {
  57. if ($this->hasError($field) || !isset($this->data[$field])) {
  58. return $this;
  59. }
  60. $digits = preg_replace('/\D+/', '', (string) $this->data[$field]);
  61. $length = strlen((string) $digits);
  62. if ($length < $min || $length > $max) {
  63. $this->addError($field, sprintf('%s must have between %d and %d digits', $label ?? $field, $min, $max));
  64. }
  65. return $this;
  66. }
  67. public function minLength(string $field, int $min, ?string $label = null): self
  68. {
  69. if ($this->hasError($field) || !isset($this->data[$field])) {
  70. return $this;
  71. }
  72. if (mb_strlen((string) $this->data[$field]) < $min) {
  73. $this->addError($field, sprintf('%s must be at least %d characters', $label ?? $field, $min));
  74. }
  75. return $this;
  76. }
  77. public function maxLength(string $field, int $max, ?string $label = null): self
  78. {
  79. if ($this->hasError($field) || !isset($this->data[$field])) {
  80. return $this;
  81. }
  82. if (mb_strlen((string) $this->data[$field]) > $max) {
  83. $this->addError($field, sprintf('%s must be at most %d characters', $label ?? $field, $max));
  84. }
  85. return $this;
  86. }
  87. /**
  88. * Garante que o valor é um inteiro dentro de [$min, $max] (limites opcionais).
  89. */
  90. public function intRange(string $field, ?int $min = null, ?int $max = null, ?string $label = null): self
  91. {
  92. if ($this->hasError($field) || !isset($this->data[$field])) {
  93. return $this;
  94. }
  95. $value = filter_var($this->data[$field], FILTER_VALIDATE_INT);
  96. if ($value === false) {
  97. $this->addError($field, sprintf('%s must be an integer', $label ?? $field));
  98. return $this;
  99. }
  100. if (($min !== null && $value < $min) || ($max !== null && $value > $max)) {
  101. $this->addError($field, sprintf('%s is out of allowed range', $label ?? $field));
  102. }
  103. return $this;
  104. }
  105. /**
  106. * Garante que o valor está em uma lista de valores permitidos.
  107. */
  108. public function in(string $field, array $allowed, ?string $label = null): self
  109. {
  110. if ($this->hasError($field) || !isset($this->data[$field])) {
  111. return $this;
  112. }
  113. if (!in_array($this->data[$field], $allowed, true)) {
  114. $this->addError($field, sprintf('%s has an invalid value', $label ?? $field));
  115. }
  116. return $this;
  117. }
  118. public function fails(): bool
  119. {
  120. return !empty($this->errors);
  121. }
  122. /**
  123. * @return array<string, string>
  124. */
  125. public function errors(): array
  126. {
  127. return $this->errors;
  128. }
  129. public function firstError(): ?string
  130. {
  131. if (empty($this->errors)) {
  132. return null;
  133. }
  134. return reset($this->errors);
  135. }
  136. private function hasError(string $field): bool
  137. {
  138. return isset($this->errors[$field]);
  139. }
  140. private function addError(string $field, string $message): void
  141. {
  142. if (!isset($this->errors[$field])) {
  143. $this->errors[$field] = $message;
  144. }
  145. }
  146. }