From 05871cb8476856fe486f9efe9ee151a93b368bed Mon Sep 17 00:00:00 2001 From: Matias Navarro Carter Date: Wed, 8 Aug 2018 02:51:48 -0400 Subject: [PATCH] Initial commit --- .gitignore | 3 + README.md | 28 ++++ composer.json | 33 +++++ phpunit.xml.dist | 25 ++++ src/Bridge/Doctrine/DBAL/Types/RutType.php | 60 ++++++++ src/Bridge/Symfony/Form/RutType.php | 27 ++++ src/Bridge/Symfony/Validator/IsValidRut.php | 13 ++ .../Symfony/Validator/IsValidRutValidator.php | 52 +++++++ src/Exception/InvalidRutException.php | 37 +++++ src/Rut/Rut.php | 131 ++++++++++++++++++ src/Util/Correlative.php | 56 ++++++++ src/Validator/ChainRutValidator.php | 51 +++++++ src/Validator/RutValidator.php | 35 +++++ src/Validator/SimpleRutValidator.php | 27 ++++ tests/Rut/RutTest.php | 66 +++++++++ tests/Validator/SimpleRutValidatorTest.php | 31 +++++ 16 files changed, 675 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/Bridge/Doctrine/DBAL/Types/RutType.php create mode 100644 src/Bridge/Symfony/Form/RutType.php create mode 100644 src/Bridge/Symfony/Validator/IsValidRut.php create mode 100644 src/Bridge/Symfony/Validator/IsValidRutValidator.php create mode 100644 src/Exception/InvalidRutException.php create mode 100644 src/Rut/Rut.php create mode 100644 src/Util/Correlative.php create mode 100644 src/Validator/ChainRutValidator.php create mode 100644 src/Validator/RutValidator.php create mode 100644 src/Validator/SimpleRutValidator.php create mode 100644 tests/Rut/RutTest.php create mode 100644 tests/Validator/SimpleRutValidatorTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..116f35f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor +composer.lock +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1832e57 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +Rut Chileno +=========== + +Esta librería implementa una clase Rut como un *value object* inmutable, incluyendo +una api de validación flexible y extendible. + +Además, posee un validador para `symfony/validator`, un *form type* para `symfony/form` +y un *type* para `doctrine/dbal`. + +Sólo es compatible con PHP 7.1 o superior. + +## ¿Cómo nació y por qué esta librería? +Esta libería nace de la necesidad de estandarizar una clase Rut común para todos mis proyectos +PHP. +Si bien es cierto, hay muchas liberías con implementaciones de Rut chilenos en PHP, +muchas de ellas tienen notorias deficiencias: + +1. No están testeadas unitariamente, +2. No separan bien responsabilidades, como la lógica de validación con la de instanciación. +3. No proveen validación extensible por medio de interfaces, limitando la validación +solo a ser algorítmica. +4. Están acopladas a un framework +5. No proveen herramientas ni integraciones con librerías de terceros. + +## ¿Por qué PHP 7.1? +El fin del soporte de PHP 5.6 será a fines de 2018. PHP 7.1 es una de las últimas +versiones estables, y me beneficio mucho de su sistema de tipado estricto en esta libería. + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..dba58c1 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "mnavarrocarter/chilean-rut", + "description": "PHP Rut Value Object with validation utilities, doctrine type, and other cool features.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Matias Navarro Carter", + "email": "mnavarro@option.cl" + } + ], + "minimum-stability": "stable", + "require": { + "php": "^7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.12", + "phpunit/phpunit": "^7.3", + "doctrine/dbal": "^2.5", + "symfony/form": "^3.4|^4.0", + "symfony/validator": "^3.4|^4.0" + }, + "autoload": { + "psr-4": { + "MNC\\ChileanRut\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "MNC\\ChileanRut\\Tests\\": "tests" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d723ff0 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./tests + + + + + + ./src + + + + \ No newline at end of file diff --git a/src/Bridge/Doctrine/DBAL/Types/RutType.php b/src/Bridge/Doctrine/DBAL/Types/RutType.php new file mode 100644 index 0000000..606bd83 --- /dev/null +++ b/src/Bridge/Doctrine/DBAL/Types/RutType.php @@ -0,0 +1,60 @@ + + */ +class RutType extends StringType +{ + public const NAME = 'rut'; + + /** + * @return string + */ + public function getName(): string + { + return self::NAME; + } + + /** + * @param mixed $value + * @param AbstractPlatform $platform + * @return mixed + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if (null === $value) { + return $value; + } + + if ($value instanceof Rut) { + return parent::convertToDatabaseValue($value->format(), $platform); + } + + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'Rut']); + } + + /** + * @param mixed $value + * @param AbstractPlatform $platform + * @return mixed + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + $value = parent::convertToPHPValue($value, $platform); + + if ($value === null || $value instanceof Rut) { + return $value; + } + + return new Rut($value); + } +} \ No newline at end of file diff --git a/src/Bridge/Symfony/Form/RutType.php b/src/Bridge/Symfony/Form/RutType.php new file mode 100644 index 0000000..50df4cd --- /dev/null +++ b/src/Bridge/Symfony/Form/RutType.php @@ -0,0 +1,27 @@ + + */ +class RutType extends TextType +{ + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Rut::class, + ]); + } +} \ No newline at end of file diff --git a/src/Bridge/Symfony/Validator/IsValidRut.php b/src/Bridge/Symfony/Validator/IsValidRut.php new file mode 100644 index 0000000..b96282b --- /dev/null +++ b/src/Bridge/Symfony/Validator/IsValidRut.php @@ -0,0 +1,13 @@ + + */ +class IsValidRutValidator extends ConstraintValidator +{ + /** + * @var RutValidator + */ + private $validator; + + public function __construct(RutValidator $validator = null) + { + $this->validator = $validator ?? new SimpleRutValidator(); + } + + /** + * @param mixed $value + * @param Constraint $constraint + */ + public function validate($value, Constraint $constraint): void + { + if (null === $value || '' === $value) { + return; + } + + if (!$value instanceof Rut) { + throw new UnexpectedTypeException($value, Rut::class); + } + + try { + $this->validator->validate($value); + } catch (InvalidRutException $exception) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value->format(Rut::FORMAT_CLEAR)) + ->addViolation(); + } + } +} \ No newline at end of file diff --git a/src/Exception/InvalidRutException.php b/src/Exception/InvalidRutException.php new file mode 100644 index 0000000..44426a0 --- /dev/null +++ b/src/Exception/InvalidRutException.php @@ -0,0 +1,37 @@ + + */ +class InvalidRutException extends \LogicException +{ + /** + * @var Rut + */ + private $rut; + + /** + * InvalidRutException constructor. + * @param Rut $rut + */ + public function __construct(Rut $rut) + { + $message = sprintf('Rut %s is not a valid rut.', $rut->format(Rut::FORMAT_READABLE)); + $this->rut = $rut; + parent::__construct($message); + } + + /** + * @return Rut + */ + public function getRut(): Rut + { + return $this->rut; + } +} \ No newline at end of file diff --git a/src/Rut/Rut.php b/src/Rut/Rut.php new file mode 100644 index 0000000..d93ab9c --- /dev/null +++ b/src/Rut/Rut.php @@ -0,0 +1,131 @@ + + */ +class Rut +{ + public const FORMAT_HYPHENED = 0; // 14533535-5 + public const FORMAT_CLEAR = 1; // 145335355 + public const FORMAT_READABLE = 2; // 14.533.535-5 + + /** + * @var string + */ + private $value; + /** + * @var string + */ + private $dv; + + /** + * Rut constructor. + * @param string $rut + * @param RutValidator|null $validator if provided validates the Rut. + */ + public function __construct(string $rut, RutValidator $validator = null) + { + $sanitized = $this->sanitize($rut); + $this->value = substr($sanitized, 0, -1); + $this->dv = $sanitized[\strlen($sanitized) - 1]; + + if (null !== $validator) { + $validator->validate($this); + } + } + + /** + * @param string $correlative + * @param string $verifierDigit + * @return Rut + */ + public static function fromParts(string $correlative, string $verifierDigit): Rut + { + return new self($correlative.$verifierDigit); + } + + /** + * @param string $rut + * @return Rut + */ + public static function fromString(string $rut): Rut + { + return new self($rut); + } + + /** + * @param Rut $rut + * @return bool + */ + public function isEqualTo(Rut $rut): bool + { + return $this->format() === $rut->format(); + } + + /** + * @param string $value + * @return string + */ + private function sanitize(string $value): string + { + $value = trim($value); + $value = strtoupper($value); + return str_replace(['.', ',', '-'], '', $value); + } + + /** + * @param int $format One of the FORMAT_ constants. + * @return string + */ + public function format(int $format = 0): string + { + switch ($format) { + case self::FORMAT_HYPHENED: + return $this->value . '-' . $this->dv; + break; + case self::FORMAT_CLEAR: + return $this->value . $this->dv; + break; + case self::FORMAT_READABLE: + return sprintf('%s-%s', number_format($this->value, 0, '', '.'), $this->dv); + break; + default: + throw new \InvalidArgumentException( + sprintf( + 'Argument provided for %s method of class %s is invalid.', + __METHOD__, + __CLASS__ + ) + ); + } + } + + /** + * @return string + */ + public function getCorrelative(): string + { + return $this->value; + } + + /** + * @return string + */ + public function getVerifierDigit(): string + { + return $this->dv; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->format(self::FORMAT_READABLE); + } +} \ No newline at end of file diff --git a/src/Util/Correlative.php b/src/Util/Correlative.php new file mode 100644 index 0000000..7c24089 --- /dev/null +++ b/src/Util/Correlative.php @@ -0,0 +1,56 @@ + + */ +class Correlative +{ + /** + * Finds the verifier digit of a correlative. + * + * @param string $correlative + * @return string + */ + public static function findVerifierDigit(string $correlative): string + { + $x = 2; + $s = 0; + + for ($i = \strlen($correlative) - 1; $i >= 0; $i--) { + if ($x > 7) { + $x = 2; + } + $s += $correlative[$i] * $x; + $x++; + } + + $dv = 11 - ($s % 11); + + if ($dv === 10) { + $dv = 'K'; + } + + if ($dv === 11) { + $dv = '0'; + } + + return (string) $dv; + } + + /** + * Instantiates a valid Rut object just providing a correlative. + * + * @param string $correlative + * @return Rut + */ + public static function createValidRutOnlyFromCorrelative(string $correlative): Rut + { + return Rut::fromParts($correlative, static::findVerifierDigit($correlative)); + } +} \ No newline at end of file diff --git a/src/Validator/ChainRutValidator.php b/src/Validator/ChainRutValidator.php new file mode 100644 index 0000000..5549d6f --- /dev/null +++ b/src/Validator/ChainRutValidator.php @@ -0,0 +1,51 @@ + + */ +class ChainRutValidator implements RutValidator +{ + /** + * @var RutValidator[] + */ + private $validators; + + /** + * ChainRutValidator constructor. + */ + public function __construct() + { + $this->validators = []; + } + + /** + * @param RutValidator $validator + * @return ChainRutValidator + */ + public function addValidator(RutValidator $validator): ChainRutValidator + { + $this->validators[] = $validator; + return $this; + } + + /** + * @param Rut $rut + * @throws InvalidRutException on invalid Rut. + */ + public function validate(Rut $rut): void + { + foreach ($this->validators as $validator) { + $validator->validate($rut); + } + } +} \ No newline at end of file diff --git a/src/Validator/RutValidator.php b/src/Validator/RutValidator.php new file mode 100644 index 0000000..d8b8fc2 --- /dev/null +++ b/src/Validator/RutValidator.php @@ -0,0 +1,35 @@ + + */ +interface RutValidator +{ + /** + * Validates a Rut. + * + * The implementation MUST throw an InvalidRutException if validation fails. + * + * The different clients CAN catch that exception and handle the validation + * error according to their business rules. + * + * @param Rut $rut + * @throws InvalidRutException on invalid Rut. + */ + public function validate(Rut $rut): void; +} \ No newline at end of file diff --git a/src/Validator/SimpleRutValidator.php b/src/Validator/SimpleRutValidator.php new file mode 100644 index 0000000..9297f0c --- /dev/null +++ b/src/Validator/SimpleRutValidator.php @@ -0,0 +1,27 @@ + + */ +class SimpleRutValidator implements RutValidator +{ + /** + * @param Rut $rut + */ + public function validate(Rut $rut): void + { + $digit = Correlative::findVerifierDigit($rut->getCorrelative()); + + if ($digit !== $rut->getVerifierDigit()) { + throw new InvalidRutException($rut); + } + } +} \ No newline at end of file diff --git a/tests/Rut/RutTest.php b/tests/Rut/RutTest.php new file mode 100644 index 0000000..94201e1 --- /dev/null +++ b/tests/Rut/RutTest.php @@ -0,0 +1,66 @@ +assertEquals('16894365', $rut->getCorrelative()); + $this->assertEquals('2', $rut->getVerifierDigit()); + } + + public function testThatRutsInstantiatedDifferentFormatButWithEqualValueAreIndeedEqual() + { + $rut1 = new Rut('16.894.365-2'); + $rut2 = new Rut('16894365-2'); + $this->assertTrue($rut1->isEqualTo($rut2)); + } + + public function testThatFormatClearWorks() + { + $rut = new Rut('16.894.365-2'); + $this->assertEquals('168943652', $rut->format(Rut::FORMAT_CLEAR)); + } + + public function testThatFormatWithHyphenWorks() + { + $rut = new Rut('16.894.365-2'); + $this->assertEquals('16894365-2', $rut->format(Rut::FORMAT_HYPHENED)); + } + + public function testThatFormatReadableWorks() + { + $rut = new Rut('168943652'); + $this->assertEquals('16.894.365-2', $rut->format(Rut::FORMAT_READABLE)); + } + + public function testThatIntegratedValidationThrowsExceptionOnInvalidRut() + { + $this->expectException(InvalidRutException::class); + + $validator = new SimpleRutValidator(); + $rut = new Rut('4444444-2', $validator); + } + + public function testThatIntegratedValidationDoesNotThrowExceptionOnValidRut() + { + $validator = new SimpleRutValidator(); + $rut = new Rut('16.894.365-2', $validator); + + $this->assertInstanceOf(Rut::class, $rut); + } +} diff --git a/tests/Validator/SimpleRutValidatorTest.php b/tests/Validator/SimpleRutValidatorTest.php new file mode 100644 index 0000000..3dbbd37 --- /dev/null +++ b/tests/Validator/SimpleRutValidatorTest.php @@ -0,0 +1,31 @@ +validate($rut); + + $this->assertInstanceOf(Rut::class, $rut); + } + + public function testValidationFailsOnInvalidRut() + { + $this->expectException(InvalidRutException::class); + + $rut = new Rut('34.4534.353-1'); + $validator = new SimpleRutValidator(); + + $validator->validate($rut); + } +}