CSV validator library.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

315 lines
7.3KB

  1. <?php
  2. namespace Deblan\CsvValidator;
  3. use Deblan\Csv\CsvParser;
  4. use Symfony\Component\Validator\Constraint;
  5. use Symfony\Component\Validator\ConstraintViolationList;
  6. use Symfony\Component\Validator\Validator\RecursiveValidator;
  7. use Symfony\Component\Validator\ConstraintViolation;
  8. use Symfony\Component\Validator\Validation;
  9. /**
  10. * Class Validator.
  11. *
  12. * @author Simon Vieille <simon@deblan.fr>
  13. */
  14. class Validator
  15. {
  16. /**
  17. * @var CsvParser
  18. */
  19. protected $parser;
  20. /**
  21. * @var RecursiveValidator
  22. */
  23. protected $validator;
  24. /**
  25. * @var array
  26. */
  27. protected $fieldConstraints = [];
  28. /**
  29. * @var array
  30. */
  31. protected $dataConstraints = [];
  32. /**
  33. * @var bool
  34. */
  35. protected $hasValidate = false;
  36. /**
  37. * @var array
  38. */
  39. protected $errors = [];
  40. /**
  41. * @var array
  42. */
  43. protected $expectedHeaders = [];
  44. /**
  45. * Constructor.
  46. *
  47. * @param RecursiveValidator $validator
  48. */
  49. public function __construct(RecursiveValidator $validator = null)
  50. {
  51. if ($validator === null) {
  52. $validator = Validation::createValidator();
  53. }
  54. $this->validator = $validator;
  55. }
  56. /**
  57. * Append a constraint to a specific column.
  58. *
  59. * @param int $key The column number
  60. * @param Constraint $constraint The constraint
  61. *
  62. * @return Validator
  63. */
  64. public function addFieldConstraint($key, Constraint $constraint)
  65. {
  66. if (!array_key_exists($key, $this->fieldConstraints)) {
  67. $this->fieldConstraints[$key] = [];
  68. }
  69. $this->fieldConstraints[$key][] = $constraint;
  70. return $this;
  71. }
  72. /**
  73. * Append a constraint to a specific line.
  74. *
  75. * @param Constraint $constraint The constraint
  76. *
  77. * @return Validator
  78. */
  79. public function addDataConstraint(Constraint $constraint)
  80. {
  81. $this->dataConstraints[] = $constraint;
  82. return $this;
  83. }
  84. /**
  85. * Set the expected legend.
  86. *
  87. * @param array $legend Expected legend
  88. *
  89. * @return Validator
  90. */
  91. public function setExpectedHeaders(array $legend)
  92. {
  93. $this->expectedHeaders = $legend;
  94. return $this;
  95. }
  96. /**
  97. * Run the validation.
  98. *
  99. * @param CsvParser $parser
  100. */
  101. public function validate(CsvParser $parser)
  102. {
  103. if ($this->parser !== $parser) {
  104. $this->parser = $parser;
  105. $this->errors = [];
  106. } elseif ($this->hasValidate) {
  107. return;
  108. }
  109. $this->validateHeaders();
  110. $this->validateDatas();
  111. $this->validateFields();
  112. $this->hasValidate = true;
  113. }
  114. /**
  115. * Validates the legend.
  116. */
  117. protected function validateHeaders()
  118. {
  119. if (!$this->parser->getHasHeaders()) {
  120. return;
  121. }
  122. if (empty($this->expectedHeaders)) {
  123. return;
  124. }
  125. if ($this->parser->getHeaders() !== $this->expectedHeaders) {
  126. $this->mergeErrorMessage('Invalid legend.', 1);
  127. }
  128. }
  129. /**
  130. * Validates datas.
  131. */
  132. protected function validateDatas()
  133. {
  134. if (empty($this->dataConstraints)) {
  135. return;
  136. }
  137. foreach ($this->parser->getDatas() as $line => $data) {
  138. foreach ($this->dataConstraints as $constraint) {
  139. $violations = $this->validator->validate($data, $constraint);
  140. $this->mergeViolationsMessages($violations, $this->getTrueLine($line));
  141. }
  142. }
  143. }
  144. /**
  145. * Validates fields.
  146. */
  147. protected function validateFields()
  148. {
  149. if (empty($this->fieldConstraints)) {
  150. return;
  151. }
  152. foreach ($this->parser->getDatas() as $line => $data) {
  153. foreach ($this->fieldConstraints as $key => $constraints) {
  154. if (!isset($data[$key])) {
  155. $column = $this->getTrueColunm($key);
  156. $this->mergeErrorMessage(
  157. sprintf('Field "%s" does not exist.', $column),
  158. $this->getTrueLine($line),
  159. $column
  160. );
  161. } else {
  162. foreach ($constraints as $constraint) {
  163. $violations = $this->validator->validate($data[$key], $constraint);
  164. $this->mergeViolationsMessages(
  165. $violations,
  166. $this->getTrueLine($line),
  167. $this->getTrueColunm($key)
  168. );
  169. }
  170. }
  171. }
  172. }
  173. }
  174. /**
  175. * Add violations.
  176. *
  177. * @param ConstraintViolationList $violations
  178. * @param int $line The line of the violations
  179. * @param int|null $key The column of the violations
  180. */
  181. protected function mergeViolationsMessages(ConstraintViolationList $violations, $line, $key = null)
  182. {
  183. if (count($violations) === 0) {
  184. return;
  185. }
  186. foreach ($violations as $violation) {
  187. $this->errors[] = $this->generateViolation($line, $key, $violation);
  188. }
  189. }
  190. /**
  191. * Create and append a violation from a string error.
  192. *
  193. * @param string $message The error message
  194. * @param int $line The line of the violations
  195. * @param int|null $key The column of the violations
  196. */
  197. protected function mergeErrorMessage($message, $line, $key = null)
  198. {
  199. $violation = $this->generateConstraintViolation($message);
  200. $this->errors[] = $this->generateViolation($line, $key, $violation);
  201. }
  202. /**
  203. * Returns the validation status.
  204. *
  205. * @return bool
  206. * @throw RuntimeException No validation yet
  207. */
  208. public function isValid()
  209. {
  210. if (!$this->hasValidate) {
  211. throw new \RuntimeException('You must validate before.');
  212. }
  213. return empty($this->errors);
  214. }
  215. /**
  216. * Returns the errors.
  217. *
  218. * @return array
  219. */
  220. public function getErrors()
  221. {
  222. return $this->errors;
  223. }
  224. /**
  225. * Generate a ConstraintViolation.
  226. *
  227. * @param string $message The error message
  228. *
  229. * @return ConstraintViolation
  230. */
  231. protected function generateConstraintViolation($message)
  232. {
  233. return new ConstraintViolation($message, $message, [], null, '', null);
  234. }
  235. /**
  236. * Generate a Violation.
  237. *
  238. * @param string $message The error message
  239. * @param int $line The line of the violations
  240. * @param int|null $key The column of the violations
  241. *
  242. * @return Violation
  243. */
  244. protected function generateViolation($line, $key, ConstraintViolation $violation)
  245. {
  246. return new Violation($line, $key, $violation);
  247. }
  248. /**
  249. * Get the true line number of an error.
  250. *
  251. * @param int $line
  252. *
  253. * @return int
  254. */
  255. protected function getTrueLine($line)
  256. {
  257. if ($this->parser->getHasHeaders()) {
  258. ++$line;
  259. }
  260. return ++$line;
  261. }
  262. /**
  263. * Get the true culumn number of an error.
  264. *
  265. * @param int $key
  266. *
  267. * @return int
  268. */
  269. protected function getTrueColunm($key)
  270. {
  271. return ++$key;
  272. }
  273. }