Initial commit

This commit is contained in:
Matias Navarro Carter
2018-08-08 02:51:48 -04:00
commit 05871cb847
16 changed files with 675 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vendor
composer.lock
.idea

28
README.md Normal file
View File

@@ -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.

33
composer.json Normal file
View File

@@ -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"
}
}
}

25
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,60 @@
<?php
namespace MNC\ChileanRut\Bridge\Doctrine\DBAL\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\StringType;
use MNC\ChileanRut\Rut\Rut;
/**
* Class RutType
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace MNC\ChileanRut\Bridge\Symfony\Form;
use MNC\ChileanRut\Rut\Rut;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class RutType
* @package MNC\ChileanRut\Bridge\Symfony\Form
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
class RutType extends TextType
{
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Rut::class,
]);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace MNC\ChileanRut\Bridge\Symfony\Validator;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class IsValidRut extends Constraint
{
public $message = 'The rut "{{value}}" is not valid.';
}

View File

@@ -0,0 +1,52 @@
<?php
namespace MNC\ChileanRut\Bridge\Symfony\Validator;
use MNC\ChileanRut\Exception\InvalidRutException;
use MNC\ChileanRut\Rut\Rut;
use MNC\ChileanRut\Validator\RutValidator;
use MNC\ChileanRut\Validator\SimpleRutValidator;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Class IsValidRutValidator
* @package MNC\ChileanRut\Bridge\Symfony\Validator
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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();
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace MNC\ChileanRut\Exception;
use MNC\ChileanRut\Rut\Rut;
/**
* Class InvalidRutException
* @package MNC\ChileanRut\Rut
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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;
}
}

131
src/Rut/Rut.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
namespace MNC\ChileanRut\Rut;
use MNC\ChileanRut\Validator\RutValidator;
/**
* Class Rut
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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);
}
}

56
src/Util/Correlative.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
namespace MNC\ChileanRut\Util;
use MNC\ChileanRut\Rut\Rut;
/**
* This class provides utils for a Rut correlative.
*
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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));
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace MNC\ChileanRut\Validator;
use MNC\ChileanRut\Exception\InvalidRutException;
use MNC\ChileanRut\Rut\Rut;
/**
* A ChainRutValidator
*
* Use this implementation when you want to validate a Rut against multiple
* validators. Add the validators in order by calling addValidator().
*
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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);
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace MNC\ChileanRut\Validator;
use MNC\ChileanRut\Exception\InvalidRutException;
use MNC\ChileanRut\Rut\Rut;
/**
* This is the base contract for a Rut validator.
*
* You can implement any logic here that you can use to validate a Rut.
* For example, the SimpleRutValidator only validates that a Rut is algorithmically
* correct, but not that it actually exists.
*
* You could create a HTTPRutValidator that performs a request to validate that a
* Rut exists against a Rest Api or a third party service.
*
* @package MNC\ChileanRut\Validator
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace MNC\ChileanRut\Validator;
use MNC\ChileanRut\Exception\InvalidRutException;
use MNC\ChileanRut\Rut\Rut;
use MNC\ChileanRut\Util\Correlative;
/**
* Validates the Rut using the Module 11 algorithm.
*
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
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);
}
}
}

66
tests/Rut/RutTest.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
/**
* Created by PhpStorm.
* User: mnavarro
* Date: 07-08-18
* Time: 23:45
*/
namespace MNC\ChileanRut\Tests\Rut;
use MNC\ChileanRut\Exception\InvalidRutException;
use MNC\ChileanRut\Rut\Rut;
use MNC\ChileanRut\Validator\SimpleRutValidator;
use PHPUnit\Framework\TestCase;
class RutTest extends TestCase
{
public function testThatRutIsSanitizedProperlyOnInstantiation()
{
$rut = Rut::fromString('16.894.365-2');
$this->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);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace MNC\ChileanRut\Tests\Validator;
use MNC\ChileanRut\Exception\InvalidRutException;
use MNC\ChileanRut\Rut\Rut;
use MNC\ChileanRut\Validator\SimpleRutValidator;
use PHPUnit\Framework\TestCase;
class SimpleRutValidatorTest extends TestCase
{
public function testValidationPassesOnValidRut()
{
$rut = new Rut('16.894.365-2');
$validator = new SimpleRutValidator();
$validator->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);
}
}