Rfc::checkIsValid()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 5
nop 2
dl 0
loc 19
ccs 14
cts 14
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace CfdiUtils\Utils;
4
5
class Rfc
6
{
7
    public const RFC_GENERIC = 'XAXX010101000';
8
9
    public const RFC_FOREIGN = 'XEXX010101000';
10
11
    public const DISALLOW_GENERIC = 1;
12
13
    public const DISALLOW_FOREIGN = 2;
14
15
    /** @var string */
16
    private $rfc;
17
18
    /** @var int */
19
    private $length;
20
21
    /** @var string contains calculated checksum */
22
    private $checkSum;
23
24
    /** @var bool */
25
    private $checkSumMatch;
26
27 10
    public function __construct(string $rfc, int $flags = 0)
28
    {
29 10
        $this->checkIsValid($rfc, $flags);
30 6
        $this->rfc = $rfc;
31 6
        $this->length = mb_strlen($rfc);
32 6
        $this->checkSum = static::obtainCheckSum($rfc);
33 6
        $this->checkSumMatch = ($this->checkSum === strval(substr($rfc, -1)));
34
    }
35
36 2
    public function rfc(): string
37
    {
38 2
        return $this->rfc;
39
    }
40
41 4
    public function isPerson(): bool
42
    {
43 4
        return (13 === $this->length);
44
    }
45
46 5
    public function isMoral(): bool
47
    {
48 5
        return (12 === $this->length);
49
    }
50
51 4
    public function isGeneric(): bool
52
    {
53 4
        return (static::RFC_GENERIC === $this->rfc);
54
    }
55
56 4
    public function isForeign(): bool
57
    {
58 4
        return (static::RFC_FOREIGN === $this->rfc);
59
    }
60
61 2
    public function checkSum(): string
62
    {
63 2
        return $this->checkSum;
64
    }
65
66 2
    public function checkSumMatch(): bool
67
    {
68 2
        return $this->checkSumMatch;
69
    }
70
71 2
    public function __toString(): string
72
    {
73 2
        return $this->rfc();
74
    }
75
76 2
    public static function isValid(string $value): bool
77
    {
78
        try {
79 2
            static::checkIsValid($value);
80 1
            return true;
81 1
        } catch (\UnexpectedValueException $exception) {
82 1
            return false;
83
        }
84
    }
85
86
    /**
87
     * @param string $value
88
     * @param int $flags
89
     * @throws \UnexpectedValueException when the value is generic and is not allowed by flags
90
     * @throws \UnexpectedValueException when the value is foreign and is not allowed by flags
91
     * @throws \UnexpectedValueException when the value does not match with the RFC format
92
     * @throws \UnexpectedValueException when the date inside the value is not valid
93
     * @throws \UnexpectedValueException when the last digit does not match the checksum
94
     */
95 52
    public static function checkIsValid(string $value, int $flags = 0)
96
    {
97 52
        if ($flags & static::DISALLOW_GENERIC && $value === static::RFC_GENERIC) {
98 4
            throw new \UnexpectedValueException('No se permite el RFC genérico para público en general');
99
        }
100 48
        if ($flags & static::DISALLOW_FOREIGN && $value === static::RFC_FOREIGN) {
101 2
            throw new \UnexpectedValueException('No se permite el RFC genérico para operaciones con extranjeros');
102
        }
103
        // validate agains a regular expression (values and length)
104 46
        $regex = '/^' // desde el inicio
105 46
            . '[A-ZÑ&]{3,4}' // letras y números para el nombre (3 para morales, 4 para físicas)
106 46
            . '([0-9]{6})' // año mes y día, la validez de la fecha se comprueba después
107 46
            . '[A-Z0-9]{2}[A0-9]{1}' // homoclave (letra o dígito 2 veces + A o dígito 1 vez)
108 46
            . '$/u'; // hasta el final, considerar la cadena unicode
109 46
        if (1 !== preg_match($regex, $value)) {
110 9
            throw new \UnexpectedValueException('No coincide el formato de un RFC');
111
        }
112 37
        if (0 === static::obtainDate($value)) {
113 6
            throw new \UnexpectedValueException('La fecha obtenida no es lógica');
114
        }
115
    }
116
117 6
    public static function obtainCheckSum(string $rfc): string
118
    {
119
        // 'Ñ' translated to '#' due it is multibyte 0xC3 0xB1
120 6
        $dictionary = array_flip(str_split('0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ #', 1));
0 ignored issues
show
Bug introduced by
It seems like str_split('0123456789ABC...LMN&OPQRSTUVWXYZ #', 1) can also be of type true; however, parameter $array of array_flip() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
        $dictionary = array_flip(/** @scrutinizer ignore-type */ str_split('0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ #', 1));
Loading history...
121 6
        $chars = str_split(str_replace('Ñ', '#', $rfc), 1);
122 6
        array_pop($chars); // remove predefined checksum
0 ignored issues
show
Bug introduced by
It seems like $chars can also be of type true; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
        array_pop(/** @scrutinizer ignore-type */ $chars); // remove predefined checksum
Loading history...
123 6
        $length = count($chars);
124 6
        $sum = (11 === $length) ? 481 : 0; // 481 para morales, 0 para físicas
125 6
        $j = $length + 1;
126 6
        foreach ($chars as $i => $char) {
127 6
            $sum += ($dictionary[$char] ?? 0) * ($j - $i);
128
        }
129 6
        $digit = strval(11 - $sum % 11);
130 6
        if ('11' === $digit) {
131 2
            $digit = '0';
132 4
        } elseif ('10' === $digit) {
133 3
            $digit = 'A';
134
        }
135 6
        return $digit;
136
    }
137
138
    /**
139
     * The date is always from the year 2000 since RFC does not provide century and 000229 is valid.
140
     * Please, change this function on year 2100!
141
     *
142
     * @param string $rfc
143
     * @return int
144
     */
145 48
    public static function obtainDate(string $rfc): int
146
    {
147
        // rfc is multibyte
148 48
        $begin = (12 === mb_strlen($rfc)) ? 3 : 4;
149
        // strdate is not multibyte
150 48
        $strdate = strval(mb_substr($rfc, $begin, 6));
151 48
        if (6 !== strlen($strdate)) {
152 1
            return 0;
153
        }
154 47
        $parts = str_split($strdate, 2);
155
        // year 2000 is leap year (%4 & %100 & %400)
156 47
        $date = mktime(0, 0, 0, (int) $parts[1], (int) $parts[2], (int) ('20' . $parts[0]));
157 47
        if (false === $date) {
158
            /** @codeCoverageIgnore it is unlikely to enter this block */
159
            return 0;
160
        }
161 47
        if (date('ymd', $date) !== $strdate) {
162 16
            return 0;
163
        }
164 32
        return $date;
165
    }
166
}
167