Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
CpfCnpjValidator
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
7 / 7
33
100.00% covered (success)
100.00%
1 / 1
 validate
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 passes
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 passesCpf
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 passesCnpj
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 sanitize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateCpf
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 validateCnpj
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelEloquentPlus\Validators;
6
7use Closure;
8use Illuminate\Contracts\Validation\ValidationRule;
9
10/**
11 * Validation rule for Brazilian CPF (11 digits) and CNPJ (14 digits).
12 *
13 * Validates the mathematical check digits according to the official algorithm.
14 * Automatically removes non-numeric characters before validation.
15 *
16 * Usage:
17 * ```php
18 * $rules = [
19 *     'document' => ['required', new CpfCnpjValidator()],
20 * ];
21 * ```
22 *
23 * Accepts formats:
24 * - CPF: "123.456.789-00" or "12345678900"
25 * - CNPJ: "12.345.678/0001-00" or "12345678000100"
26 *
27 * @package DevToolbelt\LaravelEloquentPlus\Validators
28 */
29final class CpfCnpjValidator implements ValidationRule
30{
31    private const int CPF_LENGTH = 11;
32    private const int CNPJ_LENGTH = 14;
33
34    /**
35     * Run the validation rule.
36     *
37     * @param string $attribute The attribute name being validated
38     * @param mixed $value The value to validate
39     * @param Closure(string, string|null=): \Illuminate\Translation\PotentiallyTranslatedString $fail
40     * @return void
41     */
42    public function validate(string $attribute, mixed $value, Closure $fail): void
43    {
44        if ($value === null || $value === '') {
45            return;
46        }
47
48        $value = $this->sanitize((string) $value);
49        $length = strlen($value);
50
51        if ($length === self::CPF_LENGTH) {
52            if (!$this->validateCpf($value)) {
53                $fail('The :attribute is not a valid CPF.');
54            }
55            return;
56        }
57
58        if ($length === self::CNPJ_LENGTH) {
59            if (!$this->validateCnpj($value)) {
60                $fail('The :attribute is not a valid CNPJ.');
61            }
62            return;
63        }
64
65        $fail('The :attribute must be a valid CPF (11 digits) or CNPJ (14 digits).');
66    }
67
68    /**
69     * Determine if the validation rule passes.
70     *
71     * This method can be used for simple boolean validation checks,
72     * useful for Validator::extend() registration.
73     *
74     * @param mixed $value The value to validate
75     * @return bool True if valid, false otherwise
76     */
77    public function passes(mixed $value): bool
78    {
79        if ($value === null || $value === '') {
80            return true;
81        }
82
83        $value = $this->sanitize((string) $value);
84        $length = strlen($value);
85
86        if ($length === self::CPF_LENGTH) {
87            return $this->validateCpf($value);
88        }
89
90        if ($length === self::CNPJ_LENGTH) {
91            return $this->validateCnpj($value);
92        }
93
94        return false;
95    }
96
97    /**
98     * Determine if the value is a valid CPF only.
99     *
100     * @param mixed $value The value to validate
101     * @return bool True if valid CPF, false otherwise
102     */
103    public function passesCpf(mixed $value): bool
104    {
105        if ($value === null || $value === '') {
106            return true;
107        }
108
109        $value = $this->sanitize((string) $value);
110
111        if (strlen($value) !== self::CPF_LENGTH) {
112            return false;
113        }
114
115        return $this->validateCpf($value);
116    }
117
118    /**
119     * Determine if the value is a valid CNPJ only.
120     *
121     * @param mixed $value The value to validate
122     * @return bool True if valid CNPJ, false otherwise
123     */
124    public function passesCnpj(mixed $value): bool
125    {
126        if ($value === null || $value === '') {
127            return true;
128        }
129
130        $value = $this->sanitize((string) $value);
131
132        if (strlen($value) !== self::CNPJ_LENGTH) {
133            return false;
134        }
135
136        return $this->validateCnpj($value);
137    }
138
139    /**
140     * Remove all non-numeric characters from the value.
141     *
142     * @param string $value
143     * @return string
144     */
145    private function sanitize(string $value): string
146    {
147        return (string) preg_replace('/\D/', '', trim($value));
148    }
149
150    /**
151     * Validate a CPF number using the official algorithm.
152     *
153     * @param string $cpf The 11-digit CPF number
154     * @return bool True if valid, false otherwise
155     */
156    private function validateCpf(string $cpf): bool
157    {
158        // Reject sequences of repeated digits (e.g., 111.111.111-11)
159        if ((bool) preg_match('/(\d)\1{10}/', $cpf)) {
160            return false;
161        }
162
163        // Validate check digits
164        for ($t = 9; $t < 11; $t++) {
165            $sum = 0;
166
167            for ($c = 0; $c < $t; $c++) {
168                $sum += (int) $cpf[$c] * (($t + 1) - $c);
169            }
170
171            $digit = ((10 * $sum) % 11) % 10;
172
173            if ((int) $cpf[$t] !== $digit) {
174                return false;
175            }
176        }
177
178        return true;
179    }
180
181    /**
182     * Validate a CNPJ number using the official algorithm.
183     *
184     * @param string $cnpj The 14-digit CNPJ number
185     * @return bool True if valid, false otherwise
186     */
187    private function validateCnpj(string $cnpj): bool
188    {
189        // Reject sequences of repeated digits (e.g., 11.111.111/1111-11)
190        if ((bool) preg_match('/(\d)\1{13}/', $cnpj)) {
191            return false;
192        }
193
194        // Validate first check digit
195        $weights = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
196        $sum = 0;
197
198        for ($i = 0; $i < 12; $i++) {
199            $sum += (int) $cnpj[$i] * $weights[$i];
200        }
201
202        $digit = $sum % 11;
203        $digit = $digit < 2 ? 0 : 11 - $digit;
204
205        if ((int) $cnpj[12] !== $digit) {
206            return false;
207        }
208
209        // Validate second check digit
210        $weights = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
211        $sum = 0;
212
213        for ($i = 0; $i < 13; $i++) {
214            $sum += (int) $cnpj[$i] * $weights[$i];
215        }
216
217        $digit = $sum % 11;
218        $digit = $digit < 2 ? 0 : 11 - $digit;
219
220        return (int) $cnpj[13] === $digit;
221    }
222}