feat: new v4 version (#5)

This commit is contained in:
2024-03-04 22:02:49 +00:00
committed by GitHub
parent f1b14d4d40
commit 8d5b060acf
28 changed files with 5385 additions and 522 deletions

1
.dev/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
coverage

View File

@@ -0,0 +1,50 @@
FROM alpine:3.18 as base
ARG UID=1000
ARG USER=mnavarro
ARG COMPOSER_VERSION=2.7.0
# Create a development user
RUN adduser $USER --disabled-password --home /$USER --uid $UID
# Install PHP Extensions
RUN apk add --no-cache \
php82-cli \
php82-phar \
php82-mbstring \
php82-json \
php82-zip \
php82-openssl \
php82-dom \
php82-xml \
php82-soap \
php82-session \
php82-xmlwriter \
php82-sockets \
php82-simplexml \
php82-bcmath \
php82-xmlreader \
php82-tokenizer \
php82-iconv \
php82-sodium \
php82-fileinfo \
php82-curl \
php82-ctype \
php82-pcntl \
php82-posix
# Link php82 to php
RUN ln -s /usr/bin/php82 /usr/bin/php
# Add wget to make requests
RUN apk add --no-cache wget
# Download and install composer
RUN wget -O /usr/bin/composer https://github.com/composer/composer/releases/download/$COMPOSER_VERSION/composer.phar && \
chmod +x /usr/bin/composer
FROM base as dev
# Install and Configure XDebug
RUN apk add --no-cache php82-pecl-xdebug
COPY ./xdebug.ini /etc/php82/conf.d/60_xdebug.ini

View File

@@ -0,0 +1,6 @@
zend_extension=xdebug.so
[xdebug]
xdebug.mode=debug,develop,coverage
xdebug.client_host=host.docker.internal
xdebug.output_dir=/castor/context/.dev/debug

7
.dev/init Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
mkdir -p .dev/coverage
composer install

12
.gitattributes vendored Normal file
View File

@@ -0,0 +1,12 @@
/.idea export-ignore
/vendor export-ignore
/tests export-ignore
/.dev export-ignore
/.php-cs-fixer.dist.php export-ignore
/compose.yml export-ignore
/composer.lock export-ignore
/phpunit.xml export-ignore
/psalm.xml export-ignore
/psalm-baseline.xml export-ignore
/.gitignore export-ignore
/README.md export-ignore

39
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: "Chilean Rut CI"
on:
pull_request:
branches: ['master']
paths:
- '**.php'
- 'composer.json'
- '.github/workflows/ci.yml'
- '.github/workflows/php.yml'
push:
branches: ['master']
paths:
- '**.php'
- 'composer.json'
- '.github/workflows/ci.yml'
- '.github/workflows/php.yml'
jobs:
ci:
name: 'CI'
uses: ./.github/workflows/php.yml
with:
php-version: 8.2
composer-cmd: install --ansi --no-interaction --no-progress --no-suggest --prefer-dist
secrets: inherit
# Publishes code coverage report
publish-coverage:
name: "Publish Coverage"
needs: ['ci']
if: ${{ github.ref_name == 'master' }}
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

51
.github/workflows/php.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: "PHP Checks"
on:
workflow_call:
inputs:
php-version:
description: 'The version of PHP to use'
default: '8.2'
type: string
composer-cmd:
description: 'Command to install dependencies'
type: string
default: 'install --ansi --no-interaction --no-progress --no-suggest --prefer-dist'
jobs:
php-checks:
name: "PHP Checks"
runs-on: ubuntu-latest
steps:
- name: "Checkout Code"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "pcov"
php-version: "${{ inputs.php-version }}"
ini-values: memory_limit=-1
tools: composer:v2
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: "Cache dependencies"
uses: "actions/cache@v2"
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}"
restore-keys: "${{ runner.os }}-composer-"
- name: "Install dependencies"
run: "composer ${{ inputs.composer-cmd }}"
- name: "Check Code Style"
run: "composer fmt:check"
- name: "Run Psalm"
run: "composer psalm:gh"
- name: "Run Test Suite"
run: "composer test"
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: '.dev/coverage'

View File

@@ -1,122 +0,0 @@
name: "Review PR"
on:
pull_request:
env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --no-suggest --prefer-dist"
jobs:
# This job ensures the coding standard is followed
coding-standards:
name: "Coding Standards"
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
php-version:
- "7.4"
operating-system:
- "ubuntu-latest"
steps:
- name: "Checkout Code"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1
tools: composer:v2
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache dependencies"
uses: "actions/cache@v2"
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "composer-cache"
restore-keys: "composer-cache"
- name: "Install dependencies"
run: "composer install ${{ env.COMPOSER_FLAGS }}"
- name: "Coding Standard"
run: "vendor/bin/php-cs-fixer fix --dry-run -vvv"
type-analysis:
name: "Type Coverage"
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
php-version:
- "7.4"
operating-system:
- "ubuntu-latest"
steps:
- name: "Checkout Code"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1
tools: composer:v2
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache dependencies"
uses: "actions/cache@v2"
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "composer-cache"
restore-keys: "composer-cache"
- name: "Install dependencies"
run: "composer install ${{ env.COMPOSER_FLAGS }}"
- name: "Run Psalm"
run: "vendor/bin/psalm --output-format=github --shepherd --stats"
continue-on-error: true
unit-test:
name: "Unit Testing"
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
dependencies:
- "lowest"
- "highest"
php-version:
- "7.4"
- "8.0"
operating-system:
- "ubuntu-latest"
steps:
- name: "Checkout Code"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1
tools: composer:v2
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache dependencies"
uses: "actions/cache@v2"
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "composer-cache"
restore-keys: "composer-cache"
- name: "Install lowest dependencies"
if: ${{ matrix.dependencies == 'lowest' }}
run: "composer update ${{ env.COMPOSER_FLAGS }} --prefer-lowest"
- name: "Install highest dependencies"
if: ${{ matrix.dependencies == 'highest' }}
run: "composer install ${{ env.COMPOSER_FLAGS }}"
- name: "Run PHPUnit"
run: "vendor/bin/phpunit --testdox --coverage-text"

6
.gitignore vendored
View File

@@ -1,5 +1,3 @@
vendor
composer.lock
.idea
.php_cs.cache
.phpunit.result.cache
.env
compose.override.yml

36
.php-cs-fixer.dist.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
use PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer;
$header = <<<EOF
@project Chilean RUT
@link https://github.com/mnavarrocarter/chilean-rut
@package castor/log
@author Matias Navarro-Carter mnavarrocarter@gmail.com
@license MIT
@copyright 2024 Matias Navarro-Carter
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
return (new PhpCsFixer\Config())
->setCacheFile('/tmp/php-cs-fixer')
->setRiskyAllowed(true)
->setRules([
'@PhpCsFixer' => true,
'declare_strict_types' => true,
'header_comment' => ['header' => $header, 'comment_type' => 'PHPDoc'],
'yoda_style' => false,
'php_unit_internal_class' => false,
'php_unit_test_class_requires_covers' => false,
'native_function_invocation' => [
'include' => [NativeFunctionInvocationFixer::SET_ALL],
'scope' => 'namespaced',
]
])
->setFinder(
PhpCsFixer\Finder::create()
->in(["src", "tests"])
)
;

View File

@@ -7,7 +7,7 @@ Además, posee dos *types* para `doctrine/dbal`.
## Instalación
Esta libería funciona con composer:
Esta puede ser instalada mediante composer:
```
composer require mnavarrocarter/chilean-rut
@@ -18,8 +18,11 @@ composer require mnavarrocarter/chilean-rut
### Parseando un Rut
La clase Rut es capaz de parsear cualquier tipo de rut sin importar el formato usando
el método `Rut::parse()`. Confiadamente puedes poner el valor directamente de un
formulario web y `parse` se encargara de sanitizar el string y ver si el RUT es valido.
el método `Rut::parse()`. Confiadamente, puedes poner el valor directamente de un
formulario web y `parse` se encargará de sanitizar el string y ver si el RUT es válido.
> NOTA: Un Rut se considera válido cuando su dígito verificador es algorítmicamente válido
> para el número. Esta libreria no puede validar que el Rut existe realmente.
```php
<?php
@@ -29,50 +32,53 @@ use MNC\ChileanRut\Rut;
$rut = Rut::parse('23.546.565-4');
```
### Validando el Rut
### Validando el RUT
> TLDR: Un objeto `Rut` siempre será valido.
Si tu RUT no es valido, el método `parse` lanzara una excepción de tipo
`MNC\ChileanRut\InvalidRut`. Esto es para seguir buenos principios de *objects
calisthenics*: un objeto de valor siempre se crea en un estado valido, y se mantiene
valido a través de todo su ciclo de vida. No se permiten mutaciones que dejen el
objeto en un estado invalido.
Si tu RUT no es válido, el método `parse` lanzara una excepción de tipo
`MNC\Rut\IsInvalid`. Esto es para seguir buenos principios de *objects
calisthenics*: un objeto de valor siempre se crea en un estado válido, y se mantiene
válido a través de todo su ciclo de vida. No se permiten mutaciones que dejen el
objeto en un estado inválido.
Por esta razón el objecto `$rut` es completamente inmutable. Esto quiere decir
Por esta razón el objeto `MNC\Rut\IsInvalid` es completamente inmutable. Esto quiere decir
que una vez creado no puedes cambiar su estado interno: solo puedes leer información.
Estos son los unicos métodos que puedes usar:
```php
<?php
use MNC\ChileanRut\Rut;
use MNC\Rut;
$rut = Rut::parse('23.546.565-4');
$rut->getNumber(); // (int) 23546565
$rut->getVerifier(); // (string) 4
$rut->number; // (int) 23546565
$rut->verifier; // (MNC\Rut\Verifier::Four) 4
```
### Formateando el Rut
Existen muchas formas distintas de formatear un rut y esta librería soporta muchas
Existen muchas formas distintas de formatear un RUT y esta librería soporta muchas
de ellas. El método format devuelve un objeto al cual puedes encadenar llamadas para
formatear el rut y luego castearlo a un string. La interfaz es encadenable para que
puedas combinar las opciones de formato como quieras.
formatear el RUT y extraer su información de diversas maneras.
He aquí algunos ejemplos:
```php
<?php
use MNC\ChileanRut\Rut;
use MNC\Rut;
$rut = Rut::parse('23.546.565-4');
echo $rut->format()->hyphened(); // 23546565-4
echo $rut->format()->dotted()->hyphened(); // 23.546.565-2
echo $rut->format()->dotted()->hyphened()->obfuscated(); // **.***.565-2
echo $rut->format()->obfuscated()->hyphened(); // *****565-2
echo $rut->format()->obfuscated(); // *****5652
echo $rut->toString(); // 235465654
echo $rut->toSimple(); // 23546565-4
echo $rut->toHuman(); // 23.546.565-4
echo $rut->last(4); // 6565
echo $rut->last(4, pad: '*'); // ****6565
echo $rut->first(4); // 2354
echo $rut->first(4, pad: '*'); // 2354****
```
## Integraciones con Librerías de Terceros
@@ -82,13 +88,13 @@ Esta librería provee dos [*Custom Types*](https://www.doctrine-project.org/proj
para Doctrine, con el objetivo de que puedas mapear tus objetos `Rut` fácilmente
a una base de datos relacional.
El `MNC\ChileanRut\Doctrine\RutType` mapeara tu RUT a una columna VARCHAR.
`MNC\Rut\Doctrine\RutType` mapea tu RUT a una columna VARCHAR.
El string se guarda con puntos y guion. Ex: `16.894.365-2`. Es una forma no tan
eficiente de guardar los RUTS (en términos de espacio), pero ayuda mucho cuando
se visualiza o exporta la base de datos a otras fuentes.
El `MNC\ChileanRut\Doctrine\NumericRutType` mapeara tu RUT a una columna INTEGER.
El numero se guarda sin digito verificador y es recalculado cuando la columna
`MNC\Rut\Doctrine\NumericRutType` mapea tu RUT a una columna INTEGER.
El número se guarda sin dígito verificador y es recalculado cuando la columna
es transformada a un valor PHP. Esta forma de guardar ruts es muy eficiente (en
términos de espacio), pero cuesta comparar y leer los números si visualizas o
exportas los registros en la base de datos.
@@ -97,18 +103,15 @@ Por supuesto, puedes elegir el `Type` que más se ajuste a tus necesidades.
## FAQ
### ¿Cómo nació y por qué esta librería?
### ¿Por qué esta librería?
Esta librería nace de la necesidad de estandarizar una clase Rut común para todos
mis proyectos PHP.
Si bien es cierto, hay muchas librerías con implementaciones de Rut chilenos en PHP,
muchas de ellas tienen notorias deficiencias:
1. No están testeadas unitariamente,
2. No tienen un buen diseño y sus apis tienen efectos secundarios.
3. Están acopladas a un framework (Laravel Rut y otras hierbas)
4. No proveen herramientas ni integraciones con librerías de terceros.
### ¿Por qué PHP 7.4?
PHP 7.3 ya no tiene mucho tiempo de soporte y 8.0 esta ad portas de ser lanzado. Hay que
empujar el ecosistema hacia adelante.
1. No están testeadas unitariamente.
2. No estan tipadas apropiadamente
3. No tienen un buen diseño y sus apis tienen efectos secundarios.
4. Están acopladas a un framework (Laravel Rut y otras hierbas)
5. No proveen herramientas ni integraciones con librerías de terceros, como Doctrine.

27
compose.yml Normal file
View File

@@ -0,0 +1,27 @@
name: "mnavarro"
services:
chilean-rut: &php
image: mnavarro/chilean-rut:dev
build:
context: .dev/docker/php
dockerfile: Dockerfile
target: dev
args:
UID: ${UID:-1000}
user: ${UID:-1000}
restart: unless-stopped
depends_on:
chilean-rut-init:
condition: service_completed_successfully
working_dir: /mnavarro/chilean-rut
volumes:
- ./:/mnavarro/chilean-rut
command: ["php", "-S", "0.0.0.0:8000", "-t", ".dev/coverage"]
chilean-rut-init:
<<: *php
depends_on: []
restart: no
command: [".dev/init"]

View File

@@ -3,39 +3,46 @@
"description": "PHP Rut Value Object with validation utilities, doctrine type, and other cool features.",
"type": "library",
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"authors": [
{
"name": "Matias Navarro Carter",
"email": "mnavarro@option.cl"
"name": "Matias Navarro-Carter",
"email": "mnavarrocarter@gmail.com",
"role": "Lead Maintainer"
}
],
"minimum-stability": "stable",
"require": {
"php": "^7.4|^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"phpunit/phpunit": "^9.0",
"doctrine/dbal": "^2.10.1|^3.0",
"symfony/var-dumper": "^5.1",
"vimeo/psalm": "^4.2"
},
"autoload": {
"psr-4": {
"MNC\\ChileanRut\\": "src/"
"MNC\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"MNC\\ChileanRut\\": "tests"
"MNC\\": "tests"
}
},
"require": {
"php": ">=8.2"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"friendsofphp/php-cs-fixer": "^3.49",
"vimeo/psalm": "^5.22",
"doctrine/dbal": "^2.10.1|^3.0"
},
"scripts": {
"lint": "php-cs-fixer fix --allow-risky=yes",
"pr": [
"php-cs-fixer fix --dry-run -vvv",
"phpunit --testdox --coverage-text",
"psalm --stats"
]
"pr": ["@fmt", "@psalm", "@test"],
"ci": ["@fmt:check", "@psalm:gh", "@test"],
"fmt": "php-cs-fixer fix --diff --ansi",
"fmt:check": "php-cs-fixer fix --dry-run --diff --ansi",
"test": ["phpunit --colors"],
"test:unit": "phpunit --colors --exclude-group=integration --exclude-group=e2e",
"test:e2e": "phpunit --colors --group=e2e",
"test:integration": "phpunit --colors --group=integration",
"psalm": "psalm --no-cache --threads=5 --use-baseline",
"psalm:gh": "psalm --no-cache --threads=5 --long-progress --output-format=github --use-baseline",
"psalm:fix": "psalm --update-baseline",
"psalm:allow": "psalm --set-baseline=psalm-baseline.xml"
}
}

4635
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
phpunit.xml Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
cacheResultFile="/tmp/phpunit.result.cache"
failOnWarning="true">
<testsuites>
<testsuite name="main">
<directory>tests</directory>
</testsuite>
</testsuites>
<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
<coverage>
<report>
<html outputDirectory=".dev/coverage"/>
</report>
</coverage>
</phpunit>

8
psalm-baseline.xml Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.22.2@d768d914152dbbf3486c36398802f74e80cfde48">
<file src="src/Rut/Doctrine/NumericRutType.php">
<ImplementedReturnTypeMismatch>
<code><![CDATA[?Rut]]></code>
</ImplementedReturnTypeMismatch>
</file>
</files>

17
psalm.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="false"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

View File

@@ -1,87 +0,0 @@
<?php
declare(strict_types=1);
/**
* @project Chilean Rut
* @link https://github.com/mnavarrocarter/chilean-rut
* @package mnavarrocarter/chilean-rut
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2020 Matias Navarro Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\ChileanRut;
/**
* A FormattedRut encapsulates logic for formatting a Rut.
*/
class FormattedRut
{
private const HYPHENED = 1;
private const DOTTED = 2;
private const OBFUSCATED = 4;
private Rut $rut;
private int $flags;
/**
* FormattedRut constructor.
*/
public function __construct(Rut $rut)
{
$this->rut = $rut;
$this->flags = 0;
}
public function hyphened(): FormattedRut
{
return $this->add(self::HYPHENED);
}
public function dotted(): FormattedRut
{
return $this->add(self::DOTTED);
}
public function obfuscated(): FormattedRut
{
return $this->add(self::OBFUSCATED);
}
private function add(int $flag): FormattedRut
{
$clone = clone $this;
$clone->flags += $flag;
return $clone;
}
public function __toString(): string
{
$number = (string) $this->rut->getNumber();
$verifier = $this->rut->getVerifier();
if ($this->has(self::OBFUSCATED)) {
$replace = str_repeat('*', strlen($number) - 3);
$number = substr_replace($number, $replace, 0, -3);
}
if ($this->has(self::DOTTED)) {
$number = substr_replace($number, '.', -3, 0);
$number = substr_replace($number, '.', -7, 0);
}
if ($this->has(self::HYPHENED)) {
return $number.'-'.$verifier;
}
return $number.$verifier;
}
private function has(int $flag): bool
{
return ($this->flags & $flag) !== 0;
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
/**
* @project Chilean Rut
* @link https://github.com/mnavarrocarter/chilean-rut
* @package mnavarrocarter/chilean-rut
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2020 Matias Navarro Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\ChileanRut;
use InvalidArgumentException;
/**
* Class InvalidRut.
*/
class InvalidRut extends InvalidArgumentException
{
public static function number(): InvalidRut
{
return new self('Rut number cannot be greater than 999.999.999 and smaller than zero');
}
public static function digit(string $digit): InvalidRut
{
return new self(sprintf('The verifier digit "%s" is not valid', $digit));
}
}

View File

@@ -3,83 +3,91 @@
declare(strict_types=1);
/**
* @project Chilean Rut
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2020 Matias Navarro Carter
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\ChileanRut;
namespace MNC;
use MNC\Rut\Verifier;
/**
* Rut represents a the Chilean National ID Number.
* Esta clase representa un RUT.
*
* All residents of Chile are uniquely identified by one of these and it is
* mainly used for tax purposes.
* Una vez creado, el RUT es siempre valido
*/
class Rut
final readonly class Rut
{
private const VALID_VERIFIERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'K'];
private const MAX_NUMBER = 999_999_999;
private const MIN_NUMBER = 0;
private function __construct(
public int $number,
public Verifier $verifier
) {}
/**
* The actual RUT number.
* Parsea un objeto RUT a partir de una cadena de texto.
*
* El RUT DEBE contener el digito verificador.
*
* Si no se cuenta con el verificador, el metodo create debe ser usado.
*
* @see Rut::create
*
* @throws Rut\IsInvalid si el RUT no es valido
*/
private int $number;
/**
* The RUT verifier digit.
*/
private string $verifier;
public static function parse(string $rut): Rut
{
// Remove space, dots and hyphens
$rut = str_replace([' ', '.', '-'], '', $rut);
$rut = \str_replace([' ', '.', '-'], '', $rut);
return new self(
(int) substr($rut, 0, -1),
substr($rut, -1)
return self::create(
(int) \substr($rut, 0, -1),
Verifier::fromString(\substr($rut, -1))
);
}
/**
* Creates a valid rut out of a number.
*/
public static function create(int $number): Rut
{
$verifier = self::calculateVerifier($number);
return new Rut($number, $verifier);
}
/**
* Rut constructor.
* Crea un objeto RUT.
*
* @throws InvalidRut if the verifier digit is invalid
* El digito verificador es opcional. Cuando es recibido, es validado.
*
* Si el digito verificador no es provisto, es generado automáticamente.
*
* @throws Rut\IsInvalid si el Rut es invalido
*/
public function __construct(int $number, string $verifier)
public static function create(int $number, ?Verifier $verifier = null): self
{
$this->number = $number;
$this->verifier = strtoupper($verifier);
$this->guard();
if ($number < self::MIN_NUMBER) {
throw Rut\IsInvalid::numberTooSmall($number);
}
public function getNumber(): int
{
return $this->number;
if ($number > self::MAX_NUMBER) {
throw Rut\IsInvalid::numberTooBig($number);
}
public function getVerifier(): string
{
return $this->verifier;
$computed = self::computeVerifier($number);
if ($verifier === null) {
return new self($number, $computed);
}
if ($computed !== $verifier) {
throw Rut\IsInvalid::rut($number, $verifier);
}
return new self($number, $verifier);
}
/**
* Compares whether a Rut is equal to another or not.
* Compara si un RUT es igual a otro o no.
*/
public function equals(Rut $rut): bool
{
@@ -87,38 +95,71 @@ class Rut
}
/**
* Formats a Rut to a string.
* Retorna el RUT en formato "12345678K".
*/
public function format(): FormattedRut
public function toString(): string
{
return new FormattedRut($this);
}
private function guard(): void
{
// Check if rut is between zero and 999.999.999
if ($this->number < 0 || $this->number > 999_999_999) {
throw InvalidRut::number();
}
// Check if the verifier digit is in the range of valid ones
if (!in_array($this->verifier, self::VALID_VERIFIERS, true)) {
throw InvalidRut::digit($this->verifier);
}
// Check the verifier is algorithmically correct
if ($this->verifier !== self::calculateVerifier($this->number)) {
throw InvalidRut::digit($this->verifier);
}
return $this->number.$this->verifier->toString();
}
/**
* Calculates a verifier digit from a number.
* Retorna el RUT en formato "12345678-K".
*/
private static function calculateVerifier(int $number): string
public function toSimple(): string
{
/** @var list<int> $sequence */
$sequence = array_filter(array_reverse(str_split((string) $number)), function ($d) {
return preg_match('/\d/', $d);
});
return $this->number.'-'.$this->verifier->toString();
}
/**
* Retorna el RUT en formato "12.345.678-K".
*/
public function toHuman(): string
{
return \number_format($this->number, 0, ',', '.').'-'.$this->verifier->toString();
}
/**
* Retorna los $n ultimos numeros del RUT.
*
* El digito verificador no es considerado.
*/
public function last(int $n, string $pad = ''): string
{
$number = (string) $this->number;
$last = \substr($number, -$n);
if ($pad !== '') {
$last = \str_repeat($pad, \strlen($number) - $n).$last;
}
return $last;
}
/**
* Retorna los $n primeros numeros del RUT.
*
* El digito verificador no es considerado.
*/
public function first(int $n, string $pad = ''): string
{
$number = (string) $this->number;
$first = \substr($number, 0, $n);
if ($pad !== '') {
$first .= \str_repeat($pad, \strlen($number) - $n);
}
return $first;
}
/**
* Computa el digito verificador de un RUT a partir del numero.
*/
public static function computeVerifier(int $number): Verifier
{
$sequence = \array_reverse(\array_map(
static fn (string $d): int => (int) $d,
\str_split((string) $number)
));
$x = 2;
$s = 0;
foreach ($sequence as $digit) {
@@ -128,14 +169,7 @@ class Rut
$s += $digit * $x;
++$x;
}
$dv = 11 - ($s % 11);
if ($dv === 10) {
$dv = 'K';
}
if ($dv === 11) {
$dv = '0';
}
return (string) $dv;
return Verifier::from(11 - ($s % 11));
}
}

View File

@@ -3,29 +3,26 @@
declare(strict_types=1);
/**
* @project Chilean Rut
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2020 Matias Navarro Carter
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\ChileanRut\Doctrine;
namespace MNC\Rut\Doctrine;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\IntegerType;
use MNC\ChileanRut\Rut;
use MNC\Rut;
/**
* Class NumericRutType.
*
* This type maps the rut to a number table and stores it without the verifier
* digit. This is because the digit is derived from the number.
* Mapea el Rut a una columna integer.
*/
class NumericRutType extends IntegerType
{
@@ -39,18 +36,16 @@ class NumericRutType extends IntegerType
/**
* @param mixed $value
*
* @return mixed
*
* @throws ConversionException
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return $value;
return null;
}
if ($value instanceof Rut) {
return (string) $value->getNumber();
return (string) $value->number;
}
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', Rut::class]);
@@ -59,16 +54,21 @@ class NumericRutType extends IntegerType
/**
* @param mixed $value
*
* @return mixed
* @throws ConversionException
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
public function convertToPHPValue($value, AbstractPlatform $platform): ?Rut
{
$value = parent::convertToPHPValue($value, $platform);
if ($value === null) {
return $value;
return null;
}
return Rut::create($value);
}
public function requiresSQLCommentHint(AbstractPlatform $platform): true
{
return true;
}
}

View File

@@ -3,29 +3,26 @@
declare(strict_types=1);
/**
* @project Chilean Rut
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2020 Matias Navarro Carter
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\ChileanRut\Doctrine;
namespace MNC\Rut\Doctrine;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\StringType;
use MNC\ChileanRut\Rut;
use MNC\Rut;
/**
* Class RutTextType.
*
* This type maps the rut to a string and stores it with the verifier number,
* including dots and the hyphen.
* Mapea el RUT a una columna VARCHAR.
*/
class RutType extends StringType
{
@@ -39,18 +36,16 @@ class RutType extends StringType
/**
* @param mixed $value
*
* @return mixed
*
* @throws ConversionException
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return $value;
return null;
}
if ($value instanceof Rut) {
return (string) $value->format()->hyphened()->dotted();
return $value->toHuman();
}
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', Rut::class]);
@@ -59,20 +54,23 @@ class RutType extends StringType
/**
* @param mixed $value
*
* @return mixed
*
* @throws ConversionException
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
public function convertToPHPValue($value, AbstractPlatform $platform): ?Rut
{
if ($value === null) {
return $value;
return null;
}
if (is_string($value)) {
if (\is_string($value)) {
return Rut::parse($value);
}
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string']);
}
public function requiresSQLCommentHint(AbstractPlatform $platform): true
{
return true;
}
}

53
src/Rut/IsInvalid.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\Rut;
class IsInvalid extends \InvalidArgumentException
{
public static function numberTooBig(int $number): IsInvalid
{
return new self(\sprintf(
'El RUT numero %d es mayor a 99.999.999',
$number
));
}
public static function numberTooSmall(int $number): IsInvalid
{
return new self(\sprintf(
'El RUT numero %d es menor a cero',
$number
));
}
public static function rut(int $number, Verifier $verifier): IsInvalid
{
return new self(\sprintf(
'El digito verificador %s no es valido para el rut %d',
$verifier->toString(),
$number
));
}
public static function verifier(string $verifier): IsInvalid
{
return new self(\sprintf(
'Encontrado un digito verificador invalido con valor %s',
$verifier,
));
}
}

78
src/Rut/Verifier.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\Rut;
/**
* Representa el digito verificador de un RUT.
*
* Internamente, se guarda el modulo del RUT, y no su valor como texto.
*/
enum Verifier: int
{
case One = 1;
case Two = 2;
case Three = 3;
case Four = 4;
case Five = 5;
case Six = 6;
case Seven = 7;
case Eight = 8;
case Nine = 9;
case K = 10;
case Zero = 11;
/**
* @throws IsInvalid si el verificador es invalido
*/
public static function fromString(string $v): Verifier
{
return match ($v) {
'1' => self::One,
'2' => self::Two,
'3' => self::Three,
'4' => self::Four,
'5' => self::Five,
'6' => self::Six,
'7' => self::Seven,
'8' => self::Eight,
'9' => self::Nine,
'0' => self::Zero,
'K' => self::K,
default => throw IsInvalid::verifier($v)
};
}
/**
* Retorna la representacion textual del digito verificador.
*/
public function toString(): string
{
return match ($this) {
self::One => '1',
self::Two => '2',
self::Three => '3',
self::Four => '4',
self::Five => '5',
self::Six => '6',
self::Seven => '7',
self::Eight => '8',
self::Nine => '9',
self::K => 'K',
self::Zero => '0',
};
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace MNC\ChileanRut;
use PHPUnit\Framework\TestCase;
/**
* Class FormattedRutTest
* @package MNC\ChileanRut
*/
class FormattedRutTest extends TestCase
{
public function testItFormatsHyphened(): void
{
$formatted = (string) Rut::parse('168943652')->format()->hyphened();
self::assertSame('16894365-2', $formatted);
}
public function testItFormatsDotted(): void
{
$formatted = (string) Rut::parse('168943652')->format()->dotted();
self::assertSame('16.894.3652', $formatted);
}
public function testItFormatsHyphenedAndDotted(): void
{
$formatted = (string) Rut::parse('168943652')->format()->hyphened()->dotted();
self::assertSame('16.894.365-2', $formatted);
}
public function testItFormatsObfuscated(): void
{
$formatted = (string) Rut::parse('168943652')->format()->obfuscated();
self::assertSame('*****3652', $formatted);
}
public function testItFormatsObfuscatedAndHyphened(): void
{
$formatted = (string) Rut::parse('168943652')->format()->hyphened()->obfuscated();
self::assertSame('*****365-2', $formatted);
}
public function testItFormatsObfuscatedAndDotted(): void
{
$formatted = (string) Rut::parse('168943652')->format()->obfuscated()->dotted();
self::assertSame('**.***.3652', $formatted);
}
public function testItFormatsWithAll(): void
{
$formatted = (string) Rut::parse('168943652')->format()->hyphened()->dotted()->obfuscated();
self::assertSame('**.***.365-2', $formatted);
}
}

View File

@@ -1,13 +1,32 @@
<?php
namespace MNC\ChileanRut\Doctrine;
declare(strict_types=1);
/**
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\Rut\Doctrine;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use MNC\ChileanRut\Rut;
use MNC\Rut;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(NumericRutType::class)]
#[CoversClass(Rut::class)]
#[CoversClass(Rut\Verifier::class)]
class NumericRutTypeTest extends TestCase
{
public static function setUpBeforeClass(): void
@@ -54,4 +73,11 @@ class NumericRutTypeTest extends TestCase
$this->expectException(ConversionException::class);
$type->convertToDatabaseValue(new \DateTime(), $platform);
}
#[Test]
public function it_returns_true_for_comment(): void
{
$platform = $this->createMock(AbstractPlatform::class);
$this->assertTrue(Type::getType(NumericRutType::NAME)->requiresSQLCommentHint($platform));
}
}

View File

@@ -1,13 +1,32 @@
<?php
namespace MNC\ChileanRut\Doctrine;
declare(strict_types=1);
/**
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2024 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\Rut\Doctrine;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use MNC\ChileanRut\Rut;
use MNC\Rut;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(RutType::class)]
#[CoversClass(Rut::class)]
#[CoversClass(Rut\Verifier::class)]
class RutTypeTest extends TestCase
{
public static function setUpBeforeClass(): void
@@ -62,4 +81,11 @@ class RutTypeTest extends TestCase
$this->expectException(ConversionException::class);
$type->convertToDatabaseValue(new \DateTime(), $platform);
}
#[Test]
public function it_returns_true_for_comment(): void
{
$platform = $this->createMock(AbstractPlatform::class);
$this->assertTrue(Type::getType(RutType::NAME)->requiresSQLCommentHint($platform));
}
}

View File

@@ -1,87 +1,107 @@
<?php
/*
* This file is part of the MNC\ChileanRut library.
declare(strict_types=1);
/**
* @project Chilean RUT
* @link https://github.com/mnavarrocarter/chilean-rut
* @package castor/log
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2024 Matias Navarro-Carter
*
* (c) Matías Navarro Carter <mnavarrocarter@gmail.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MNC\ChileanRut;
namespace MNC;
use MNC\Rut\IsInvalid;
use MNC\Rut\Verifier;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* Class RutTest
* @package MNC\ChileanRut\Tests\Rut
*/
#[CoversClass(Rut::class)]
#[CoversClass(IsInvalid::class)]
#[CoversClass(Verifier::class)]
class RutTest extends TestCase
{
/**
* @dataProvider getRutDataset
* @param string $raw
* @param int $expectedNumber
* @param string $expectedVerifier
*/
public function testItParsesRuts(string $raw, int $expectedNumber, string $expectedVerifier): void
#[Test]
#[DataProvider('getParseData')]
public function it_parses_ruts(string $raw, int $expectedNumber, Verifier $expectedVerifier): void
{
$rut = Rut::parse($raw);
self::assertSame($expectedNumber, $rut->getNumber());
self::assertSame($expectedVerifier, $rut->getVerifier());
$this->assertSame($expectedNumber, $rut->number);
$this->assertSame($expectedVerifier, $rut->verifier);
}
public function testItDetectsOutOfRangeVerifier(): void
#[Test]
#[DataProvider('getParseWithErrorData')]
public function it_parses_with_error(string $raw, string $expectedError): void
{
$this->expectException(InvalidRut::class);
Rut::parse('16894365F');
$this->expectException(IsInvalid::class);
$this->expectExceptionMessage($expectedError);
Rut::parse($raw);
}
public function testItDetectsInvalidVerifier(): void
#[Test]
public function it_cannot_be_negative(): void
{
$this->expectException(InvalidRut::class);
Rut::parse('16894365K');
$this->expectException(IsInvalid::class);
$this->expectExceptionMessage('El RUT numero -22224525 es menor a cero');
Rut::create(-22_224_525);
}
public function testItDetectsRutTooBig(): void
{
$this->expectException(InvalidRut::class);
Rut::create(3_355_535_353);
}
public function testItComparesToEqual(): void
#[Test]
public function it_checks_for_equality(): void
{
$rut1 = Rut::parse('168943652');
$rut2 = Rut::parse('16.894.365-2');
$rut3 = Rut::create(22_224_525);
self::assertTrue($rut1->equals($rut2));
self::assertFalse($rut1->equals($rut3));
$this->assertTrue($rut1->equals($rut2));
$this->assertFalse($rut1->equals($rut3));
}
public static function testItCreatesARut(): void
#[Test]
public function it_creates_with_no_verifier(): void
{
$rut = Rut::create(22_457_309);
self::assertSame(22_457_309, $rut->getNumber());
$this->assertSame(22_457_309, $rut->number);
}
public static function testItCanFormatRut(): void
#[Test]
public function it_formats(): void
{
$rut = (string) Rut::create(22_457_309)->format();
self::assertSame('22457309K', $rut);
$rut = Rut::create(22_457_309);
$this->assertSame('22457309K', $rut->toString());
$this->assertSame('22457309-K', $rut->toSimple());
$this->assertSame('22.457.309-K', $rut->toHuman());
$this->assertSame('7309', $rut->last(4));
$this->assertSame('****7309', $rut->last(4, '*'));
$this->assertSame('2245', $rut->first(4));
$this->assertSame('2245****', $rut->first(4, '*'));
}
/**
* @return array[]
*/
public function getRutDataset(): array
public static function getParseData(): array
{
return [
['16.894.365-2', 16_894_365, '2'],
['24 736.7322', 24_736_732, '2'],
[' 24 232.. 442 -- 0', 24_232_442, '0'],
['35323325', 3_532_332, '5'],
['22.457.309K', 22_457_309, 'K'],
['15450088K', 15450088, 'K']
['16.894.365-2', 16_894_365, Verifier::Two],
['24 736.7322', 24_736_732, Verifier::Two],
[' 24 232.. 442 -- 0', 24_232_442, Verifier::Zero],
['35323325', 3_532_332, Verifier::Five],
['22.457.309K', 22_457_309, Verifier::K],
['15450088K', 15_450_088, Verifier::K],
];
}
public static function getParseWithErrorData(): array
{
return [
['212321312321-1', 'El RUT numero 212321312321 es mayor a 99.999.999'],
['23.232.123-K', 'El digito verificador K no es valido para el rut 23232123'],
['12.2324.232-P', 'Encontrado un digito verificador invalido con valor P'],
];
}
}