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)); |
|
|
|
|
121
|
6 |
|
$chars = str_split(str_replace('Ñ', '#', $rfc), 1); |
122
|
6 |
|
array_pop($chars); // remove predefined checksum |
|
|
|
|
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
|
|
|
|