IsbnValidator.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Validator\Constraints;
  11. use Symfony\Component\Validator\Constraint;
  12. use Symfony\Component\Validator\ConstraintValidator;
  13. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  14. use Symfony\Component\Validator\Exception\UnexpectedValueException;
  15. /**
  16. * Validates whether the value is a valid ISBN-10 or ISBN-13.
  17. *
  18. * @author The Whole Life To Learn <thewholelifetolearn@gmail.com>
  19. * @author Manuel Reinhard <manu@sprain.ch>
  20. * @author Bernhard Schussek <bschussek@gmail.com>
  21. *
  22. * @see https://en.wikipedia.org/wiki/Isbn
  23. */
  24. class IsbnValidator extends ConstraintValidator
  25. {
  26. /**
  27. * {@inheritdoc}
  28. */
  29. public function validate($value, Constraint $constraint)
  30. {
  31. if (!$constraint instanceof Isbn) {
  32. throw new UnexpectedTypeException($constraint, Isbn::class);
  33. }
  34. if (null === $value || '' === $value) {
  35. return;
  36. }
  37. if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
  38. throw new UnexpectedValueException($value, 'string');
  39. }
  40. $value = (string) $value;
  41. $canonical = str_replace('-', '', $value);
  42. // Explicitly validate against ISBN-10
  43. if (Isbn::ISBN_10 === $constraint->type) {
  44. if (true !== ($code = $this->validateIsbn10($canonical))) {
  45. $this->context->buildViolation($this->getMessage($constraint, $constraint->type))
  46. ->setParameter('{{ value }}', $this->formatValue($value))
  47. ->setCode($code)
  48. ->addViolation();
  49. }
  50. return;
  51. }
  52. // Explicitly validate against ISBN-13
  53. if (Isbn::ISBN_13 === $constraint->type) {
  54. if (true !== ($code = $this->validateIsbn13($canonical))) {
  55. $this->context->buildViolation($this->getMessage($constraint, $constraint->type))
  56. ->setParameter('{{ value }}', $this->formatValue($value))
  57. ->setCode($code)
  58. ->addViolation();
  59. }
  60. return;
  61. }
  62. // Try both ISBNs
  63. // First, try ISBN-10
  64. $code = $this->validateIsbn10($canonical);
  65. // The ISBN can only be an ISBN-13 if the value was too long for ISBN-10
  66. if (Isbn::TOO_LONG_ERROR === $code) {
  67. // Try ISBN-13 now
  68. $code = $this->validateIsbn13($canonical);
  69. // If too short, this means we have 11 or 12 digits
  70. if (Isbn::TOO_SHORT_ERROR === $code) {
  71. $code = Isbn::TYPE_NOT_RECOGNIZED_ERROR;
  72. }
  73. }
  74. if (true !== $code) {
  75. $this->context->buildViolation($this->getMessage($constraint))
  76. ->setParameter('{{ value }}', $this->formatValue($value))
  77. ->setCode($code)
  78. ->addViolation();
  79. }
  80. }
  81. protected function validateIsbn10($isbn)
  82. {
  83. // Choose an algorithm so that ERROR_INVALID_CHARACTERS is preferred
  84. // over ERROR_TOO_SHORT/ERROR_TOO_LONG
  85. // Otherwise "0-45122-5244" passes, but "0-45122_5244" reports
  86. // "too long"
  87. // Error priority:
  88. // 1. ERROR_INVALID_CHARACTERS
  89. // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG
  90. // 3. ERROR_CHECKSUM_FAILED
  91. $checkSum = 0;
  92. for ($i = 0; $i < 10; ++$i) {
  93. // If we test the length before the loop, we get an ERROR_TOO_SHORT
  94. // when actually an ERROR_INVALID_CHARACTERS is wanted, e.g. for
  95. // "0-45122_5244" (typo)
  96. if (!isset($isbn[$i])) {
  97. return Isbn::TOO_SHORT_ERROR;
  98. }
  99. if ('X' === $isbn[$i]) {
  100. $digit = 10;
  101. } elseif (ctype_digit($isbn[$i])) {
  102. $digit = $isbn[$i];
  103. } else {
  104. return Isbn::INVALID_CHARACTERS_ERROR;
  105. }
  106. $checkSum += $digit * (10 - $i);
  107. }
  108. if (isset($isbn[$i])) {
  109. return Isbn::TOO_LONG_ERROR;
  110. }
  111. return 0 === $checkSum % 11 ? true : Isbn::CHECKSUM_FAILED_ERROR;
  112. }
  113. protected function validateIsbn13($isbn)
  114. {
  115. // Error priority:
  116. // 1. ERROR_INVALID_CHARACTERS
  117. // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG
  118. // 3. ERROR_CHECKSUM_FAILED
  119. if (!ctype_digit($isbn)) {
  120. return Isbn::INVALID_CHARACTERS_ERROR;
  121. }
  122. $length = \strlen($isbn);
  123. if ($length < 13) {
  124. return Isbn::TOO_SHORT_ERROR;
  125. }
  126. if ($length > 13) {
  127. return Isbn::TOO_LONG_ERROR;
  128. }
  129. $checkSum = 0;
  130. for ($i = 0; $i < 13; $i += 2) {
  131. $checkSum += $isbn[$i];
  132. }
  133. for ($i = 1; $i < 12; $i += 2) {
  134. $checkSum += $isbn[$i]
  135. * 3;
  136. }
  137. return 0 === $checkSum % 10 ? true : Isbn::CHECKSUM_FAILED_ERROR;
  138. }
  139. protected function getMessage($constraint, $type = null)
  140. {
  141. if (null !== $constraint->message) {
  142. return $constraint->message;
  143. } elseif (Isbn::ISBN_10 === $type) {
  144. return $constraint->isbn10Message;
  145. } elseif (Isbn::ISBN_13 === $type) {
  146. return $constraint->isbn13Message;
  147. }
  148. return $constraint->bothIsbnMessage;
  149. }
  150. }