Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Validator\Constraints; Chris@0: Chris@0: use Symfony\Component\Validator\Constraint; Chris@0: use Symfony\Component\Validator\ConstraintValidator; Chris@0: use Symfony\Component\Validator\Exception\UnexpectedTypeException; Chris@0: Chris@0: /** Chris@0: * Validates whether the value is a valid ISBN-10 or ISBN-13. Chris@0: * Chris@0: * @author The Whole Life To Learn Chris@0: * @author Manuel Reinhard Chris@0: * @author Bernhard Schussek Chris@0: * Chris@0: * @see https://en.wikipedia.org/wiki/Isbn Chris@0: */ Chris@0: class IsbnValidator extends ConstraintValidator Chris@0: { Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function validate($value, Constraint $constraint) Chris@0: { Chris@0: if (!$constraint instanceof Isbn) { Chris@0: throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Isbn'); Chris@0: } Chris@0: Chris@0: if (null === $value || '' === $value) { Chris@0: return; Chris@0: } Chris@0: Chris@17: if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { Chris@0: throw new UnexpectedTypeException($value, 'string'); Chris@0: } Chris@0: Chris@0: $value = (string) $value; Chris@0: $canonical = str_replace('-', '', $value); Chris@0: Chris@0: // Explicitly validate against ISBN-10 Chris@0: if ('isbn10' === $constraint->type) { Chris@0: if (true !== ($code = $this->validateIsbn10($canonical))) { Chris@0: $this->context->buildViolation($this->getMessage($constraint, $constraint->type)) Chris@0: ->setParameter('{{ value }}', $this->formatValue($value)) Chris@0: ->setCode($code) Chris@0: ->addViolation(); Chris@0: } Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // Explicitly validate against ISBN-13 Chris@0: if ('isbn13' === $constraint->type) { Chris@0: if (true !== ($code = $this->validateIsbn13($canonical))) { Chris@0: $this->context->buildViolation($this->getMessage($constraint, $constraint->type)) Chris@0: ->setParameter('{{ value }}', $this->formatValue($value)) Chris@0: ->setCode($code) Chris@0: ->addViolation(); Chris@0: } Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // Try both ISBNs Chris@0: Chris@0: // First, try ISBN-10 Chris@0: $code = $this->validateIsbn10($canonical); Chris@0: Chris@0: // The ISBN can only be an ISBN-13 if the value was too long for ISBN-10 Chris@0: if (Isbn::TOO_LONG_ERROR === $code) { Chris@0: // Try ISBN-13 now Chris@0: $code = $this->validateIsbn13($canonical); Chris@0: Chris@0: // If too short, this means we have 11 or 12 digits Chris@0: if (Isbn::TOO_SHORT_ERROR === $code) { Chris@0: $code = Isbn::TYPE_NOT_RECOGNIZED_ERROR; Chris@0: } Chris@0: } Chris@0: Chris@0: if (true !== $code) { Chris@0: $this->context->buildViolation($this->getMessage($constraint)) Chris@0: ->setParameter('{{ value }}', $this->formatValue($value)) Chris@0: ->setCode($code) Chris@0: ->addViolation(); Chris@0: } Chris@0: } Chris@0: Chris@0: protected function validateIsbn10($isbn) Chris@0: { Chris@0: // Choose an algorithm so that ERROR_INVALID_CHARACTERS is preferred Chris@0: // over ERROR_TOO_SHORT/ERROR_TOO_LONG Chris@0: // Otherwise "0-45122-5244" passes, but "0-45122_5244" reports Chris@0: // "too long" Chris@0: Chris@0: // Error priority: Chris@0: // 1. ERROR_INVALID_CHARACTERS Chris@0: // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG Chris@0: // 3. ERROR_CHECKSUM_FAILED Chris@0: Chris@0: $checkSum = 0; Chris@0: Chris@0: for ($i = 0; $i < 10; ++$i) { Chris@0: // If we test the length before the loop, we get an ERROR_TOO_SHORT Chris@0: // when actually an ERROR_INVALID_CHARACTERS is wanted, e.g. for Chris@0: // "0-45122_5244" (typo) Chris@0: if (!isset($isbn[$i])) { Chris@0: return Isbn::TOO_SHORT_ERROR; Chris@0: } Chris@0: Chris@0: if ('X' === $isbn[$i]) { Chris@0: $digit = 10; Chris@0: } elseif (ctype_digit($isbn[$i])) { Chris@0: $digit = $isbn[$i]; Chris@0: } else { Chris@0: return Isbn::INVALID_CHARACTERS_ERROR; Chris@0: } Chris@0: Chris@0: $checkSum += $digit * (10 - $i); Chris@0: } Chris@0: Chris@0: if (isset($isbn[$i])) { Chris@0: return Isbn::TOO_LONG_ERROR; Chris@0: } Chris@0: Chris@0: return 0 === $checkSum % 11 ? true : Isbn::CHECKSUM_FAILED_ERROR; Chris@0: } Chris@0: Chris@0: protected function validateIsbn13($isbn) Chris@0: { Chris@0: // Error priority: Chris@0: // 1. ERROR_INVALID_CHARACTERS Chris@0: // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG Chris@0: // 3. ERROR_CHECKSUM_FAILED Chris@0: Chris@0: if (!ctype_digit($isbn)) { Chris@0: return Isbn::INVALID_CHARACTERS_ERROR; Chris@0: } Chris@0: Chris@17: $length = \strlen($isbn); Chris@0: Chris@0: if ($length < 13) { Chris@0: return Isbn::TOO_SHORT_ERROR; Chris@0: } Chris@0: Chris@0: if ($length > 13) { Chris@0: return Isbn::TOO_LONG_ERROR; Chris@0: } Chris@0: Chris@0: $checkSum = 0; Chris@0: Chris@0: for ($i = 0; $i < 13; $i += 2) { Chris@0: $checkSum += $isbn[$i]; Chris@0: } Chris@0: Chris@0: for ($i = 1; $i < 12; $i += 2) { Chris@0: $checkSum += $isbn[$i] Chris@0: * 3; Chris@0: } Chris@0: Chris@0: return 0 === $checkSum % 10 ? true : Isbn::CHECKSUM_FAILED_ERROR; Chris@0: } Chris@0: Chris@0: protected function getMessage($constraint, $type = null) Chris@0: { Chris@0: if (null !== $constraint->message) { Chris@0: return $constraint->message; Chris@0: } elseif ('isbn10' === $type) { Chris@0: return $constraint->isbn10Message; Chris@0: } elseif ('isbn13' === $type) { Chris@0: return $constraint->isbn13Message; Chris@0: } Chris@0: Chris@0: return $constraint->bothIsbnMessage; Chris@0: } Chris@0: }