vendor/symfony/validator/Constraints/IsbnValidator.php line 169

  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.     public function validate(mixed $valueConstraint $constraint)
  27.     {
  28.         if (!$constraint instanceof Isbn) {
  29.             throw new UnexpectedTypeException($constraintIsbn::class);
  30.         }
  31.         if (null === $value || '' === $value) {
  32.             return;
  33.         }
  34.         if (!\is_scalar($value) && !$value instanceof \Stringable) {
  35.             throw new UnexpectedValueException($value'string');
  36.         }
  37.         $value = (string) $value;
  38.         $canonical str_replace('-'''$value);
  39.         // Explicitly validate against ISBN-10
  40.         if (Isbn::ISBN_10 === $constraint->type) {
  41.             if (true !== ($code $this->validateIsbn10($canonical))) {
  42.                 $this->context->buildViolation($this->getMessage($constraint$constraint->type))
  43.                     ->setParameter('{{ value }}'$this->formatValue($value))
  44.                     ->setCode($code)
  45.                     ->addViolation();
  46.             }
  47.             return;
  48.         }
  49.         // Explicitly validate against ISBN-13
  50.         if (Isbn::ISBN_13 === $constraint->type) {
  51.             if (true !== ($code $this->validateIsbn13($canonical))) {
  52.                 $this->context->buildViolation($this->getMessage($constraint$constraint->type))
  53.                     ->setParameter('{{ value }}'$this->formatValue($value))
  54.                     ->setCode($code)
  55.                     ->addViolation();
  56.             }
  57.             return;
  58.         }
  59.         // Try both ISBNs
  60.         // First, try ISBN-10
  61.         $code $this->validateIsbn10($canonical);
  62.         // The ISBN can only be an ISBN-13 if the value was too long for ISBN-10
  63.         if (Isbn::TOO_LONG_ERROR === $code) {
  64.             // Try ISBN-13 now
  65.             $code $this->validateIsbn13($canonical);
  66.             // If too short, this means we have 11 or 12 digits
  67.             if (Isbn::TOO_SHORT_ERROR === $code) {
  68.                 $code Isbn::TYPE_NOT_RECOGNIZED_ERROR;
  69.             }
  70.         }
  71.         if (true !== $code) {
  72.             $this->context->buildViolation($this->getMessage($constraint))
  73.                 ->setParameter('{{ value }}'$this->formatValue($value))
  74.                 ->setCode($code)
  75.                 ->addViolation();
  76.         }
  77.     }
  78.     protected function validateIsbn10(string $isbn)
  79.     {
  80.         // Choose an algorithm so that ERROR_INVALID_CHARACTERS is preferred
  81.         // over ERROR_TOO_SHORT/ERROR_TOO_LONG
  82.         // Otherwise "0-45122-5244" passes, but "0-45122_5244" reports
  83.         // "too long"
  84.         // Error priority:
  85.         // 1. ERROR_INVALID_CHARACTERS
  86.         // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG
  87.         // 3. ERROR_CHECKSUM_FAILED
  88.         $checkSum 0;
  89.         for ($i 0$i 10; ++$i) {
  90.             // If we test the length before the loop, we get an ERROR_TOO_SHORT
  91.             // when actually an ERROR_INVALID_CHARACTERS is wanted, e.g. for
  92.             // "0-45122_5244" (typo)
  93.             if (!isset($isbn[$i])) {
  94.                 return Isbn::TOO_SHORT_ERROR;
  95.             }
  96.             if ('X' === $isbn[$i]) {
  97.                 $digit 10;
  98.             } elseif (ctype_digit($isbn[$i])) {
  99.                 $digit $isbn[$i];
  100.             } else {
  101.                 return Isbn::INVALID_CHARACTERS_ERROR;
  102.             }
  103.             $checkSum += $digit * (10 $i);
  104.         }
  105.         if (isset($isbn[$i])) {
  106.             return Isbn::TOO_LONG_ERROR;
  107.         }
  108.         return === $checkSum 11 true Isbn::CHECKSUM_FAILED_ERROR;
  109.     }
  110.     protected function validateIsbn13(string $isbn)
  111.     {
  112.         // Error priority:
  113.         // 1. ERROR_INVALID_CHARACTERS
  114.         // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG
  115.         // 3. ERROR_CHECKSUM_FAILED
  116.         if (!ctype_digit($isbn)) {
  117.             return Isbn::INVALID_CHARACTERS_ERROR;
  118.         }
  119.         $length \strlen($isbn);
  120.         if ($length 13) {
  121.             return Isbn::TOO_SHORT_ERROR;
  122.         }
  123.         if ($length 13) {
  124.             return Isbn::TOO_LONG_ERROR;
  125.         }
  126.         $checkSum 0;
  127.         for ($i 0$i 13$i += 2) {
  128.             $checkSum += $isbn[$i];
  129.         }
  130.         for ($i 1$i 12$i += 2) {
  131.             $checkSum += $isbn[$i] * 3;
  132.         }
  133.         return === $checkSum 10 true Isbn::CHECKSUM_FAILED_ERROR;
  134.     }
  135.     protected function getMessage(Isbn $constraintstring $type null)
  136.     {
  137.         if (null !== $constraint->message) {
  138.             return $constraint->message;
  139.         } elseif (Isbn::ISBN_10 === $type) {
  140.             return $constraint->isbn10Message;
  141.         } elseif (Isbn::ISBN_13 === $type) {
  142.             return $constraint->isbn13Message;
  143.         }
  144.         return $constraint->bothIsbnMessage;
  145.     }
  146. }