saldanhakun/brazilian-validators

Implementation of Constraints and Validators for brazilian common entities like CPF, CNPJ, phones and more.

1.1.0 2021-12-16 22:12 UTC

This package is auto-updated.

Last update: 2024-04-24 15:59:42 UTC


README

Welcome to Brazilian Validators for Symfony!

What is this repository for?

In developing application for clients on Nautilos, validating information specifically for brazilian audiences was commonplace. Wherever a CPF validation was required, a small function was grabbed from a previous project and merged in the core of the project. This, albeit workable, was inefficient, and prone to upgrade nightmares if some bug or new norm was required.

So this project is an attempt to remedy that situation, providing an easy to use symfony-compatible way to handle the same stuff. The idea is simple enough:

  1. Require with composer (standard procedure)
  2. Set up the Annotations
  3. See the magic validation in place :-)

How do I get set up?

The idea is to really avoid any special setup or cumbersomeness. With that in mind, we aimed for simple installation with default options. If required, some extra steps could be done to change a few behaviours, but usually none of those are ever needed.

Installation steps:

Require with Composer

composer require saldanhakun/brazilian-validators

Set up the Annotations (in the example, we used CPF):

<?php

namespace App\Entity;

...
use Saldanhakun\BrazilianValidators\Constraint as Brazil;
...

/**
 * @ORM\Entity(repositoryClass=AppEntityRepository::class)
 */
class AppEntity
{
    ...
    
    /**
     * @ORM\Column(type="string", length=14, unique=true) 
     * @Brazil\CPF() 
     */
    private $document;

    public function getDocument(): string
    {
        return $this->document;
    }
    
    public function setDocument(string $document): self
    {
        $this->document = $document;
        
        return $this;
    }
    
    ...
}

Translations

This project, at its current state, is not yet a valid Symfony Bundle. Therefore, it does not compute for automatic translation recognition, such as what happens with `symfony/form` bundle.

However, you will find a valid translation file, for a few (growing) languages, inside `Resources/translations. After installing this package, you can find such files as vendor/saldanhakun/brazilian-|validators/Resources/validators.pt_BR.yaml which you can append or copy to your translations/validators.pt_BR.yaml` in order to make use of the provided translations.

Of course, you should replace `pt_BR` with you actually language. The list of available translations, as of today, follows:

  • English (`fallback`)
  • Brazilian Portuguese (`pt_BR`)

Provided Validations

These are the currently available validators provided by the project:

  • `@Brazil\Cnpj` brazilian CNPJ, the national company unique identification (similar to VAT)
  • `@Brazil\Cpf` brazilian CPF, the national person unique identification
  • `@Brazil\Phone` brazilian common phone number, either a landline or mobile are accepted
  • `@Brazil\Landline` brazilian landline phone number only
  • `@Brazil\Mobile` brazilian mobile phone number only
  • `@Brazil\Service` brazilian especial service numbers only. Supports 3-digit (like police), 4-digit and 5-digit (like carriers)
  • `@Brazil\Toll` brazilian toll numbers, like free callers and such

Details about validation

The DV

"DV" stands for `Dígito Verificador, or Verification Code, and is usually represented as one or two digits at the end of the document, usually separated with a hifen. For instance, the CNPJ of "Banco do Brasil", the first brazilian Bank (instituted on 1808, way before CNPJ was even created) is 00.000.000/0001-91. The DV is 91`.

DV are usually calculated as some kind of sum check, or parity check. Two algorithms exists for this, the mod-11, which uses 2 digits for DV (as in CPF and CNPJ) and the mod-10, which uses just one (as in banks agencies and accounts, but may require a special handling of a tenth valid result, usually forcing as `0 or using X`).

Simple enough, a DV validation fails when the DV from the input doesn't match the calculated DV from the other digits from the input. For instance, `00.000.000/0001-91 is a valid CNPJ because calculating the **mod-11** DV for 000000000001 yields 91`. Should any digit change, due to incorrect typing or whatever, the DV would yield something else, and probably result in fail.

CNPJ

CNPJ stands for `Cadastro Nacional de Pessoa Jurídica`, or National Company Number (in a loosely translation). This is an unique identifier assigned to every company that is properly registered with the brazilian government.

It has 14 digits divided in 3 parts, each with a meaning:

  • The first 8 digits are the register base number. Assigned almost randomly, this number is unique to a business, and only shared with its own subsidiaries. This allows for almost 100 million registered companies in the country, across the history (there is no reusing of this base code).
  • The next 4 digits are the subsidiary ID. Starting from `0001`, each CNPJ can therefore allocate almost 10 thousand subsidiaries. Most companies don't have them, but some niches like bank agencies may come close to extrapolate this limit.
  • The last 2 digits are the DV, calculated with a mod-11 algorithm.

The punctuation is optional, but officially the register base uses dots (e.g. `00.000.000), the subsidiary is separated from the base with a slash / and the DV is separated from the subsidiary with a hifen -`. People usually mistake this punctuation when typing, or ignore them altogether, so validation must take that into account.

CNPJ Constraint Usage

You may decide how to store the validated document, but it is always recommended to use string fields, and never integers. With the punctuation, CNPJ has always 18 digits, and 14 with digits-only. The `CnpjValidator::normalize` method can be used to normalize inputs in the database, when used directly in the setter.

<?php

namespace App\Entity;

...
use Saldanhakun\BrazilianValidators\Constraint as Brazil;
use Saldanhakun\BrazilianValidators\Validator\CnpjValidator;
...

/**
 * @ORM\Entity(repositoryClass=CompanyRepository::class)
 */
class Company
{
    ...
    
    /**
     * When you want to store all CNPJs with full punctuation, which is
     * useful for easy printing of pretty and normalized output.
     * Here, we use 18 chars in storage, and allow for empty values 
     * @ORM\Column(type="string", length=CnpjValidator::ORM_LENGTH, nullable=true) 
     * @Brazil\Cnpj() 
     */
    private $document;

    public function getDocument(): ?string
    {
        // if your app has un-normalized data already stored, you may
        // want to call CnpjValidator::normalize here too, so that
        // normalization is guaranteed even if the record is not normalized
        // the database.
        return $this->document;
    }
    
    public function setDocument(?string $document): self
    {
        $this->document = CnpjValidator::normalize($document);
        
        return $this;
    }
    
    /**
     * If you prefer to store just digits, possibly because searching
     * may be easier to implement, the field must have 14 chars, and you
     * must normalize the input  
     * @ORM\Column(type="string", length=CnpjValidator::ORM_COLUMN_DIGITS_LENGTH, unique=true) 
     * @Brazil\CNPJ() 
     */
    private $document;

    public function getDocument(): string
    {
        // same as before, you may normalize when reading if your database
        // may contain un-normalized data.
        return (string) $this->document;
    }
    
    public function setDocument(string $document): self
    {
        $this->document = CnpjValidator::normalize($document, CnpjValidator::NORM_DIGITS);
        
        return $this;
    }
    
    ...
}

CNPJ Constraint Options

The CNPJ has only 2 options, and 4 messages. The messages are translatable as usual (via `translations/validators.yaml`), and represents:

  • `The value "{{ value }}" is not a valid document.`: General validation error (syntactic)
  • `The value "{{ value }}" does not have the expected length.`: Input does not have enough digits to be a valid CNPJ
  • `The document "{{ value }}" fails the validation.`: Input verification code does not match what was expected (semantic error)
  • `The document "{{ value }}" fails the validation. Expected "{{ dv }}", got "{{ input_dv }}".`: Input verification code does not match what was expected (semantic error)

The two available options are these:

  • `pad_left (**bool**, defaults to true`): If input without leading zeroes is allowed and normalized. Usually safe.
  • `hint_dv (**string**, defaults to dev): If the correct DV should be hinted in the message. Usually only available in DEV environments. Valid options are no, yes or the key to some environment (e.g. dev, test, prod`)

CPF

CPF stands for `Cadastro de Pessoa Física` or Person Number (loosely). It resembles CNPJ in many ways, which is by design, as both handle two sides of a juridic representation. Each person receives an unique CPF (nowadays, as soon as their birth is recorded).

It has 11 digits divided in 2 parts, each with a meaning:

  • The first 9 digits are the register base number. Assigned almost randomly, this number is unique to a person. This allows for almost 1 billion registered citizens in the country, across the history (there is no reusing of this base code).
  • The last 2 digits are the DV, calculated with a mod-11 algorithm.

The punctuation is optional, but officially the register base uses dots (e.g. `000.000.000) and the DV is separated from the base with a hifen -`. People usually mistake this punctuation when typing, or ignore them altogether, so validation must take that into account.

CPF Constraint Usage

You may decide how to store the validated document, but it is always recommended to use string fields, and never integers. With the punctuation, CPF has always 14 digits, and 11 with digits-only. The `CpfValidator::normalize` method can be used to normalize inputs in the database, when used directly in the setter.

<?php

namespace App\Entity;

...
use Saldanhakun\BrazilianValidators\Constraint as Brazil;
use Saldanhakun\BrazilianValidators\Validator\CpfValidator;
...

/**
 * @ORM\Entity(repositoryClass=PersonRepository::class)
 */
class Person
{
    ...
    
    /**
     * When you want to store all CPFs with full punctuation, which is 
     * useful for easy printing of pretty and normalized output.
     * Here, we use 14 chars in storage, and allow for empty values 
     * @ORM\Column(type="string", length=CpfValidator::ORM_LENGTH, nullable=true) 
     * @Brazil\CPF() 
     */
    private $document;

    public function getDocument(): ?string
    {
        // if your app has un-normalized data already stored, you may
        // want to call CpfValidator::normalize here too, so that
        // normalization is garanteed even if the record is not normalized
        // the database.
        return $this->document;
    }
    
    public function setDocument(?string $document): self
    {
        $this->document = CpfValidator::normalize($document);
        
        return $this;
    }
    
    /**
     * If you prefer to store just digits, possibly because searching
     * may be easier to implement, the field must have 11 chars, and you
     * must normalize the input  
     * @ORM\Column(type="string", length=CpfValidator::ORM_COLUMN_DIGITS_LENGTH, unique=true) 
     * @Brazil\CPF() 
     */
    private $document;

    public function getDocument(): string
    {
        // same as before, you may normalize when reading if your database
        // may contain un-normalized data.
        return (string) $this->document;
    }
    
    public function setDocument(string $document): self
    {
        $this->document = CpfValidator::normalize($document, CpfValidator::NORM_DIGITS);
        
        return $this;
    }
    
    ...
}

CPF Constraint Options

The CPF has only 2 options, and 4 messages. The messages are translatable as usual (via `translations/validators.yaml`), and represents:

  • `The value "{{ value }}" is not a valid document.`: General validation error (syntactic)
  • `The value "{{ value }}" does not have the expected length.`: Input does not have enough digits to be a valid CPF
  • `The document "{{ value }}" fails the validation.`: Input verification code does not match what was expected (semantic error)
  • `The document "{{ value }}" fails the validation. Expected "{{ dv }}", got "{{ input_dv }}".`: Input verification code does not match what was expected (semantic error)

The two available options are these:

  • `pad_left (**bool**, defaults to true`): If input without leading zeroes is allowed and normalized. Usually safe.
  • `hint_dv (**string**, defaults to dev): If the correct DV should be hinted in the message. Usually only available in DEV environments. Valid options are no, yes or the key to some environment (e.g. dev, test, prod`)

Mixed CPF and CNPJ usage

In some cases, a field must store and validate either a CPF or a CNPJ, depending on some flag. This requires a ORM column configuration that allows for both, and also a validation strategy that guarantee the required document type was supplied.

The validators here implemented allow this in a easy faction, requiring just a little more configuration on the validation definitions:

<?php

namespace App\Entity;

...
use Symfony\Component\Validator\Constraints as Assert;
use Saldanhakun\BrazilianValidators\Constraint as Brazil;
use Saldanhakun\BrazilianValidators\Validator\CpfValidator;
use Saldanhakun\BrazilianValidators\Validator\CnpjValidator;
...

/**
 * @ORM\Entity(repositoryClass=ClientRepository::class)
 */
class Client
{
    ...
    
    /**
     * This column must accept strings whose length is compliant with
     * CPF and CNPJ. The validators already know that exact value and
     * in fact uses it as the default column length. Good compatibility!
     * @ORM\Column(type="string", length=CpfValidator::ORM_LENGTH, nullable=false)
     * For validation, there is no need of a "third, hybrid implementation",
     * because one of Symfony's core validators can handle the logic here:  
     * @Assert\AtLeastOneOf({
     *     @Brazil\Cpf(),
     *     @Brazil\Cnpj(),
     * })
    */
    private $document;

    public function getDocument(): ?string
    {
        // In this situation, a full normalize strategy is the way to go.
        return $this->document;
    }
    
    public function setDocument(?string $document): self
    {
        if (CpfValidator::canNormalize($document)) {
            // Looks like a CPF, so normalize as such
            $this->document = CpfValidator::normalize($document);
        }
        else {
            // Should then be either blank, or a CNPJ
            $this->document = CnpjValidator::normalize($document);
        }
        
        return $this;
    }
    
    // This takes care of the document validation per se. But there is
    // one more step that probaby should be taken: validating that a
    // CPF (or CNPJ) was received when one was actually required.
    // This requires a "flag" field, and a special validation for it.
    /**
     * This column will store the Client type, and from this information
     * the validation will guarantee that an appropriate document is valid.
     * There are (at least) 2 ways of doing this: using a boolean flag like
     * "is_company" that switches between CPF and CNPJ, or using a more
     * descriptive, enum-like flag. We will do that in this example.    
     * @ORM\Column(type="string", length=15, nullable=false)
     * @Assert\Choice(choices=self::CLIENT_TYPES) 
     * @Assert\Callback(callback="validateClientDocument")
    */
    private $type;
    public const TYPE_PERSON = 'person';
    public const TYPE_COMPANY = 'company';
    //public const TYPE_WHATEVER = 'yours-needs'; // not so binary!
    public const CLIENT_TYPES = [
        self::TYPE_PERSON,
        self::TYPE_COMPANY,
    ];
    
    public function getType(): string
    {
        return (string) $this->type;
    }
    
    public function setType(string $type): self
    {
        $this->type = $type;
        
        return $this;
    }
    
    public function validateClientDocument(ExecutionContextInterface $context): void
    {
        if ($this->getType() === self::TYPE_PERSON) {
            if (!CpfValidator::canNormalize($this->getDocument())) {
                $context->buildValidation(Brazil\Cpf::$message)
                    ->addViolation();
            }
        }
        elseif ($this->getType() === self::TYPE_COMPANY) {
            if (!CnpjValidator::canNormalize($this->getDocument())) {
                $context->buildValidation(Brazil\Cnpj::$message)
                    ->addViolation();
            }
        }
    }
    
    ...
}

Support

Dependencies

This library was developing with PHP 7.2+ compatibility in mind, so it should be safe enough to use with modern PHP projects (^7.4, 8+) and at the same time be usable in older projects. Maybe with a few tweaks, it can be made compatible with PHP 5.4.

Contribution guidelines

At the current time, this project is not accepting collaborations yet. Mainly because I don't have all things in place to handle feedbacks, pull requests, etc.

This will change, as I really intend this to be a useful and supported project, albeit with a niche usability and small popularity.

Who do I talk to?

If you have any suggestion, critics, or even if you need help integrating in you own project, you can reach me by e-mail, and I will do my best to help. Just don't expect a SLA or anything like that, this is a side project for me, after all.

You can reach me at saldanha AT nautilos.com.br

This project is distributed under the GPL - GNU General Public License v3.0 v3.0. See COPYING for more about it.