vendor/symfony/validator/Constraints/BicValidator.php line 61

  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\Intl\Countries;
  12. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  13. use Symfony\Component\PropertyAccess\PropertyAccess;
  14. use Symfony\Component\PropertyAccess\PropertyAccessor;
  15. use Symfony\Component\Validator\Constraint;
  16. use Symfony\Component\Validator\ConstraintValidator;
  17. use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
  18. use Symfony\Component\Validator\Exception\LogicException;
  19. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  20. use Symfony\Component\Validator\Exception\UnexpectedValueException;
  21. /**
  22.  * @author Michael Hirschler <michael.vhirsch@gmail.com>
  23.  *
  24.  * @see https://en.wikipedia.org/wiki/ISO_9362#Structure
  25.  */
  26. class BicValidator extends ConstraintValidator
  27. {
  28.     // Reference: https://www.iban.com/structure
  29.     private const BIC_COUNTRY_TO_IBAN_COUNTRY_MAP = [
  30.         // FR includes:
  31.         'GF' => 'FR'// French Guiana
  32.         'PF' => 'FR'// French Polynesia
  33.         'TF' => 'FR'// French Southern Territories
  34.         'GP' => 'FR'// Guadeloupe
  35.         'MQ' => 'FR'// Martinique
  36.         'YT' => 'FR'// Mayotte
  37.         'NC' => 'FR'// New Caledonia
  38.         'RE' => 'FR'// Reunion
  39.         'BL' => 'FR'// Saint Barthelemy
  40.         'MF' => 'FR'// Saint Martin (French part)
  41.         'PM' => 'FR'// Saint Pierre and Miquelon
  42.         'WF' => 'FR'// Wallis and Futuna Islands
  43.         // GB includes:
  44.         'JE' => 'GB'// Jersey
  45.         'IM' => 'GB'// Isle of Man
  46.         'GG' => 'GB'// Guernsey
  47.         'VG' => 'GB'// British Virgin Islands
  48.         // FI includes:
  49.         'AX' => 'FI'// Aland Islands
  50.         // ES includes:
  51.         'IC' => 'ES'// Canary Islands
  52.         'EA' => 'ES'// Ceuta and Melilla
  53.     ];
  54.     private ?PropertyAccessor $propertyAccessor;
  55.     public function __construct(PropertyAccessor $propertyAccessor null)
  56.     {
  57.         $this->propertyAccessor $propertyAccessor;
  58.     }
  59.     public function validate(mixed $valueConstraint $constraint)
  60.     {
  61.         if (!$constraint instanceof Bic) {
  62.             throw new UnexpectedTypeException($constraintBic::class);
  63.         }
  64.         if (null === $value || '' === $value) {
  65.             return;
  66.         }
  67.         if (!\is_scalar($value) && !$value instanceof \Stringable) {
  68.             throw new UnexpectedValueException($value'string');
  69.         }
  70.         $canonicalize str_replace(' '''$value);
  71.         // the bic must be either 8 or 11 characters long
  72.         if (!\in_array(\strlen($canonicalize), [811])) {
  73.             $this->context->buildViolation($constraint->message)
  74.                 ->setParameter('{{ value }}'$this->formatValue($value))
  75.                 ->setCode(Bic::INVALID_LENGTH_ERROR)
  76.                 ->addViolation();
  77.             return;
  78.         }
  79.         // must contain alphanumeric values only
  80.         if (!ctype_alnum($canonicalize)) {
  81.             $this->context->buildViolation($constraint->message)
  82.                 ->setParameter('{{ value }}'$this->formatValue($value))
  83.                 ->setCode(Bic::INVALID_CHARACTERS_ERROR)
  84.                 ->addViolation();
  85.             return;
  86.         }
  87.         // first 4 letters must be alphabetic (bank code)
  88.         if (!ctype_alpha(substr($canonicalize04))) {
  89.             $this->context->buildViolation($constraint->message)
  90.                 ->setParameter('{{ value }}'$this->formatValue($value))
  91.                 ->setCode(Bic::INVALID_BANK_CODE_ERROR)
  92.                 ->addViolation();
  93.             return;
  94.         }
  95.         $bicCountryCode substr($canonicalize42);
  96.         if (!isset(self::BIC_COUNTRY_TO_IBAN_COUNTRY_MAP[$bicCountryCode]) && !Countries::exists($bicCountryCode)) {
  97.             $this->context->buildViolation($constraint->message)
  98.                 ->setParameter('{{ value }}'$this->formatValue($value))
  99.                 ->setCode(Bic::INVALID_COUNTRY_CODE_ERROR)
  100.                 ->addViolation();
  101.             return;
  102.         }
  103.         // should contain uppercase characters only
  104.         if (strtoupper($canonicalize) !== $canonicalize) {
  105.             $this->context->buildViolation($constraint->message)
  106.                 ->setParameter('{{ value }}'$this->formatValue($value))
  107.                 ->setCode(Bic::INVALID_CASE_ERROR)
  108.                 ->addViolation();
  109.             return;
  110.         }
  111.         // check against an IBAN
  112.         $iban $constraint->iban;
  113.         $path $constraint->ibanPropertyPath;
  114.         if ($path && null !== $object $this->context->getObject()) {
  115.             try {
  116.                 $iban $this->getPropertyAccessor()->getValue($object$path);
  117.             } catch (NoSuchPropertyException $e) {
  118.                 throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: '$pathget_debug_type($constraint)).$e->getMessage(), 0$e);
  119.             }
  120.         }
  121.         if (!$iban) {
  122.             return;
  123.         }
  124.         $ibanCountryCode substr($iban02);
  125.         if (ctype_alpha($ibanCountryCode) && !$this->bicAndIbanCountriesMatch($bicCountryCode$ibanCountryCode)) {
  126.             $this->context->buildViolation($constraint->ibanMessage)
  127.                 ->setParameter('{{ value }}'$this->formatValue($value))
  128.                 ->setParameter('{{ iban }}'$iban)
  129.                 ->setCode(Bic::INVALID_IBAN_COUNTRY_CODE_ERROR)
  130.                 ->addViolation();
  131.         }
  132.     }
  133.     private function getPropertyAccessor(): PropertyAccessor
  134.     {
  135.         if (null === $this->propertyAccessor) {
  136.             if (!class_exists(PropertyAccess::class)) {
  137.                 throw new LogicException('Unable to use property path as the Symfony PropertyAccess component is not installed.');
  138.             }
  139.             $this->propertyAccessor PropertyAccess::createPropertyAccessor();
  140.         }
  141.         return $this->propertyAccessor;
  142.     }
  143.     private function bicAndIbanCountriesMatch(string $bicCountryCodestring $ibanCountryCode): bool
  144.     {
  145.         return $ibanCountryCode === $bicCountryCode || $ibanCountryCode === (self::BIC_COUNTRY_TO_IBAN_COUNTRY_MAP[$bicCountryCode] ?? null);
  146.     }
  147. }