diff --git a/AGENTS.md b/AGENTS.md index 1b47d33..42b2c5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,8 @@ This file is the **primary entry point** for AI agents working in the PHP subrepo. Read this file first. It provides baseline rules for every task and links to the specialized harnesses in [`agents/`](agents/) for task-specific instructions. +**Reference standard:** `packages/utils` and `packages/cnpj-*` reflect the current v2 conventions. `packages/cpf-*` (except `cpf-dv`) still follow older v1 patterns (PHPUnit, legacy namespaces, `*Test.php` files) and are being migrated — match `cnpj-*` for new or updated packages. + ## Instruction precedence When instructions conflict, **the more specific scope wins**: @@ -18,7 +20,7 @@ Apply every layer relevant to your task. Where a package-level `AGENTS.md` or `a ### Runtime and package manager -The project is managed by **Composer**. Each package has its own `composer.json` and `vendor/` directory — there is no hoisted monorepo install. Install dependencies per package: +The project uses **PHP** (`^8.2`) and **Composer**. Each package has its own `composer.json` and `vendor/` directory — there is no hoisted monorepo install. Install dependencies per package: ```bash composer install --working-dir=packages/ @@ -102,6 +104,12 @@ See [`agents/ci-release.md`](agents/ci-release.md) for the full pipeline (matrix ## Package-specific guidelines +### PHP version and strictness + +- Require `"php": "^8.2"` in all modern (v2) packages. +- Every PHP file must start with `declare(strict_types=1);`. +- Use typed properties, parameters, and return types. + ### Lint / static analysis (DRY) See [`agents/lint-config.md`](agents/lint-config.md) for shared config invocation patterns, PHPStan level, and the rule against adding per-package lint config files. diff --git a/packages/br-utils/.pest.config.xml b/packages/br-utils/.pest.config.xml new file mode 100644 index 0000000..f4a7df9 --- /dev/null +++ b/packages/br-utils/.pest.config.xml @@ -0,0 +1,33 @@ + + + + + tests/specs/ + + + + + src/ + + + vendor/ + tests/ + + + + + + + + + diff --git a/packages/br-utils/phpunit.xml b/packages/br-utils/.phpunit.config.xml similarity index 93% rename from packages/br-utils/phpunit.xml rename to packages/br-utils/.phpunit.config.xml index 8868a8e..b11764e 100644 --- a/packages/br-utils/phpunit.xml +++ b/packages/br-utils/.phpunit.config.xml @@ -12,7 +12,7 @@ > - tests/ + tests/phpunit/ diff --git a/packages/br-utils/CHANGELOG.md b/packages/br-utils/CHANGELOG.md index e78e3ac..c1ffb6f 100644 --- a/packages/br-utils/CHANGELOG.md +++ b/packages/br-utils/CHANGELOG.md @@ -1,5 +1,59 @@ # lacus/br-utils +## 2.0.0 + +### 🎉 v2 at a glance 🎊 + +- 🆕 **Alphanumeric CNPJ** — Full support for the new [14-character alphanumeric CNPJ](https://www.gov.br/receitafederal/pt-br/assuntos/noticias/2023/julho/cnpj-alfa-numerico) via upgraded `lacus/cnpj-utils` ^2.0 and bundled `lacus/cnpj-*` v2 components. +- ⚙️ **Validator options** — New `CnpjValidatorOptions`; configure `type` and `caseSensitive` on `BrUtils`, `CnpjUtils`, or per `isValid()` call. +- 🛡️ **Structured CNPJ errors** — Typed exceptions from bundled CNPJ packages propagate through `$brUtils->cnpj` for clearer error handling. +- 📁 **Namespace realignment** — `BrUtils` lives in `Lacus\`; CPF and CNPJ utilities move to `Lacus\BrUtils\Cpf\` and `Lacus\BrUtils\Cnpj\`. +- ⚙️ **`BrUtils` constructor** — Accepts pre-built `CpfUtils` / `CnpjUtils` instances or config arrays with `*Options` objects (including CNPJ `validator` settings). +- 📦 **Unified import surface** — CPF component classes and helpers stay under `Lacus\BrUtils\Cpf\`; CNPJ symbols are provided by bundled packages under `Lacus\BrUtils\Cnpj\`. + +### BREAKING CHANGES + +- **Namespaces**: + - `BrUtils` is now `Lacus\BrUtils` (was `Lacus\BrUtils\BrUtils`); + - `CpfUtils` → `Lacus\BrUtils\Cpf\CpfUtils`; + - `CnpjUtils` → `Lacus\BrUtils\Cnpj\CnpjUtils`. +- **PHP 8.2** — Minimum PHP raised from `>=8.1` to `^8.2`. +- **`BrUtils` constructor** — `$cpf` is the first argument (was second); each parameter accepts a utils instance or a named config array spread into the utils constructor. +- **Autoload root** — PSR-4 prefix changed from `Lacus\BrUtils\` to `Lacus\` (only `BrUtils.php` at the root; domain code lives under `src/BrUtils/`). +- **CNPJ local wrappers removed** — `CnpjFormatter`, `CnpjGenerator`, and `CnpjValidator` are no longer defined in this package; import them from `lacus/cnpj-fmt`, `lacus/cnpj-gen`, and `lacus/cnpj-val` (still under `Lacus\BrUtils\Cnpj\`). +- **CNPJ helpers** — `cnpj_fmt()`, `cnpj_gen()`, and `cnpj_val()` are no longer autoloaded by this package; they are provided by the bundled CNPJ component packages (same namespace, updated v2 signatures). +- **CNPJ API** — Inherits v2 changes from bundled `lacus/cnpj-*` packages: + - **Alphanumeric CNPJ** — letters are kept during sanitization; default validation is **alphanumeric** (pass `type: 'numeric'` to restore legacy numeric-only behavior); + - **Signatures** — `format()` / `isValid()` accept `string|list`; `format()` adds `encode` and `CnpjFormatterOptions`; `generate()` adds `CnpjGeneratorOptions` and `CnpjType`; `isValid()` adds `CnpjValidatorOptions`; + - **`onFail` default** — CNPJ formatter `onFail` now returns `''` on invalid length (v1 returned the original input); + - **Options model** — `*Options` use property access and `overrides` merging; `merge()` and getter/setter style removed; + - **Check digits** — generation and validation delegate to `lacus/cnpj-dv` (`CnpjCheckDigits`) instead of inline/`CnpjGeneratorVerifierDigit`; + - **Input errors** — invalid input types throw typed `*InputTypeError` exceptions instead of native `TypeError` or unspecified behavior. +- **Dependencies** — Runtime requires `lacus/cpf-utils` ^1.1 and `lacus/cnpj-utils` ^2.0 (were ^1.0 each). + +### New Features + +- **`BrUtils` dependency injection** — Pass pre-built `CpfUtils` / `CnpjUtils` instances or spread config arrays with `formatter`, `generator`, and (for CNPJ) `validator` keys accepting `*Options` objects. +- **CPF options re-exports** — `CpfFormatterOptions` and `CpfGeneratorOptions` are now available under `Lacus\BrUtils\Cpf\`. +- **Alphanumeric CNPJ** — Format, generate, and validate the new 14-character alphanumeric CNPJ through `$brUtils->cnpj` (digits and `A`–`Z`, uppercased on input). +- **`encode` option** — `$brUtils->cnpj->format()` can URL-encode the formatted CNPJ (from `lacus/cnpj-fmt` ^2.0). +- **Array input** — `$brUtils->cnpj->format()` and `->isValid()` concatenate a `list` (e.g. grouped or formatted segments). +- **`CnpjValidatorOptions`** — Configure `type` (`CnpjValidationType::Alphanumeric` or `::Numeric`) and `caseSensitive` on the `BrUtils` / `CnpjUtils` instance, per `isValid()` call, or via `getValidator()->getOptions()`. +- **`CnpjType` generation modes** — `$brUtils->cnpj->generate()` supports `Numeric`, `Alphabetic`, and `Alphanumeric` output via the `CnpjType` enum (from `lacus/cnpj-gen` ^2.1). +- **Alphanumeric prefix generation** — `$brUtils->cnpj->generate()` accepts alphanumeric prefixes (stripped, uppercased, capped at 12 base characters). +- **Structured CNPJ exceptions** — Typed `TypeError` / `Exception` hierarchies from `lacus/cnpj-fmt`, `lacus/cnpj-gen`, and `lacus/cnpj-val` propagate through `$brUtils->cnpj`. + +### Improvements + +- **New PT-BR documentation** — New [README in Brazilian Portuguese](./README.pt.md). +- **Documentation** — README and README.pt.md updated for the v2 API (namespaces, constructor, CNPJ validator options, bundled-package imports). +- **CNPJ dependency alignment** — Transitive runtime updated to `lacus/cnpj-fmt` ^2.0, `lacus/cnpj-gen` ^2.1, `lacus/cnpj-val` ^2.0, and `lacus/cnpj-dv` ^1.1. +- **CNPJ options objects** — Formatter, generator, and validator settings use `*Options` instances with per-call overrides via named parameters or an options object. +- **`CpfUtils` constructor** — Accepts `CpfFormatterOptions` / `CpfGeneratorOptions` instances or option arrays when constructing directly or via `BrUtils`. +- **CNPJ generator reliability** — Internal retry when check-digit computation rejects a generated candidate (from `lacus/cnpj-gen` ^2.0). +- **CNPJ validator reuse** — `cnpj_val()` keeps the `CnpjValidator` instance alive across calls (from `lacus/cnpj-val` ^2.0). +- **CNPJ check-digit performance** — Faster `CnpjCheckDigits` engine used by generation and validation (from `lacus/cnpj-dv` ^1.1). + ## 1.0.0 ### Stable v1 API @@ -16,6 +70,6 @@ First stable release of **`lacus/br-utils`** — CPF and CNPJ formatting, genera - **Unified façade**: `BrUtils`, `CpfUtils`, and `CnpjUtils` expose `format()`, `generate()`, and `isValid()`; constructor accepts `formatter` and `generator` option arrays per document type. - **Component access**: `getFormatter()`, `getGenerator()`, and `getValidator()` expose the underlying instances for direct use. - **Scope**: numeric CPF only (11 digits); numeric CNPJ only (14 digits). -- **Dependencies**: `lacus/cpf-utils` `^1.1`, `lacus/cnpj-utils` `^1.1`, plus the underlying `lacus/cpf-*` and `lacus/cnpj-*` component packages. +- **Dependencies**: `lacus/cpf-utils` ^1.0 and `lacus/cnpj-utils` ^1.0 (transitive `lacus/cpf-*` and `lacus/cnpj-*` component packages). - **Runtime**: PHP `>=8.1`. - **Testing**: PHPUnit-based suite. diff --git a/packages/br-utils/README.md b/packages/br-utils/README.md index 98ee4fc..33b1a44 100644 --- a/packages/br-utils/README.md +++ b/packages/br-utils/README.md @@ -7,35 +7,92 @@ [![Last Update Date](https://img.shields.io/github/last-commit/LacusSolutions/br-utils-php)](https://github.com/LacusSolutions/br-utils-php) [![Project License](https://img.shields.io/github/license/LacusSolutions/br-utils-php)](https://github.com/LacusSolutions/br-utils-php/blob/main/LICENSE) -Toolkit to handle the main operations with Brazilian-related data for PHP programming language: +> 🚀 **Full support for the [new alphanumeric CNPJ format](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).** + +> 🌎 [Acessar documentação em português](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/br-utils/README.pt.md) + +A PHP toolkit to handle the main operations with Brazilian-related data: CPF (Individual's Taxpayer ID) and CNPJ (Business Tax ID). It provides a top-level `BrUtils` wrapper around [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) and [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils), exposing all bundled resources under unified namespaces. -- CPF (personal ID) ([demo](https://cpf-utils.vercel.app/)) -- CNPJ (employer ID) ([demo](https://cnpj-utils.vercel.app/)) ## PHP Support -| ![PHP 8.1](https://img.shields.io/badge/PHP-8.1-777BB4?logo=php&logoColor=white) | ![PHP 8.2](https://img.shields.io/badge/PHP-8.2-777BB4?logo=php&logoColor=white) | ![PHP 8.3](https://img.shields.io/badge/PHP-8.3-777BB4?logo=php&logoColor=white) | ![PHP 8.4](https://img.shields.io/badge/PHP-8.4-777BB4?logo=php&logoColor=white) | -|--- | --- | --- | --- | +| ![PHP 8.2](https://img.shields.io/badge/PHP-8.2-777BB4?logo=php&logoColor=white) | ![PHP 8.3](https://img.shields.io/badge/PHP-8.3-777BB4?logo=php&logoColor=white) | ![PHP 8.4](https://img.shields.io/badge/PHP-8.4-777BB4?logo=php&logoColor=white) | ![PHP 8.5](https://img.shields.io/badge/PHP-8.5-777BB4?logo=php&logoColor=white) | +| --- | --- | --- | --- | | Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | +## Features + +- ✅ **Unified top-level API**: One `BrUtils` instance with `$cpf` and `$cnpj` domain accessors +- ✅ **Bundled domains**: [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) and [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) installed together +- ✅ **Alphanumeric CNPJ**: Full support for the new alphanumeric CNPJ format (introduced in 2026) +- ✅ **Configurable defaults**: Set formatter, generator, and (for CNPJ) validator options on each domain instance +- ✅ **Per-call overrides**: Override any component option for a single method call +- ✅ **Dual API style**: Top-level façade (`BrUtils`), domain aggregators (`CpfUtils`, `CnpjUtils`), standalone components, and functional helpers +- ✅ **Shared namespaces**: CPF symbols under `Lacus\BrUtils\Cpf\`; CNPJ symbols under `Lacus\BrUtils\Cnpj\` +- ✅ **Typed error handling**: Dedicated exception hierarchies from bundled packages (CNPJ v2 `TypeError` / `Exception` model; CPF v1 `InvalidArgumentException` for invalid options) + ## Installation ```bash +# using Composer $ composer require lacus/br-utils ``` +This installs **`lacus/br-utils`** together with [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) and [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) (which in turn pulls in the CNPJ component packages). You do **not** need separate `composer require` calls for the domain packages when using **`lacus/br-utils`**. + ## Import +Pick the API that fits your use case. + +**Top-level façade:** + ```php cpf->format($cpf); // returns '111.444.777-35' -echo $brUtils->cpf->isValid($cpf); // returns true -echo $brUtils->cpf->generate(); // returns '12345678901' +use Lacus\BrUtils; -// CNPJ operations +$utils = new BrUtils(); +$cpf = '11144477735'; $cnpj = '03603568000195'; -echo $brUtils->cnpj->format($cnpj); // returns '03.603.568/0001-95' -echo $brUtils->cnpj->isValid($cnpj); // returns true -echo $brUtils->cnpj->generate(); // returns '65453043000178' -``` -#### With Configuration Options +$utils->cpf->format($cpf); // '111.444.777-35' +$utils->cpf->isValid($cpf); // true +$utils->cpf->generate(); // e.g. '11508890048' -You can configure both CPF and CNPJ utilities with custom options: +$utils->cnpj->format($cnpj); // '03.603.568/0001-95' +$utils->cnpj->isValid($cnpj); // true +$utils->cnpj->generate(); // e.g. '1GJTR3J3XSSA96' +``` + +**With domain aggregators:** ```php -$brUtils = new BrUtils( - cpf: [ - 'formatter' => [ - 'hidden' => true, - 'hiddenKey' => '#', - 'hiddenStart' => 3, - 'hiddenEnd' => 9 - ], - 'generator' => [ - 'format' => true - ] - ], - cnpj: [ - 'formatter' => [ - 'hidden' => true, - 'hiddenKey' => '#', - 'hiddenStart' => 5, - 'hiddenEnd' => 13 - ], - 'generator' => [ - 'format' => true - ] - ] -); +cpf->format($cpf); // returns '111.###.###-##' -echo $brUtils->cnpj->format($cnpj); // returns '03.603.###/####-##' -echo $brUtils->cpf->generate(); // returns '123.456.789-01' -echo $brUtils->cnpj->generate(); // returns '73.008.535/0005-06' +(new CpfUtils())->format($cpf); // '111.444.777-35' +(new CnpjUtils())->format($cnpj); // '03.603.568/0001-95' +(new CpfUtils())->isValid($cpf); // true +(new CnpjUtils())->isValid($cnpj); // true ``` -### Individual Utility Classes - -You can also use the individual utility classes directly: +**With functional helpers:** ```php -// CPF utilities -$cpfUtils = new CpfUtils(); -$cpf = '11144477735'; +format($cpf); // returns '111.444.777-35' -echo $cpfUtils->isValid($cpf); // returns true -echo $cpfUtils->generate(); // returns '12345678901' +use function Lacus\BrUtils\Cpf\cpf_fmt; +use function Lacus\BrUtils\Cpf\cpf_val; +use function Lacus\BrUtils\Cnpj\cnpj_fmt; +use function Lacus\BrUtils\Cnpj\cnpj_val; -// CNPJ utilities -$cnpjUtils = new CnpjUtils(); +$cpf = '11144477735'; $cnpj = '03603568000195'; -echo $cnpjUtils->format($cnpj); // returns '03.603.568/0001-95' -echo $cnpjUtils->isValid($cnpj); // returns true -echo $cnpjUtils->generate(); // returns '65453043000178' +cpf_fmt($cpf); // '111.444.777-35' +cpf_val($cpf); // true +cnpj_fmt($cnpj); // '03.603.568/0001-95' +cnpj_val($cnpj); // true ``` -### Functional Programming +## Usage -The package also provides standalone functions for each operation: +You can work in four equivalent ways: -```php -$cpf = '11144477735'; -$cnpj = '03603568000195'; +1. **`BrUtils`** — single instance with shared defaults across both CPF and CNPJ domains. +2. **Domain aggregators** — `CpfUtils` and `CnpjUtils` directly (same classes used internally by `BrUtils`). +3. **Component classes** — `CpfFormatter`, `CnpjGenerator`, and so on. +4. **Functional helpers** — `cpf_fmt()`, `cnpj_gen()`, and related functions for one-off calls. -// CPF functions -echo cpf_fmt($cpf); // returns '111.444.777-35' -echo cpf_val($cpf); // returns true -echo cpf_gen(); // returns '12345678901' +All approaches expose the same options and behavior within each domain. For full option tables and component-specific details, see the README of each [bundled package](#bundled-packages). -// CNPJ functions -echo cnpj_fmt($cnpj); // returns '03.603.568/0001-95' -echo cnpj_val($cnpj); // returns true -echo cnpj_gen(); // returns '65453043000178' -``` +### `BrUtils` -## API Reference +- **`__construct`**: `new BrUtils($cpf = [], $cnpj = [])` -### CPF Operations + Each `$cpf` / `$cnpj` argument may be a pre-built `CpfUtils` / `CnpjUtils` instance or a configuration array spread into the corresponding utils constructor. Within that array, each resource key (`formatter`, `generator`, and `validator` for CNPJ) accepts either an options object or an associative array of option values. -#### Formatting (`cpf_fmt` / `CpfUtils::format`) + Example: `new BrUtils(cpf: ['formatter' => ['hidden' => true]], cnpj: ['validator' => ['type' => CnpjValidationType::Numeric]])`. -Formats a CPF string with customizable delimiters and masking options. +- **`$cpf`**, **`$cnpj`**: Property-style access to the domain utils instances (`CpfUtils` and `CnpjUtils`). + +- **`getCpfUtils()`**, **`getCnpjUtils()`**: Return the internal domain instances for direct use. ```php -cpf_fmt( - string $cpfString, - ?bool $escape = null, - ?bool $hidden = null, - ?string $hiddenKey = null, - ?int $hiddenStart = null, - ?int $hiddenEnd = null, - ?string $dotKey = null, - ?string $dashKey = null, - ?Closure $onFail = null, -): string -``` + $v` | Fallback function for invalid input | +$utils = new BrUtils(); -**Examples:** +$utils->cpf->format('11144477735'); // '111.444.777-35' +$utils->cpf->isValid('11144477735'); // true +$utils->cpf->generate(); // e.g. '11508890048' -```php -$cpf = '11144477735'; +$utils->cnpj->format('03603568000195'); // '03.603.568/0001-95' +$utils->cnpj->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' +$utils->cnpj->isValid('1QB5UKALPYFP59'); // true +$utils->cnpj->generate(format: true); // e.g. 'V1.J0V.8WE/DVZ7-50' +$utils->cnpj->generate( // e.g. '15381773354961' + type: CnpjGenerationType::Numeric, +); +``` -// Basic formatting -echo cpf_fmt($cpf); // '111.444.777-35' +### Instance defaults and per-call overrides -// With hidden digits -echo cpf_fmt($cpf, hidden: true); // '111.***.***-**' +```php +$utils = new BrUtils( + cpf: [ + 'formatter' => ['hidden' => true, 'hiddenKey' => '#'], + 'generator' => ['format' => true], + ], + cnpj: [ + 'formatter' => ['hidden' => true, 'hiddenKey' => '#'], + 'generator' => ['format' => true], + 'validator' => ['type' => CnpjValidationType::Numeric], + ], +); -// Custom delimiters -echo cpf_fmt($cpf, dotKey: '', dashKey: '_'); // '111444777_35' +$cpf = '11144477735'; +$cnpj = '03603568000195'; -// Custom hidden range -echo cpf_fmt($cpf, hidden: true, hiddenStart: 0, hiddenEnd: 6, hiddenKey: '#'); // '###.###.777-35' +$utils->cpf->format($cpf); // '111.###.###-##' +$utils->cpf->format($cpf, hidden: false); // '111.444.777-35' +$utils->cpf->generate(format: false); // e.g. '58450042259' + +$utils->cnpj->format($cnpj); // '03.603.###/####-##' +$utils->cnpj->format($cnpj, hidden: false); // '03.603.568/0001-95' +$utils->cnpj->isValid('1QB5UKALPYFP59'); // false +$utils->cnpj->isValid( // true + '1QB5UKALPYFP59', + type: CnpjValidationType::Alphanumeric, +); ``` -#### Generation (`cpf_gen` / `CpfUtils::generate`) +Passing a `CnpjFormatterOptions`, `CnpjGeneratorOptions`, or `CnpjValidatorOptions` instance to the `BrUtils` constructor stores that object by reference — mutating it later affects subsequent calls with no per-call override. -Generates valid CPF numbers with optional formatting and prefix completion. +### CPF operations -```php -cpf_gen( - ?bool $format = null, - ?string $prefix = null, -): string -``` +CPF methods are accessed via `$utils->cpf`, `CpfUtils`, or the `cpf_*()` helpers. CPF uses the v1 API from [`lacus/cpf-utils`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cpf-utils/README.md): string-only input, positional/named formatter and generator options, and no validator settings. -**Parameters:** +#### Formatting (`format` / `cpf_fmt`) | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `format` | `?bool` | `false` | Whether to format the output | -| `prefix` | `?string` | `''` | Prefix to complete with valid digits (1-9 digits) | +| `escape` | `?bool` | `false` | When `true`, HTML-escapes the final string | +| `hidden` | `?bool` | `false` | When `true`, replaces the inclusive index range `[hiddenStart, hiddenEnd]` on the normalized 11-digit string before punctuation is applied | +| `hiddenKey` | `?string` | `'*'` | Replacement for each hidden position | +| `hiddenStart` | `?int` | `3` | Start index `0`–`10` (inclusive) | +| `hiddenEnd` | `?int` | `10` | End index `0`–`10` (inclusive) | +| `dotKey` | `?string` | `'.'` | Separator between digit groups | +| `dashKey` | `?string` | `'-'` | Separator before the last two digits | +| `onFail` | `?\Closure` | see below | `Closure(mixed $value, Exception $e): string` — used when sanitized length ≠ 11 | -**Examples:** +Default **`onFail`** returns the original input unchanged. Invalid length does **not** throw from `format()`. ```php -// Generate random CPF -echo cpf_gen(); // '12345678901' - -// Generate formatted CPF -echo cpf_gen(format: true); // '123.456.789-01' +$cpf = '11144477735'; -// Complete a prefix -echo cpf_gen(prefix: '123456789'); // '12345678901' +$utils->cpf->format($cpf); // '111.444.777-35' +$utils->cpf->format($cpf, hidden: true, hiddenKey: '#'); // '111.###.###-##' +$utils->cpf->format($cpf, dotKey: '', dashKey: '_'); // '111444777_35' -// Complete and format -echo cpf_gen(prefix: '123456789', format: true); // '123.456.789-01' +cpf_fmt($cpf, hidden: true); // '111.***.***-**' ``` -#### Validation (`cpf_val` / `CpfUtils::isValid`) +#### Generation (`generate` / `cpf_gen`) -Validates CPF numbers using the official algorithm. +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `format` | `?bool` | `false` | When `true`, returns formatted CPF (`000.000.000-00`); otherwise returns compact 11-digit output | +| `prefix` | `?string` | `''` | Base seed for generation. Non-digit characters are stripped; only the first 9 digits (indexes `0`–`8`) are used | ```php -cpf_val(string $cpfString): bool +$utils->cpf->generate(); // e.g. '11508890048' +$utils->cpf->generate(format: true); // e.g. '661.134.831-00' +$utils->cpf->generate(prefix: '123456789'); // '12345678909' +cpf_gen(prefix: '123456789', format: true); // '123.456.789-09' ``` -**Examples:** +#### Validation (`isValid` / `cpf_val`) -```php -// Valid CPF -echo cpf_val('11144477735'); // true -echo cpf_val('111.444.777-35'); // true +Accepts formatted or unformatted CPF strings. Returns **`true`** or **`false`** without throwing for invalid CPF. -// Invalid CPF -echo cpf_val('11144477736'); // false +```php +$utils->cpf->isValid('11144477735'); // true +$utils->cpf->isValid('111.444.777-35'); // true +$utils->cpf->isValid('11144477736'); // false +cpf_val('11144477735'); // true ``` -### CNPJ Operations +### CNPJ operations -#### Formatting (`cnpj_fmt` / `CnpjUtils::format`) +CNPJ methods are accessed via `$utils->cnpj`, `CnpjUtils`, or the `cnpj_*()` helpers. CNPJ uses the v2 API from [`lacus/cnpj-utils`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-utils/README.md). -Formats a CNPJ string with customizable delimiters and masking options. - -```php -cnpj_fmt( - string $cnpjString, - ?bool $escape = null, - ?bool $hidden = null, - ?string $hiddenKey = null, - ?int $hiddenStart = null, - ?int $hiddenEnd = null, - ?string $dotKey = null, - ?string $slashKey = null, - ?string $dashKey = null, - ?Closure $onFail = null, -): string -``` +#### Formatting (`format` / `cnpj_fmt`) -**Parameters:** +Supports the same options as [`lacus/cnpj-fmt`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-fmt/README.md). Input accepts `string` or `list`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `escape` | `?bool` | `false` | Whether to HTML escape the result | -| `hidden` | `?bool` | `false` | Whether to hide digits with a mask | -| `hiddenKey` | `?string` | `'*'` | Character to replace hidden digits | -| `hiddenStart` | `?int` | `5` | Starting index for hidden range (0-13) | -| `hiddenEnd` | `?int` | `13` | Ending index for hidden range (0-13) | -| `dotKey` | `?string` | `'.'` | String to replace dot characters | -| `slashKey` | `?string` | `'/'` | String to replace slash character | -| `dashKey` | `?string` | `'-'` | String to replace dash character | -| `onFail` | `?callable` | `fn($v) => $v` | Fallback function for invalid input | - -**Examples:** +| `hidden` | `?bool` | `false` | When `true`, replaces the inclusive index range `[hiddenStart, hiddenEnd]` on the normalized 14-character string before punctuation is applied | +| `hiddenKey` | `?string` | `'*'` | Replacement for each hidden position (may be multi-character or empty); must not use disallowed key characters | +| `hiddenStart` | `?int` | `5` | Start index `0`–`13` (inclusive) | +| `hiddenEnd` | `?int` | `13` | End index `0`–`13` (inclusive); if `hiddenStart > hiddenEnd`, they are swapped | +| `dotKey` | `?string` | `'.'` | Separator between groups `XX` / `XXX` / `XXX` | +| `slashKey` | `?string` | `'/'` | Separator before the branch block | +| `dashKey` | `?string` | `'-'` | Separator before the last two characters | +| `escape` | `?bool` | `false` | When `true`, HTML-escapes the final string | +| `encode` | `?bool` | `false` | When `true`, URL-encodes the final string | +| `onFail` | `?\Closure` | see below | `Closure(mixed $value, CnpjFormatterException $e): string` — used when sanitized length ≠ 14 | + +Default **`onFail`** returns an empty string. Wrong input types throw **`CnpjFormatterInputTypeError`**. ```php $cnpj = '03603568000195'; -// Basic formatting -echo cnpj_fmt($cnpj); // '03.603.568/0001-95' +$utils->cnpj->format($cnpj); // '03.603.568/0001-95' +$utils->cnpj->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' +$utils->cnpj->format( // '03.603.###/####-##' + $cnpj, + hidden: true, + hiddenKey: '#', +); +$utils->cnpj->format( // '03603568|0001_95' + $cnpj, + dotKey: '', + slashKey: '|', + dashKey: '_', +); + +cnpj_fmt($cnpj); // '03.603.568/0001-95' +``` -// With hidden digits -echo cnpj_fmt($cnpj, hidden: true); // '03.603.***/****-**' +#### Generation (`generate` / `cnpj_gen`) -// Custom delimiters -echo cnpj_fmt($cnpj, dotKey: '', slashKey: '|', dashKey: '_'); // '03603568|0001_95' +Supports the same options as [`lacus/cnpj-gen`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-gen/README.md). -// Custom hidden range -echo cnpj_fmt($cnpj, hidden: true, hiddenStart: 2, hiddenEnd: 8, hiddenKey: '#'); // '03###.###/0001-95' -``` +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `format` | `?bool` | `false` | When `true`, returns formatted CNPJ (`XX.XXX.XXX/XXXX-XX`); otherwise returns compact 14-character output | +| `prefix` | `?string` | `''` | Base seed for generation. Non-alphanumeric chars are stripped, letters are uppercased, and only first 12 chars (indexes `0`–`11`) are used; characters at index `12+` are ignored | +| `type` | `CnpjGenerationType\|'alphanumeric'\|'alphabetic'\|'numeric'\|null` | `CnpjGenerationType::Alphanumeric` | Character family used for generated base positions | -#### Generation (`cnpj_gen` / `CnpjUtils::generate`) +`prefix` validation rules: -Generates valid CNPJ numbers with optional formatting and prefix completion. +- base ID `00000000` is rejected (when first 8 chars are present) +- branch ID `0000` is rejected (when chars 9–12 are present) +- 12 repeated numeric digits are rejected (e.g. `111111111111`) ```php -cnpj_gen( - ?bool $format = null, - ?string $prefix = null, -): string +$utils->cnpj->generate(); // e.g. '1GJTR3J3XSSA96' +$utils->cnpj->generate(format: true); // e.g. 'V1.J0V.8WE/DVZ7-50' +$utils->cnpj->generate( // e.g. '12345678855883' + prefix: '12345678', + type: CnpjGenerationType::Numeric, +); ``` -**Parameters:** +#### Validation (`isValid` / `cnpj_val`) + +Supports the same options as [`lacus/cnpj-val`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-val/README.md). Input accepts `string` or `list`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `format` | `?bool` | `false` | Whether to format the output | -| `prefix` | `?string` | `''` | Prefix to complete with valid digits (1-12 digits) | - -**Examples:** +| `type` | `CnpjValidationType\|'alphanumeric'\|'numeric'\|null` | `CnpjValidationType::Alphanumeric` | Character set after sanitization | +| `caseSensitive` | `?bool` | `true` | When `false`, lowercase letters are uppercased before alphanumeric validation | ```php -// Generate random CNPJ -echo cnpj_gen(); // '65453043000178' - -// Generate formatted CNPJ -echo cnpj_gen(format: true); // '73.008.535/0005-06' - -// Complete a prefix -echo cnpj_gen(prefix: '45623767'); // '45623767000296' +$utils->cnpj->isValid('98765432000198'); // true +$utils->cnpj->isValid('98765432000199'); // false +$utils->cnpj->isValid('1QB5UKALPYFP59'); // true +$utils->cnpj->isValid('1QB5UKALpyfp59'); // false +$utils->cnpj->isValid( // true + '1QB5UKALpyfp59', + caseSensitive: false, +); +$utils->cnpj->isValid( // false + '1QB5UKALPYFP59', + type: CnpjValidationType::Numeric, +); -// Complete and format -echo cnpj_gen(prefix: '456237670002', format: true); // '45.623.767/0002-96' +cnpj_val('98765432000198'); // true +cnpj_val('1QB5UKALpyfp59', caseSensitive: false); // true +cnpj_val( // false + '1QB5UKALPYFP59', + type: CnpjValidationType::Numeric, +); ``` -#### Validation (`cnpj_val` / `CnpjUtils::isValid`) +Invalid CNPJ returns **`false`** without throwing. Wrong input types throw **`CnpjValidatorInputTypeError`**. + +### Domain aggregators (standalone) -Validates CNPJ numbers using the official algorithm. +Use `CpfUtils` or `CnpjUtils` directly when you only need one domain: ```php -cnpj_val(string $cnpjString): bool + true], + generator: ['format' => true], +); + +$cnpjUtils = new CnpjUtils( + formatter: ['hidden' => true], + generator: ['format' => true], + validator: ['type' => CnpjValidationType::Numeric], +); + +$cpfUtils->format('11144477735'); // '111.***.***-**' +$cnpjUtils->format('03603568000195'); // '03.603.***/****-**' ``` -**Examples:** +### Accessing components + +Each domain aggregator exposes its internal formatter, generator, and validator: ```php -// Valid CNPJ -echo cnpj_val('98765432000198'); // true -echo cnpj_val('98.765.432/0001-98'); // true +$utils = new BrUtils(); -// Invalid CNPJ -echo cnpj_val('98765432000199'); // false +$utils->cpf->getFormatter()->format( // '111.***.***-**' + '11144477735', + hidden: true, +); +$utils->cpf->getGenerator()->generate(format: true); // e.g. '545.507.690-68' +$utils->cpf->getValidator()->isValid('11144477735'); // true + +$utils->cnpj->getFormatter()->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' +$utils->cnpj->getGenerator()->generate(format: true); // e.g. '8O.BE5.2KL/UI0Y-06' +$utils->cnpj->getValidator()->isValid('03603568000195'); // true ``` -## Advanced Usage +Use **`getCpfUtils()`** / **`getCnpjUtils()`** on `BrUtils`, or the component getters on each domain utils instance, when you already have a configured instance and want the underlying component without creating a new one. -### Accessing Individual Components +### Mixing styles -You can access the individual formatter, generator, and validator instances: +Use `BrUtils` where a shared configuration helps, and standalone components or helpers elsewhere — they are the same underlying classes: ```php -$brUtils = new BrUtils(); +cpf->getFormatter(); -$cpfGenerator = $brUtils->cpf->getGenerator(); -$cpfValidator = $brUtils->cpf->getValidator(); +use Lacus\BrUtils; +use Lacus\BrUtils\Cnpj\CnpjFormatter; +use Lacus\BrUtils\Cnpj\Enums\CnpjValidationType; -$cnpjFormatter = $brUtils->cnpj->getFormatter(); -$cnpjGenerator = $brUtils->cnpj->getGenerator(); -$cnpjValidator = $brUtils->cnpj->getValidator(); +use function Lacus\BrUtils\Cpf\cpf_fmt; +use function Lacus\BrUtils\Cnpj\cnpj_val; + +$utils = new BrUtils(cnpj: ['validator' => ['type' => CnpjValidationType::Numeric]]); + +// Via façade +$utils->cpf->format('11144477735'); // '111.444.777-35' -// Use them directly -$cpfFormatter->format('11144477735', hidden: true); -$cpfGenerator->generate(format: true); -$cpfValidator->isValid('11144477735'); +// Via component returned by the façade +$utils->cnpj->getFormatter()->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' -$cnpjFormatter->format('03603568000195', hidden: true); -$cnpjGenerator->generate(format: true); -$cnpjValidator->isValid('03603568000195'); +// Via a separate component instance +(new CnpjFormatter())->format('03603568000195'); // '03.603.568/0001-95' + +// Via functional helpers +cpf_fmt('11144477735'); // '111.444.777-35' +cnpj_val('98.765.432/0001-98'); // true ``` -### Custom Error Handling +### Errors & exceptions + +`BrUtils` does not define its own exception types; it propagates errors from the bundled packages: + +- **CPF formatting / generation**: `InvalidArgumentException` for invalid option types or values (e.g. out-of-range `hiddenStart`, prefix longer than 9 digits). +- **CNPJ formatting**: `CnpjFormatterInputTypeError`, `CnpjFormatterOptionsTypeError`, `CnpjFormatterOptionsHiddenRangeInvalidException`, `CnpjFormatterOptionsForbiddenKeyCharacterException`, and related classes. +- **CNPJ generation**: `CnpjGeneratorOptionsTypeError`, `CnpjGeneratorOptionPrefixInvalidException`, `CnpjGeneratorOptionTypeInvalidException`, and related classes. +- **CNPJ validation**: `CnpjValidatorInputTypeError`, `CnpjValidatorOptionsTypeError`, `CnpjValidatorOptionTypeInvalidException`, and related classes. + +Invalid option types on CNPJ are **`TypeError`** subclasses; invalid option values are **`Exception`** subclasses. CPF and CNPJ validation failures return `false`. CPF formatting length failure is handled by **`onFail`** (default: return input); CNPJ formatting length failure uses **`onFail`** (default: return `''`). ```php -$cpf = '123'; // Invalid length -$cnpj = '456'; // Invalid length + "Invalid CPF: {$v}"); // 'Invalid CPF: 123' -echo cnpj_fmt($cnpj, onFail: fn($v) => "Invalid CNPJ: {$v}"); // 'Invalid CNPJ: 456' +$brUtils = new BrUtils(); -// Return original value -echo cpf_fmt($cpf); // '123' -echo cnpj_fmt($cnpj); // '456' +try { + $brUtils->cnpj->format(12345); // throws CnpjFormatterInputTypeError +} catch (CnpjFormatterInputTypeError $e) { + echo $e->getMessage(); +} + +try { + $brUtils->cnpj->isValid(12345678000198); // throws CnpjValidatorInputTypeError +} catch (CnpjValidatorInputTypeError $e) { + echo $e->getMessage(); +} + +$cpfOut = $brUtils->cpf->format( // 'invalid' + 'short', + onFail: static fn ($value) => 'invalid' +); +$cnpjOut = $brUtils->cnpj->format( // 'invalid' + 'short', + onFail: static fn () => 'invalid', +); ``` -## Dependencies +For exhaustive exception lists and edge-case behavior, see each [bundled package](#bundled-packages) README. + +### Bundled packages + +| Package | Main resources | README | +|---------|----------------|--------| +| [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) | `CpfUtils`, `CpfFormatter`, `CpfGenerator`, `CpfValidator`, `cpf_fmt()`, `cpf_gen()`, `cpf_val()` | [docs](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cpf-utils/README.md) | +| [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) | `CnpjUtils`, `CnpjFormatter`, `CnpjGenerator`, `CnpjValidator`, `CnpjType`, `CnpjValidationType`, `cnpj_fmt()`, `cnpj_gen()`, `cnpj_val()` | [docs](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-utils/README.md) | + +All CPF symbols are available under **`Lacus\BrUtils\Cpf\`**; all CNPJ symbols under **`Lacus\BrUtils\Cnpj\`**. Interactive demos: [CPF](https://cpf-utils.vercel.app/) and [CNPJ](https://cnpj-utils.vercel.app/). -This package is built on top of the following specialized packages: +## API -- [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) - CNPJ utilities -- [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) - CPF utilities +- **`BrUtils`**: Top-level façade with `$cpf` / `$cnpj` property access and `getCpfUtils()` / `getCnpjUtils()` +- **`CpfUtils`**: Domain aggregator for CPF format, generate, and validate +- **`CnpjUtils`**: Domain aggregator for CNPJ format, generate, and validate +- **`CpfFormatter`**, **`CpfFormatterOptions`**, **`CpfGenerator`**, **`CpfGeneratorOptions`**, **`CpfValidator`**: CPF component classes +- **`CnpjFormatter`**, **`CnpjFormatterOptions`**, **`CnpjGenerator`**, **`CnpjGeneratorOptions`**, **`CnpjValidator`**, **`CnpjValidatorOptions`**: CNPJ component classes +- **`CnpjGenerationType`**, **`CnpjValidationType`**: CNPJ generation and validation enums +- **`cpf_fmt()`**, **`cpf_gen()`**, **`cpf_val()`**: CPF functional helpers (`Lacus\BrUtils\Cpf\`) +- **`cnpj_fmt()`**, **`cnpj_gen()`**, **`cnpj_val()`**: CNPJ functional helpers (`Lacus\BrUtils\Cnpj\`) +- **Exceptions**: CPF — `InvalidArgumentException` for invalid options; CNPJ — full `TypeError` / `Exception` hierarchies from bundled packages (see linked READMEs) ## Contribution & Support -We welcome contributions! Please see our [Contributing Guidelines](https://github.com/LacusSolutions/br-utils-php/blob/main/CONTRIBUTING.md) for details. But if you find this project helpful, please consider: +We welcome contributions! Please see our [Contributing Guidelines](https://github.com/LacusSolutions/br-utils-php/blob/main/CONTRIBUTING.md) for details. If you find this project helpful, please consider: - ⭐ Starring the repository - 🤝 Contributing to the codebase @@ -419,7 +545,7 @@ We welcome contributions! Please see our [Contributing Guidelines](https://githu ## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/LacusSolutions/br-utils-php/blob/main/LICENSE) file for details. +This project is licensed under the MIT License — see the [LICENSE](https://github.com/LacusSolutions/br-utils-php/blob/main/LICENSE) file for details. ## Changelog diff --git a/packages/br-utils/README.pt.md b/packages/br-utils/README.pt.md new file mode 100644 index 0000000..eb2061c --- /dev/null +++ b/packages/br-utils/README.pt.md @@ -0,0 +1,555 @@ +![br-utils para PHP](https://br-utils.vercel.app/img/cover_br-utils.jpg) + +[![Packagist Version](https://img.shields.io/packagist/v/lacus/br-utils)](https://packagist.org/packages/lacus/br-utils) +[![Packagist Downloads](https://img.shields.io/packagist/dm/lacus/br-utils)](https://packagist.org/packages/lacus/br-utils) +[![PHP Version](https://img.shields.io/packagist/php-v/lacus/br-utils)](https://www.php.net/) +[![Test Status](https://img.shields.io/github/actions/workflow/status/LacusSolutions/br-utils-php/ci.yml?label=ci/cd)](https://github.com/LacusSolutions/br-utils-php/actions) +[![Last Update Date](https://img.shields.io/github/last-commit/LacusSolutions/br-utils-php)](https://github.com/LacusSolutions/br-utils-php) +[![Project License](https://img.shields.io/github/license/LacusSolutions/br-utils-php)](https://github.com/LacusSolutions/br-utils-php/blob/main/LICENSE) + +> 🚀 **Suporte total ao [novo formato alfanumérico de CNPJ](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).** + +> 🌎 [Access documentation in English](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/br-utils/README.md) + +Kit de utilitários em PHP para formatar, gerar e validar CPF (Cadastro de Pessoa Física) e CNPJ (Cadastro Nacional da Pessoa Jurídica). Oferece um wrapper de alto nível `BrUtils` em torno de [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) e [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils), expondo todos os recursos empacotados em namespaces unificados. + +## Suporte a PHP + +| ![PHP 8.2](https://img.shields.io/badge/PHP-8.2-777BB4?logo=php&logoColor=white) | ![PHP 8.3](https://img.shields.io/badge/PHP-8.3-777BB4?logo=php&logoColor=white) | ![PHP 8.4](https://img.shields.io/badge/PHP-8.4-777BB4?logo=php&logoColor=white) | ![PHP 8.5](https://img.shields.io/badge/PHP-8.5-777BB4?logo=php&logoColor=white) | +| --- | --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | + +## Recursos + +- ✅ **API unificada de alto nível**: Uma instância `BrUtils` com acessores de domínio `$cpf` e `$cnpj` +- ✅ **Domínios empacotados**: [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) e [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) instalados juntos +- ✅ **CNPJ alfanumérico**: Suporte completo ao novo formato alfanumérico de CNPJ (a partir de 2026) +- ✅ **Padrões configuráveis**: Defina opções de formatador, gerador e (para CNPJ) validador em cada instância de domínio +- ✅ **Sobrescrita por chamada**: Sobrescreva qualquer opção de componente em uma única chamada de método +- ✅ **Duas formas de uso**: Fachada de alto nível (`BrUtils`), agregadores de domínio (`CpfUtils`, `CnpjUtils`), componentes isolados e helpers funcionais +- ✅ **Namespaces compartilhados**: Símbolos de CPF em `Lacus\BrUtils\Cpf\`; símbolos de CNPJ em `Lacus\BrUtils\Cnpj\` +- ✅ **Tratamento de erros tipado**: Hierarquias dedicadas de exceções dos pacotes empacotados (modelo `TypeError` / `Exception` da v2 para CNPJ; `InvalidArgumentException` da v1 para opções inválidas de CPF) + +## Instalação + +```bash +# usando Composer +$ composer require lacus/br-utils +``` + +Isso instala **`lacus/br-utils`** junto com [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) e [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) (que por sua vez traz os pacotes de componentes de CNPJ). Não é necessário executar `composer require` separado para os pacotes de domínio ao usar **`lacus/br-utils`**. + +## Importação + +Escolha a API que melhor se adapta ao seu caso. + +**Fachada de alto nível:** + +```php +cpf->format($cpf); // '111.444.777-35' +$utils->cpf->isValid($cpf); // true +$utils->cpf->generate(); // ex.: '11508890048' + +$utils->cnpj->format($cnpj); // '03.603.568/0001-95' +$utils->cnpj->isValid($cnpj); // true +$utils->cnpj->generate(); // ex.: '1GJTR3J3XSSA96' +``` + +**Com agregadores de domínio:** + +```php +format($cpf); // '111.444.777-35' +(new CnpjUtils())->format($cnpj); // '03.603.568/0001-95' +(new CpfUtils())->isValid($cpf); // true +(new CnpjUtils())->isValid($cnpj); // true +``` + +**Com helpers funcionais:** + +```php + ['hidden' => true]], cnpj: ['validator' => ['type' => CnpjValidationType::Numeric]])`. + +- **`$cpf`**, **`$cnpj`**: Acesso estilo propriedade às instâncias de utils de domínio (`CpfUtils` e `CnpjUtils`). + +- **`getCpfUtils()`**, **`getCnpjUtils()`**: Retornam as instâncias internas de domínio para uso direto. + +```php +cpf->format('11144477735'); // '111.444.777-35' +$utils->cpf->isValid('11144477735'); // true +$utils->cpf->generate(); // ex.: '11508890048' + +$utils->cnpj->format('03603568000195'); // '03.603.568/0001-95' +$utils->cnpj->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' +$utils->cnpj->isValid('1QB5UKALPYFP59'); // true +$utils->cnpj->generate(format: true); // ex.: 'V1.J0V.8WE/DVZ7-50' +$utils->cnpj->generate( // ex.: '15381773354961' + type: CnpjGenerationType::Numeric, +); +``` + +### Padrões de instância e sobrescrita por chamada + +```php +$utils = new BrUtils( + cpf: [ + 'formatter' => ['hidden' => true, 'hiddenKey' => '#'], + 'generator' => ['format' => true], + ], + cnpj: [ + 'formatter' => ['hidden' => true, 'hiddenKey' => '#'], + 'generator' => ['format' => true], + 'validator' => ['type' => CnpjValidationType::Numeric], + ], +); + +$cpf = '11144477735'; +$cnpj = '03603568000195'; + +$utils->cpf->format($cpf); // '111.###.###-##' +$utils->cpf->format($cpf, hidden: false); // '111.444.777-35' +$utils->cpf->generate(format: false); // ex.: '58450042259' + +$utils->cnpj->format($cnpj); // '03.603.###/####-##' +$utils->cnpj->format($cnpj, hidden: false); // '03.603.568/0001-95' +$utils->cnpj->isValid('1QB5UKALPYFP59'); // false +$utils->cnpj->isValid( // true + '1QB5UKALPYFP59', + type: CnpjValidationType::Alphanumeric, +); +``` + +Passar uma instância `CnpjFormatterOptions`, `CnpjGeneratorOptions` ou `CnpjValidatorOptions` ao construtor de `BrUtils` armazena esse objeto por referência — mutá-lo depois afeta chamadas subsequentes sem sobrescrita por chamada. + +### Operações de CPF + +Os métodos de CPF são acessados via `$utils->cpf`, `CpfUtils` ou os helpers `cpf_*()`. CPF usa a API v1 de [`lacus/cpf-utils`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cpf-utils/README.md): entrada apenas `string`, opções posicionais/nomeadas de formatador e gerador, e sem configurações de validador. + +#### Formatação (`format` / `cpf_fmt`) + +| Parâmetro | Tipo | Padrão | Descrição | +|-----------|------|--------|-----------| +| `escape` | `?bool` | `false` | Quando `true`, escapa HTML na string final | +| `hidden` | `?bool` | `false` | Quando `true`, substitui o intervalo inclusivo `[hiddenStart, hiddenEnd]` na string normalizada de 11 dígitos antes da pontuação | +| `hiddenKey` | `?string` | `'*'` | Substituição para cada posição oculta | +| `hiddenStart` | `?int` | `3` | Índice inicial `0`–`10` (inclusivo) | +| `hiddenEnd` | `?int` | `10` | Índice final `0`–`10` (inclusivo) | +| `dotKey` | `?string` | `'.'` | Separador entre grupos de dígitos | +| `dashKey` | `?string` | `'-'` | Separador antes dos dois últimos dígitos | +| `onFail` | `?\Closure` | veja abaixo | `Closure(mixed $value, Exception $e): string` — usado quando o comprimento sanitizado ≠ 11 | + +O **`onFail`** padrão retorna a entrada original sem alteração. Comprimento inválido **não** lança exceção em `format()`. + +```php +$cpf = '11144477735'; + +$utils->cpf->format($cpf); // '111.444.777-35' +$utils->cpf->format($cpf, hidden: true, hiddenKey: '#'); // '111.###.###-##' +$utils->cpf->format($cpf, dotKey: '', dashKey: '_'); // '111444777_35' + +cpf_fmt($cpf, hidden: true); // '111.***.***-**' +``` + +#### Geração (`generate` / `cpf_gen`) + +| Parâmetro | Tipo | Padrão | Descrição | +|-----------|------|--------|-----------| +| `format` | `?bool` | `false` | Quando `true`, retorna CPF formatado (`000.000.000-00`); caso contrário, saída compacta de 11 dígitos | +| `prefix` | `?string` | `''` | Semente base para geração. Caracteres não numéricos são removidos; apenas os primeiros 9 dígitos (índices `0`–`8`) são usados | + +```php +$utils->cpf->generate(); // ex.: '11508890048' +$utils->cpf->generate(format: true); // ex.: '661.134.831-00' +$utils->cpf->generate(prefix: '123456789'); // '12345678909' +cpf_gen(prefix: '123456789', format: true); // '123.456.789-09' +``` + +#### Validação (`isValid` / `cpf_val`) + +Aceita strings de CPF formatadas ou não. Retorna **`true`** ou **`false`** sem lançar exceção para CPF inválido. + +```php +$utils->cpf->isValid('11144477735'); // true +$utils->cpf->isValid('111.444.777-35'); // true +$utils->cpf->isValid('11144477736'); // false +cpf_val('11144477735'); // true +``` + +### Operações de CNPJ + +Os métodos de CNPJ são acessados via `$utils->cnpj`, `CnpjUtils` ou os helpers `cnpj_*()`. CNPJ usa a API v2 de [`lacus/cnpj-utils`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-utils/README.md). + +#### Formatação (`format` / `cnpj_fmt`) + +Suporta as mesmas opções de [`lacus/cnpj-fmt`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-fmt/README.md). A entrada aceita `string` ou `list`. + +| Parâmetro | Tipo | Padrão | Descrição | +|-----------|------|--------|-----------| +| `hidden` | `?bool` | `false` | Quando `true`, substitui o intervalo inclusivo `[hiddenStart, hiddenEnd]` na string normalizada de 14 caracteres antes da pontuação | +| `hiddenKey` | `?string` | `'*'` | Substituição para cada posição oculta (pode ser multi-caractere ou vazia); não pode usar caracteres proibidos | +| `hiddenStart` | `?int` | `5` | Índice inicial `0`–`13` (inclusivo) | +| `hiddenEnd` | `?int` | `13` | Índice final `0`–`13` (inclusivo); se `hiddenStart > hiddenEnd`, os valores são trocados | +| `dotKey` | `?string` | `'.'` | Separador entre grupos `XX` / `XXX` / `XXX` | +| `slashKey` | `?string` | `'/'` | Separador antes do bloco da filial | +| `dashKey` | `?string` | `'-'` | Separador antes dos dois últimos caracteres | +| `escape` | `?bool` | `false` | Quando `true`, escapa HTML na string final | +| `encode` | `?bool` | `false` | Quando `true`, codifica a string final para URL | +| `onFail` | `?\Closure` | veja abaixo | `Closure(mixed $value, CnpjFormatterException $e): string` — usado quando o comprimento sanitizado ≠ 14 | + +O **`onFail`** padrão retorna string vazia. Tipos de entrada incorretos lançam **`CnpjFormatterInputTypeError`**. + +```php +$cnpj = '03603568000195'; + +$utils->cnpj->format($cnpj); // '03.603.568/0001-95' +$utils->cnpj->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' +$utils->cnpj->format( // '03.603.###/####-##' + $cnpj, + hidden: true, + hiddenKey: '#', +); +$utils->cnpj->format( // '03603568|0001_95' + $cnpj, + dotKey: '', + slashKey: '|', + dashKey: '_', +); + +cnpj_fmt($cnpj); // '03.603.568/0001-95' +``` + +#### Geração (`generate` / `cnpj_gen`) + +Suporta as mesmas opções de [`lacus/cnpj-gen`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-gen/README.md). + +| Parâmetro | Tipo | Padrão | Descrição | +|-----------|------|--------|-----------| +| `format` | `?bool` | `false` | Quando `true`, retorna CNPJ formatado (`XX.XXX.XXX/XXXX-XX`); caso contrário, saída compacta de 14 caracteres | +| `prefix` | `?string` | `''` | Semente base para geração. Caracteres não alfanuméricos são removidos, letras são maiúsculas, e apenas os primeiros 12 caracteres (índices `0`–`11`) são usados; caracteres no índice `12+` são ignorados | +| `type` | `CnpjGenerationType\|'alphanumeric'\|'alphabetic'\|'numeric'\|null` | `CnpjGenerationType::Alphanumeric` | Família de caracteres usada nas posições base geradas | + +Regras de validação de `prefix`: + +- base ID `00000000` é rejeitado (quando os primeiros 8 caracteres estão presentes) +- filial ID `0000` é rejeitado (quando os caracteres 9–12 estão presentes) +- 12 dígitos numéricos repetidos são rejeitados (ex.: `111111111111`) + +```php +$utils->cnpj->generate(); // ex.: '1GJTR3J3XSSA96' +$utils->cnpj->generate(format: true); // ex.: 'V1.J0V.8WE/DVZ7-50' +$utils->cnpj->generate( // ex.: '12345678855883' + prefix: '12345678', + type: CnpjGenerationType::Numeric, +); +``` + +#### Validação (`isValid` / `cnpj_val`) + +Suporta as mesmas opções de [`lacus/cnpj-val`](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-val/README.md). A entrada aceita `string` ou `list`. + +| Parâmetro | Tipo | Padrão | Descrição | +|-----------|------|--------|-----------| +| `type` | `CnpjValidationType\|'alphanumeric'\|'numeric'\|null` | `CnpjValidationType::Alphanumeric` | Conjunto de caracteres após sanitização | +| `caseSensitive` | `?bool` | `true` | Quando `false`, letras minúsculas são convertidas para maiúsculas antes da validação alfanumérica | + +```php +$utils->cnpj->isValid('98765432000198'); // true +$utils->cnpj->isValid('98765432000199'); // false +$utils->cnpj->isValid('1QB5UKALPYFP59'); // true +$utils->cnpj->isValid('1QB5UKALpyfp59'); // false +$utils->cnpj->isValid( // true + '1QB5UKALpyfp59', + caseSensitive: false, +); +$utils->cnpj->isValid( // false + '1QB5UKALPYFP59', + type: CnpjValidationType::Numeric, +); + +cnpj_val('98765432000198'); // true +cnpj_val('1QB5UKALpyfp59', caseSensitive: false); // true +cnpj_val( // false + '1QB5UKALPYFP59', + type: CnpjValidationType::Numeric, +); +``` + +CNPJ inválido retorna **`false`** sem lançar exceção. Tipos de entrada incorretos lançam **`CnpjValidatorInputTypeError`**. + +### Agregadores de domínio (isolados) + +Use `CpfUtils` ou `CnpjUtils` diretamente quando precisar de apenas um domínio: + +```php + true], + generator: ['format' => true], +); + +$cnpjUtils = new CnpjUtils( + formatter: ['hidden' => true], + generator: ['format' => true], + validator: ['type' => CnpjValidationType::Numeric], +); + +$cpfUtils->format('11144477735'); // '111.***.***-**' +$cnpjUtils->format('03603568000195'); // '03.603.***/****-**' +``` + +### Acesso aos componentes + +Cada agregador de domínio expõe formatador, gerador e validador internos: + +```php +$utils = new BrUtils(); + +$utils->cpf->getFormatter()->format( // '111.***.***-**' + '11144477735', + hidden: true, +); +$utils->cpf->getGenerator()->generate(format: true); // ex.: '545.507.690-68' +$utils->cpf->getValidator()->isValid('11144477735'); // true + +$utils->cnpj->getFormatter()->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' +$utils->cnpj->getGenerator()->generate(format: true); // ex.: '8O.BE5.2KL/UI0Y-06' +$utils->cnpj->getValidator()->isValid('03603568000195'); // true +``` + +Use **`getCpfUtils()`** / **`getCnpjUtils()`** em `BrUtils`, ou os getters de componente em cada instância de utils de domínio, quando já tiver uma instância configurada e quiser o componente subjacente sem criar um novo. + +### Misturando estilos + +Use `BrUtils` onde uma configuração compartilhada ajuda, e componentes ou helpers isolados em outros pontos — são as mesmas classes subjacentes: + +```php + ['type' => CnpjValidationType::Numeric]]); + +// Via fachada +$utils->cpf->format('11144477735'); // '111.444.777-35' + +// Via componente retornado pela fachada +$utils->cnpj->getFormatter()->format('12ABC34500DE99'); // '12.ABC.345/00DE-99' + +// Via instância de componente separada +(new CnpjFormatter())->format('03603568000195'); // '03.603.568/0001-95' + +// Via helpers funcionais +cpf_fmt('11144477735'); // '111.444.777-35' +cnpj_val('98.765.432/0001-98'); // true +``` + +### Erros e exceções + +`BrUtils` não define tipos de exceção próprios; propaga erros dos pacotes empacotados: + +- **Formatação / geração de CPF**: `InvalidArgumentException` para tipos ou valores de opção inválidos (ex.: `hiddenStart` fora do intervalo, prefixo com mais de 9 dígitos). +- **Formatação de CNPJ**: `CnpjFormatterInputTypeError`, `CnpjFormatterOptionsTypeError`, `CnpjFormatterOptionsHiddenRangeInvalidException`, `CnpjFormatterOptionsForbiddenKeyCharacterException` e classes relacionadas. +- **Geração de CNPJ**: `CnpjGeneratorOptionsTypeError`, `CnpjGeneratorOptionPrefixInvalidException`, `CnpjGeneratorOptionTypeInvalidException` e classes relacionadas. +- **Validação de CNPJ**: `CnpjValidatorInputTypeError`, `CnpjValidatorOptionsTypeError`, `CnpjValidatorOptionTypeInvalidException` e classes relacionadas. + +Tipos de opção inválidos em CNPJ são subclasses de **`TypeError`**; valores de opção inválidos são subclasses de **`Exception`**. Falhas de validação de CPF e CNPJ retornam `false`. Falha de comprimento na formatação de CPF é tratada por **`onFail`** (padrão: retorna a entrada); falha de comprimento na formatação de CNPJ usa **`onFail`** (padrão: retorna `''`). + +```php +cnpj->format(12345); // lança CnpjFormatterInputTypeError +} catch (CnpjFormatterInputTypeError $e) { + echo $e->getMessage(); +} + +try { + $brUtils->cnpj->isValid(12345678000198); // lança CnpjValidatorInputTypeError +} catch (CnpjValidatorInputTypeError $e) { + echo $e->getMessage(); +} + +$cpfOut = $brUtils->cpf->format( // 'invalid' + 'short', + onFail: static fn ($value) => 'invalid' +); +$cnpjOut = $brUtils->cnpj->format( // 'invalid' + 'short', + onFail: static fn () => 'invalid', +); +``` + +Para listas completas de exceções e comportamento em casos extremos, consulte o README de cada [pacote empacotado](#pacotes-empacotados). + +### Pacotes empacotados + +| Pacote | Principais recursos | README | +|--------|---------------------|--------| +| [`lacus/cpf-utils`](https://packagist.org/packages/lacus/cpf-utils) | `CpfUtils`, `CpfFormatter`, `CpfGenerator`, `CpfValidator`, `cpf_fmt()`, `cpf_gen()`, `cpf_val()` | [docs](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cpf-utils/README.md) | +| [`lacus/cnpj-utils`](https://packagist.org/packages/lacus/cnpj-utils) | `CnpjUtils`, `CnpjFormatter`, `CnpjGenerator`, `CnpjValidator`, `CnpjType`, `CnpjValidationType`, `cnpj_fmt()`, `cnpj_gen()`, `cnpj_val()` | [docs](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/cnpj-utils/README.md) | + +Todos os símbolos de CPF estão disponíveis em **`Lacus\BrUtils\Cpf\`**; todos os de CNPJ em **`Lacus\BrUtils\Cnpj\`**. Demos interativas: [CPF](https://cpf-utils.vercel.app/) e [CNPJ](https://cnpj-utils.vercel.app/). + +## API + +- **`BrUtils`**: Fachada de alto nível com acesso `$cpf` / `$cnpj` e `getCpfUtils()` / `getCnpjUtils()` +- **`CpfUtils`**: Agregador de domínio para formatar, gerar e validar CPF +- **`CnpjUtils`**: Agregador de domínio para formatar, gerar e validar CNPJ +- **`CpfFormatter`**, **`CpfFormatterOptions`**, **`CpfGenerator`**, **`CpfGeneratorOptions`**, **`CpfValidator`**: Classes de componente de CPF +- **`CnpjFormatter`**, **`CnpjFormatterOptions`**, **`CnpjGenerator`**, **`CnpjGeneratorOptions`**, **`CnpjValidator`**, **`CnpjValidatorOptions`**: Classes de componente de CNPJ +- **`CnpjGenerationType`**, **`CnpjValidationType`**: Enums de geração e validação de CNPJ +- **`cpf_fmt()`**, **`cpf_gen()`**, **`cpf_val()`**: Helpers funcionais de CPF (`Lacus\BrUtils\Cpf\`) +- **`cnpj_fmt()`**, **`cnpj_gen()`**, **`cnpj_val()`**: Helpers funcionais de CNPJ (`Lacus\BrUtils\Cnpj\`) +- **Exceções**: CPF — `InvalidArgumentException` para opções inválidas; CNPJ — hierarquias completas de `TypeError` / `Exception` dos pacotes empacotados (veja READMEs vinculados) + +## Contribuição e Suporte + +Agradecemos contribuições! Consulte nossas [Diretrizes de Contribuição](https://github.com/LacusSolutions/br-utils-php/blob/main/CONTRIBUTING.md) para detalhes. Se este projeto for útil para você, considere: + +- ⭐ Dar uma estrela no repositório +- 🤝 Contribuir com o código +- 💡 [Sugerir novos recursos](https://github.com/LacusSolutions/br-utils-php/issues) +- 🐛 [Reportar bugs](https://github.com/LacusSolutions/br-utils-php/issues) + +## Licença + +Este projeto está licenciado sob a MIT License — consulte o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-php/blob/main/LICENSE) para detalhes. + +## Changelog + +Consulte o [CHANGELOG](https://github.com/LacusSolutions/br-utils-php/blob/main/packages/br-utils/CHANGELOG.md) para a lista de alterações e histórico de versões. + +--- + +Made with ❤️ by [Lacus Solutions](https://github.com/LacusSolutions) diff --git a/packages/br-utils/composer.json b/packages/br-utils/composer.json index b21385d..f09be08 100644 --- a/packages/br-utils/composer.json +++ b/packages/br-utils/composer.json @@ -1,7 +1,7 @@ { "name": "lacus/br-utils", "type": "library", - "description": "Utility resources to deal with Brazilian-related data.", + "description": "Utilities to deal with Brazilian-related data", "license": "MIT", "authors": [ { @@ -46,48 +46,50 @@ ], "lint:format": "@php ../../scripts/lint-format.php br-utils", "lint:check": "@php ../../scripts/lint-check.php br-utils", - "test": "phpunit", - "test:watch": "phpunit-watcher watch", - "test-coverage": "phpunit --coverage-html coverage" + "test": [ + "pest --configuration=.pest.config.xml --group isolated-process-tests", + "pest --configuration=.pest.config.xml --exclude-group isolated-process-tests", + "phpunit --configuration=.phpunit.config.xml" + ], + "test:cov": [ + "pest --configuration=.pest.config.xml --group isolated-process-tests", + "pest --configuration=.pest.config.xml --exclude-group isolated-process-tests --coverage-html coverage", + "phpunit --configuration=.phpunit.config.xml --coverage-html coverage" + ] }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } }, "require": { - "php": ">=8.1", - "lacus/cnpj-fmt": "^1.0", - "lacus/cnpj-gen": "^1.0", - "lacus/cnpj-utils": "^1.1", - "lacus/cnpj-val": "^1.0", - "lacus/cpf-fmt": "^1.0", - "lacus/cpf-gen": "^1.0", - "lacus/cpf-utils": "^1.1", - "lacus/cpf-val": "^1.0" + "php": "^8.2", + "lacus/cnpj-utils": "^2.0", + "lacus/cpf-utils": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^10.5", - "spatie/phpunit-watcher": "~1.24" + "pestphp/pest": "^3.8", + "phpunit/phpunit": "^11.5" }, "autoload": { "psr-4": { - "Lacus\\BrUtils\\": "src/" + "Lacus\\": "src/" }, "files": [ - "src/Cnpj/cnpj_utils.php", - "src/Cpf/cpf_utils.php" + "src/BrUtils/Cpf/cpf-fmt.php", + "src/BrUtils/Cpf/cpf-gen.php", + "src/BrUtils/Cpf/cpf-val.php" ] }, "autoload-dev": { "psr-4": { - "Lacus\\BrUtils\\Tests\\": "tests/", - "Lacus\\CnpjUtils\\Tests\\": "vendor/lacus/cnpj-utils/tests/", - "Lacus\\CpfUtils\\Tests\\": "vendor/lacus/cpf-utils/tests/", - "Lacus\\CnpjFmt\\Tests\\": "vendor/lacus/cnpj-fmt/tests/", - "Lacus\\CnpjGen\\Tests\\": "vendor/lacus/cnpj-gen/tests/", - "Lacus\\CnpjVal\\Tests\\": "vendor/lacus/cnpj-val/tests/", + "Lacus\\BrUtils\\Tests\\": "tests/specs/", + "Lacus\\BrUtils\\Tests\\Legacy\\": "tests/phpunit/", "Lacus\\CpfFmt\\Tests\\": "vendor/lacus/cpf-fmt/tests/", "Lacus\\CpfGen\\Tests\\": "vendor/lacus/cpf-gen/tests/", - "Lacus\\CpfVal\\Tests\\": "vendor/lacus/cpf-val/tests/" + "Lacus\\CpfVal\\Tests\\": "vendor/lacus/cpf-val/tests/", + "Lacus\\CpfUtils\\Tests\\": "vendor/lacus/cpf-utils/tests/" } } } diff --git a/packages/br-utils/src/BrUtils.php b/packages/br-utils/src/BrUtils.php index f52e091..9cfc5b4 100644 --- a/packages/br-utils/src/BrUtils.php +++ b/packages/br-utils/src/BrUtils.php @@ -2,77 +2,118 @@ declare(strict_types=1); -namespace Lacus\BrUtils; +namespace Lacus; use Closure; use InvalidArgumentException; +use Lacus\BrUtils\Cnpj\CnpjFormatterOptions; +use Lacus\BrUtils\Cnpj\CnpjGeneratorOptions; +use Lacus\BrUtils\Cnpj\CnpjUtils; +use Lacus\BrUtils\Cnpj\CnpjValidatorOptions; +use Lacus\BrUtils\Cnpj\Enums\CnpjType; +use Lacus\BrUtils\Cnpj\Enums\CnpjValidationType; +use Lacus\BrUtils\Cnpj\Exceptions\CnpjFormatterException; +use Lacus\BrUtils\Cpf\CpfFormatterOptions; +use Lacus\BrUtils\Cpf\CpfGeneratorOptions; +use Lacus\BrUtils\Cpf\CpfUtils; /** + * Utility class for Brazilian-related data, like CPF (Cadastro de Pessoa + * Física) and CNPJ (Cadastro Nacional da Pessoa Jurídica). Provides a unified + * interface for formatting, generating, and validating data. + * * @property-read CpfUtils $cpf * @property-read CnpjUtils $cnpj */ class BrUtils { - private CnpjUtils $cnpjUtils; private CpfUtils $cpfUtils; + private CnpjUtils $cnpjUtils; /** - * @param array{ - * formatter?: array{ - * escape?: bool, - * hidden?: bool, - * hiddenKey?: string, - * hiddenStart?: int, - * hiddenEnd?: int, - * dotKey?: string, - * slashKey?: string, - * dashKey?: string, - * onFail?: Closure, - * }, - * generator?: array{ - * format?: bool, - * prefix?: string, - * }, - * } $cnpj - * @param array{ - * formatter?: array{ - * escape?: bool, - * hidden?: bool, - * hiddenKey?: string, - * hiddenStart?: int, - * hiddenEnd?: int, - * dotKey?: string, - * dashKey?: string, - * onFail?: Closure, + * Creates a new instance with configurable CPF and CNPJ utilities. + * + * Each `$cpf` / `$cnpj` argument accepts either a pre-built utils instance + * or a configuration array spread into the corresponding utils + * constructor. Within that array, each resource key (`formatter`, + * `generator` and `validator` for CNPJ) accepts either an options object + * or an associative array of option values. + * + * @param CpfUtils|array{ + * formatter?: CpfFormatterOptions|array{ + * escape?: bool|null, + * hidden?: bool|null, + * hiddenKey?: string|null, + * hiddenStart?: int|null, + * hiddenEnd?: int|null, + * dotKey?: string|null, + * dashKey?: string|null, + * onFail?: Closure|null, * }, - * generator?: array{ - * format?: bool, - * prefix?: string, + * generator?: CpfGeneratorOptions|array{ + * format?: bool|null, + * prefix?: string|null, * }, * } $cpf + * @param CnpjUtils|array{ + * formatter?: CnpjFormatterOptions|array{ + * hidden?: bool|null, + * hiddenKey?: string|null, + * hiddenStart?: int|null, + * hiddenEnd?: int|null, + * dotKey?: string|null, + * slashKey?: string|null, + * dashKey?: string|null, + * escape?: bool|null, + * encode?: bool|null, + * onFail?: (Closure(mixed, CnpjFormatterException): string|null), + * }, + * generator?: CnpjGeneratorOptions|array{ + * format?: bool|null, + * prefix?: string|null, + * type?: CnpjType|null, + * }, + * validator?: CnpjValidatorOptions|array{ + * caseSensitive?: bool|null, + * type?: CnpjValidationType|'alphanumeric'|'numeric'|null, + * }, + * } $cnpj */ public function __construct( - array $cnpj = [], - array $cpf = [], + CpfUtils|array $cpf = [], + CnpjUtils|array $cnpj = [], ) { - $this->cpfUtils = new CpfUtils(...$cpf); - $this->cnpjUtils = new CnpjUtils(...$cnpj); + $this->cpfUtils = $cpf instanceof CpfUtils + ? $cpf + : new CpfUtils(...$cpf); + $this->cnpjUtils = $cnpj instanceof CnpjUtils + ? $cnpj + : new CnpjUtils(...$cnpj); } + /** + * Property-style access to the data-related utils. + */ public function __get(string $name): mixed { return match ($name) { - 'cpf' => $this->getCpfUtils(), - 'cnpj' => $this->getCnpjUtils(), - default => throw new InvalidArgumentException("Property {$name} not found"), + 'cpf' => $this->getCpfUtils(), + 'cnpj' => $this->getCnpjUtils(), + default => throw new InvalidArgumentException("Unknown property: {$name}"), }; } + /** + * Returns the CPF utilities instance. + */ public function getCpfUtils(): CpfUtils { return $this->cpfUtils; } + /** + * Returns the CNPJ utilities instance. + */ public function getCnpjUtils(): CnpjUtils { return $this->cnpjUtils; diff --git a/packages/br-utils/src/Cpf/CpfFormatter.php b/packages/br-utils/src/BrUtils/Cpf/CpfFormatter.php similarity index 100% rename from packages/br-utils/src/Cpf/CpfFormatter.php rename to packages/br-utils/src/BrUtils/Cpf/CpfFormatter.php diff --git a/packages/br-utils/src/BrUtils/Cpf/CpfFormatterOptions.php b/packages/br-utils/src/BrUtils/Cpf/CpfFormatterOptions.php new file mode 100644 index 0000000..95c03a4 --- /dev/null +++ b/packages/br-utils/src/BrUtils/Cpf/CpfFormatterOptions.php @@ -0,0 +1,11 @@ + $formatterOptions->isEscaped(), + 'hidden' => $formatterOptions->isHidden(), + 'hiddenKey' => $formatterOptions->getHiddenKey(), + 'hiddenStart' => $formatterOptions->getHiddenStart(), + 'hiddenEnd' => $formatterOptions->getHiddenEnd(), + 'dotKey' => $formatterOptions->getDotKey(), + 'dashKey' => $formatterOptions->getDashKey(), + 'onFail' => $formatterOptions->getOnFail(), + ], + generator: [ + 'format' => $generatorOptions->isFormatting(), + 'prefix' => $generatorOptions->getPrefix(), + ], + ); + } +} diff --git a/packages/br-utils/src/Cpf/CpfValidator.php b/packages/br-utils/src/BrUtils/Cpf/CpfValidator.php similarity index 100% rename from packages/br-utils/src/Cpf/CpfValidator.php rename to packages/br-utils/src/BrUtils/Cpf/CpfValidator.php diff --git a/packages/br-utils/src/BrUtils/Cpf/cpf-fmt.php b/packages/br-utils/src/BrUtils/Cpf/cpf-fmt.php new file mode 100644 index 0000000..a58be69 --- /dev/null +++ b/packages/br-utils/src/BrUtils/Cpf/cpf-fmt.php @@ -0,0 +1,52 @@ +utils = new BrUtils(); - } - - protected function format(...$args): string // @phpstan-ignore-line missingType.parameter - { - $stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); - $callerFile = $stackTrace[0]['file'] ?? ''; - - if (preg_match(CNPJ_FILE_REGEX, $callerFile)) { - return $this->formatCnpj(...$args); - } - - if (preg_match(CPF_FILE_REGEX, $callerFile)) { - return $this->formatCpf(...$args); - } - - throw new Error("Caller not found."); - } - - protected function generate(...$args): string // @phpstan-ignore-line missingType.parameter - { - $stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); - $callerFile = $stackTrace[0]['file'] ?? ''; - - if (preg_match(CNPJ_FILE_REGEX, $callerFile)) { - return $this->generateCnpj(...$args); - } - - if (preg_match(CPF_FILE_REGEX, $callerFile)) { - return $this->generateCpf(...$args); - } - - throw new Error("Caller not found."); - } - - protected function isValid(...$args): bool // @phpstan-ignore-line missingType.parameter - { - $stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); - $callerFile = $stackTrace[0]['file'] ?? ''; - - if (preg_match(CNPJ_FILE_REGEX, $callerFile)) { - return $this->isValidCnpj(...$args); - } - - if (preg_match(CPF_FILE_REGEX, $callerFile)) { - return $this->isValidCpf(...$args); - } - - throw new Error("Caller not found."); - } - - protected function formatCnpj( - string $cnpjString, - ?bool $escape = null, - ?bool $hidden = null, - ?string $hiddenKey = null, - ?int $hiddenStart = null, - ?int $hiddenEnd = null, - ?string $dotKey = null, - ?string $slashKey = null, - ?string $dashKey = null, - ?Closure $onFail = null, - ): string { - return $this->utils->cnpj->format( - $cnpjString, - $escape, - $hidden, - $hiddenKey, - $hiddenStart, - $hiddenEnd, - $dotKey, - $slashKey, - $dashKey, - $onFail, - ); - } - - protected function generateCnpj( - ?bool $format = null, - ?string $prefix = null, - ): string { - return $this->utils->cnpj->generate( - $format, - $prefix, - ); - } - - protected function isValidCnpj(string $cnpjString): bool - { - return $this->utils->cnpj->isValid($cnpjString); - } - - protected function formatCpf( - string $cpfString, - ?bool $escape = null, - ?bool $hidden = null, - ?string $hiddenKey = null, - ?int $hiddenStart = null, - ?int $hiddenEnd = null, - ?string $dotKey = null, - ?string $dashKey = null, - ?Closure $onFail = null, - ): string { - return $this->utils->cpf->format( - $cpfString, - $escape, - $hidden, - $hiddenKey, - $hiddenStart, - $hiddenEnd, - $dotKey, - $dashKey, - $onFail, - ); - } - - protected function generateCpf( - ?bool $format = null, - ?string $prefix = null, - ): string { - return $this->utils->cpf->generate( - $format, - $prefix, - ); - } - - protected function isValidCpf(string $cpfString): bool - { - return $this->utils->cpf->isValid($cpfString); - } -} diff --git a/packages/br-utils/tests/Cnpj/CnpjFormatterClassTest.php b/packages/br-utils/tests/Cnpj/CnpjFormatterClassTest.php deleted file mode 100644 index 9c31047..0000000 --- a/packages/br-utils/tests/Cnpj/CnpjFormatterClassTest.php +++ /dev/null @@ -1,48 +0,0 @@ -formatter = new CnpjFormatter(); - } - - protected function format( - string $cnpjString, - ?bool $escape = null, - ?bool $hidden = null, - ?string $hiddenKey = null, - ?int $hiddenStart = null, - ?int $hiddenEnd = null, - ?string $dotKey = null, - ?string $slashKey = null, - ?string $dashKey = null, - ?Closure $onFail = null, - ): string { - return $this->formatter->format( - $cnpjString, - $escape, - $hidden, - $hiddenKey, - $hiddenStart, - $hiddenEnd, - $dotKey, - $slashKey, - $dashKey, - $onFail, - ); - } -} diff --git a/packages/br-utils/tests/Cnpj/CnpjFormatterFunctionTest.php b/packages/br-utils/tests/Cnpj/CnpjFormatterFunctionTest.php deleted file mode 100644 index b54d542..0000000 --- a/packages/br-utils/tests/Cnpj/CnpjFormatterFunctionTest.php +++ /dev/null @@ -1,43 +0,0 @@ -generator = new CnpjGenerator(); - } - - protected function generate( - ?bool $format = null, - ?string $prefix = null, - ): string { - return $this->generator->generate( - $format, - $prefix, - ); - } -} diff --git a/packages/br-utils/tests/Cnpj/CnpjGeneratorFunctionTest.php b/packages/br-utils/tests/Cnpj/CnpjGeneratorFunctionTest.php deleted file mode 100644 index 020522c..0000000 --- a/packages/br-utils/tests/Cnpj/CnpjGeneratorFunctionTest.php +++ /dev/null @@ -1,25 +0,0 @@ -validator = new CnpjValidator(); - } - - protected function isValid(string $cnpjString): bool - { - return $this->validator->isValid($cnpjString); - } -} diff --git a/packages/br-utils/tests/Cnpj/CnpjValidatorFunctionTest.php b/packages/br-utils/tests/Cnpj/CnpjValidatorFunctionTest.php deleted file mode 100644 index e303997..0000000 --- a/packages/br-utils/tests/Cnpj/CnpjValidatorFunctionTest.php +++ /dev/null @@ -1,20 +0,0 @@ -utils = new CnpjUtils(); - } - - protected function format( - string $cnpjString, - ?bool $escape = null, - ?bool $hidden = null, - ?string $hiddenKey = null, - ?int $hiddenStart = null, - ?int $hiddenEnd = null, - ?string $dotKey = null, - ?string $slashKey = null, - ?string $dashKey = null, - ?Closure $onFail = null, - ): string { - return $this->utils->format( - $cnpjString, - $escape, - $hidden, - $hiddenKey, - $hiddenStart, - $hiddenEnd, - $dotKey, - $slashKey, - $dashKey, - $onFail, - ); - } - - protected function generate( - ?bool $format = null, - ?string $prefix = null, - ): string { - return $this->utils->generate( - $format, - $prefix, - ); - } - - protected function isValid(string $cnpjString): bool - { - return $this->utils->isValid($cnpjString); - } -} diff --git a/packages/br-utils/tests/Pest.php b/packages/br-utils/tests/Pest.php new file mode 100644 index 0000000..7f5c869 --- /dev/null +++ b/packages/br-utils/tests/Pest.php @@ -0,0 +1,15 @@ +in($specsDirectory); diff --git a/packages/br-utils/tests/Cpf/CpfFormatterClassTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfFormatterClassTest.php similarity index 95% rename from packages/br-utils/tests/Cpf/CpfFormatterClassTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfFormatterClassTest.php index ac61d23..670801e 100644 --- a/packages/br-utils/tests/Cpf/CpfFormatterClassTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfFormatterClassTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests\Cpf; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use Closure; use Lacus\BrUtils\Cpf\CpfFormatter; diff --git a/packages/br-utils/tests/Cpf/CpfFormatterFunctionTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfFormatterFunctionTest.php similarity index 95% rename from packages/br-utils/tests/Cpf/CpfFormatterFunctionTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfFormatterFunctionTest.php index 1ae6b38..eb6b339 100644 --- a/packages/br-utils/tests/Cpf/CpfFormatterFunctionTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfFormatterFunctionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests\Cpf; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use Closure; diff --git a/packages/br-utils/tests/Cpf/CpfGeneratorClassTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfGeneratorClassTest.php similarity index 93% rename from packages/br-utils/tests/Cpf/CpfGeneratorClassTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfGeneratorClassTest.php index f53a32f..9b8ebf7 100644 --- a/packages/br-utils/tests/Cpf/CpfGeneratorClassTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfGeneratorClassTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests\Cpf; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use Lacus\BrUtils\Cpf\CpfGenerator; use Lacus\CpfGen\Tests\CpfGeneratorTestCases; diff --git a/packages/br-utils/tests/Cpf/CpfGeneratorFunctionTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfGeneratorFunctionTest.php similarity index 91% rename from packages/br-utils/tests/Cpf/CpfGeneratorFunctionTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfGeneratorFunctionTest.php index 7745f08..cec8c7c 100644 --- a/packages/br-utils/tests/Cpf/CpfGeneratorFunctionTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfGeneratorFunctionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests\Cpf; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use function Lacus\BrUtils\Cpf\cpf_gen; diff --git a/packages/br-utils/tests/CpfUtilsTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfUtilsTest.php similarity index 94% rename from packages/br-utils/tests/CpfUtilsTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfUtilsTest.php index 56e5d86..87176f7 100644 --- a/packages/br-utils/tests/CpfUtilsTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfUtilsTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use Closure; -use Lacus\BrUtils\CpfUtils; +use Lacus\BrUtils\Cpf\CpfUtils; use Lacus\CpfUtils\Tests\CpfUtilsTestCases; use PHPUnit\Framework\TestCase; diff --git a/packages/br-utils/tests/Cpf/CpfValidatorClassTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfValidatorClassTest.php similarity index 92% rename from packages/br-utils/tests/Cpf/CpfValidatorClassTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfValidatorClassTest.php index 52d0ce6..8e6bdbb 100644 --- a/packages/br-utils/tests/Cpf/CpfValidatorClassTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfValidatorClassTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests\Cpf; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use Lacus\BrUtils\Cpf\CpfValidator; use Lacus\CpfVal\Tests\CpfValidatorTestCases; diff --git a/packages/br-utils/tests/Cpf/CpfValidatorFunctionTest.php b/packages/br-utils/tests/phpunit/Cpf/CpfValidatorFunctionTest.php similarity index 89% rename from packages/br-utils/tests/Cpf/CpfValidatorFunctionTest.php rename to packages/br-utils/tests/phpunit/Cpf/CpfValidatorFunctionTest.php index ed6617d..3d21357 100644 --- a/packages/br-utils/tests/Cpf/CpfValidatorFunctionTest.php +++ b/packages/br-utils/tests/phpunit/Cpf/CpfValidatorFunctionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Lacus\BrUtils\Tests\Cpf; +namespace Lacus\BrUtils\Tests\Legacy\Cpf; use function Lacus\BrUtils\Cpf\cpf_val; diff --git a/packages/br-utils/tests/specs/BrUtils.spec.php b/packages/br-utils/tests/specs/BrUtils.spec.php new file mode 100644 index 0000000..7a3a4da --- /dev/null +++ b/packages/br-utils/tests/specs/BrUtils.spec.php @@ -0,0 +1,682 @@ + $options->isEscaped(), + 'hidden' => $options->isHidden(), + 'hiddenKey' => $options->getHiddenKey(), + 'hiddenStart' => $options->getHiddenStart(), + 'hiddenEnd' => $options->getHiddenEnd(), + 'dotKey' => $options->getDotKey(), + 'dashKey' => $options->getDashKey(), + 'onFail' => $options->getOnFail(), + ]; + } + + /** + * @return array{ + * format: bool, + * prefix: string, + * } + */ + function getCpfGeneratorOptions(LegacyCpfGeneratorOptions $options): array + { + return [ + 'format' => $options->isFormatting(), + 'prefix' => $options->getPrefix(), + ]; + } + + describe('constructor', function () { + describe('when called with no arguments', function () { + it('creates an instance with necessary resource instances', function () { + $defaultCpfFormatterOptions = new CpfFormatterOptions(); + $defaultCpfGeneratorOptions = new CpfGeneratorOptions(); + $defaultCnpjFormatterOptions = new CnpjFormatterOptions(); + $defaultCnpjGeneratorOptions = new CnpjGeneratorOptions(); + $defaultCnpjValidatorOptions = new CnpjValidatorOptions(); + + $utils = new BrUtils(); + + expect( + getCpfFormatterOptions($utils->cpf->getFormatter()->getOptions()) + )->toMatchArray( + getCpfFormatterOptions($defaultCpfFormatterOptions) + ); + expect( + getCpfGeneratorOptions($utils->cpf->getGenerator()->getOptions()) + )->toMatchArray( + getCpfGeneratorOptions($defaultCpfGeneratorOptions) + ); + expect($utils->cnpj->getFormatter()->getOptions()->getAll())->toMatchArray($defaultCnpjFormatterOptions->getAll()); + expect($utils->cnpj->getGenerator()->getOptions()->getAll())->toMatchArray($defaultCnpjGeneratorOptions->getAll()); + expect($utils->cnpj->getValidator()->getOptions()->getAll())->toMatchArray($defaultCnpjValidatorOptions->getAll()); + }); + }); + + describe('when called with arguments', function () { + it("configures CPF's formatter, generator, and validator from arrays", function () { + $cpfFormatterOptions = ['hidden' => true, 'hiddenKey' => '#', 'hiddenStart' => 8, 'hiddenEnd' => 10, 'dotKey' => '_', 'dashKey' => ' dv ', 'onFail' => function () { + return '1234567890'; + }]; + $cpfGeneratorOptions = ['format' => true, 'prefix' => '12345678']; + + $utils = new BrUtils( + cpf: [ + 'formatter' => $cpfFormatterOptions, + 'generator' => $cpfGeneratorOptions, + ], + ); + + expect( + getCpfFormatterOptions($utils->cpf->getFormatter()->getOptions()) + )->toMatchArray($cpfFormatterOptions); + expect( + getCpfGeneratorOptions($utils->cpf->getGenerator()->getOptions()) + )->toMatchArray($cpfGeneratorOptions); + }); + + it("configures CNPJ's formatter, generator, and validator from arrays", function () { + $cnpjFormatterOptions = ['slashKey' => '|']; + $cnpjGeneratorOptions = ['format' => true, 'prefix' => '12345']; + $cnpjValidatorOptions = ['type' => CnpjValidationType::Numeric, 'caseSensitive' => false]; + + $utils = new BrUtils( + cnpj: [ + 'formatter' => $cnpjFormatterOptions, + 'generator' => $cnpjGeneratorOptions, + 'validator' => $cnpjValidatorOptions, + ], + ); + + expect($utils->cnpj->getFormatter()->getOptions()->getAll())->toMatchArray($cnpjFormatterOptions); + expect($utils->cnpj->getGenerator()->getOptions()->getAll())->toMatchArray($cnpjGeneratorOptions); + expect($utils->cnpj->getValidator()->getOptions()->getAll())->toMatchArray($cnpjValidatorOptions); + }); + + it('uses provided options instances directly', function () { + $cnpjFormatterOptions = new CnpjFormatterOptions(); + $cnpjGeneratorOptions = new CnpjGeneratorOptions(); + $cnpjValidatorOptions = new CnpjValidatorOptions(); + + $utils = new BrUtils( + cnpj: [ + 'formatter' => $cnpjFormatterOptions, + 'generator' => $cnpjGeneratorOptions, + 'validator' => $cnpjValidatorOptions, + ], + ); + + expect($utils->cnpj->getFormatter()->getOptions())->toBe($cnpjFormatterOptions); + expect($utils->cnpj->getGenerator()->getOptions())->toBe($cnpjGeneratorOptions); + expect($utils->cnpj->getValidator()->getOptions())->toBe($cnpjValidatorOptions); + }); + + it('mutates shared options instances and affects later calls', function () { + $cnpjFormatterOptions = new CnpjFormatterOptions(); + $utils = new BrUtils( + cnpj: [ 'formatter' => $cnpjFormatterOptions ], + ); + + $cnpjFormatterOptions->dashKey = '|'; + + expect($utils->cnpj->getFormatter()->getOptions()->getAll())->toMatchArray([ + 'dashKey' => '|', + ]); + }); + }); + + describe('when called with invalid options', function () { + it('throws CPF formatter exceptions for invalid formatter options', function () { + expect(function () { + new BrUtils(cpf: ['formatter' => ['hiddenStart' => -1]]); + })->toThrow(InvalidArgumentException::class); + }); + + it('throws CPF generator exceptions for invalid generator options', function () { + expect(function () { + new BrUtils(cpf: ['generator' => ['prefix' => '1234567890']]); + })->toThrow(InvalidArgumentException::class); + }); + + it('throws CNPJ formatter exceptions for invalid formatter options', function () { + expect(function () { + new BrUtils(cnpj: ['formatter' => ['hiddenStart' => -1]]); + })->toThrow(CnpjFormatterOptionsHiddenRangeInvalidException::class); + + expect(function () { + new BrUtils(cnpj: ['formatter' => ['dashKey' => "\u{00e5}"]]); + })->toThrow(CnpjFormatterOptionsForbiddenKeyCharacterException::class); + }); + + it('throws CNPJ generator exceptions for invalid generator options', function () { + expect(function () { + new BrUtils(cnpj: ['generator' => ['prefix' => '00000000']]); + })->toThrow(CnpjGeneratorOptionPrefixInvalidException::class); + + expect(function () { + new BrUtils(cnpj: ['generator' => ['type' => 'invalid']]); + })->toThrow(CnpjGeneratorOptionTypeInvalidException::class); + + expect(function () { + new BrUtils(cnpj: ['generator' => ['prefix' => 123]]); + })->toThrow(CnpjGeneratorOptionsTypeError::class); + }); + + it('throws CNPJ validator exceptions for invalid validator options', function () { + expect(function () { + new BrUtils(cnpj: ['validator' => ['type' => 'invalid']]); + })->toThrow(CnpjValidatorOptionTypeInvalidException::class); + }); + }); + }); + + describe('resources accessors', function () { + it('returns the `CpfUtils instance', function () { + $utils = new BrUtils(); + + expect($utils->cpf)->toBeInstanceOf(CpfUtils::class); + }); + + it('returns the `CpfFormatter` instance', function () { + $utils = new BrUtils(); + + expect($utils->cpf->getFormatter())->toBeInstanceOf(LegacyCpfFormatter::class); + }); + + it('returns the `CpfGenerator` instance', function () { + $utils = new BrUtils(); + + expect($utils->cpf->getGenerator())->toBeInstanceOf(LegacyCpfGenerator::class); + }); + + it('returns the `CpfValidator` instance', function () { + $utils = new BrUtils(); + + expect($utils->cpf->getValidator())->toBeInstanceOf(LegacyCpfValidator::class); + }); + + it('returns the `CnpjUtils instance', function () { + $utils = new BrUtils(); + + expect($utils->cnpj)->toBeInstanceOf(CnpjUtils::class); + }); + + it('returns the `CnpjFormatter` instance', function () { + $utils = new BrUtils(); + + expect($utils->cnpj->getFormatter())->toBeInstanceOf(CnpjFormatter::class); + }); + + it('returns the `CnpjGenerator` instance', function () { + $utils = new BrUtils(); + + expect($utils->cnpj->getGenerator())->toBeInstanceOf(CnpjGenerator::class); + }); + + it('returns the `CnpjValidator` instance', function () { + $utils = new BrUtils(); + + expect($utils->cnpj->getValidator())->toBeInstanceOf(CnpjValidator::class); + }); + }); + + describe('CPF utils', function () { + describe('`format` method', function () { + /** + * @param ?string $dotKey + * @param ?string $dashKey + */ + $formatWithNamedOptionsInConstructor = function (string $cpf, $dotKey = null, $dashKey = null): string { + $utils = new BrUtils(cpf: ['formatter' => compact('dotKey', 'dashKey')]); + + return $utils->cpf->format($cpf); + }; + + /** + * @param ?string $dotKey + * @param ?string $dashKey + */ + $formatWithNamedOptionsInMethod = function (string $cpf, $dotKey = null, $dashKey = null): string { + $utils = new BrUtils(); + + return $utils->cpf->format($cpf, dotKey: $dotKey, dashKey: $dashKey); + }; + + $formatContexts = [ + ['when options are passed to constructor as an array', $formatWithNamedOptionsInConstructor], + ['when options are passed to the method as named arguments', $formatWithNamedOptionsInMethod], + ]; + + foreach ($formatContexts as $formatContext) { + [$description, $format] = $formatContext; + + describe($description, function () use ($format) { + it('matches `CpfFormatter::format` behavior', function () use ($format) { + $input = '80976511061'; + $formatter = new CpfFormatter(); + + $result = $format($input); + + expect($result)->toBe($formatter->format($input)); + }); + + it('forwards formatting options', function () use ($format) { + $input = '80976511061'; + $dotKey = '_'; + $dashKey = ' dv '; + + $result = $format($input, $dotKey, $dashKey); + + expect($result)->toBe("809_765_110 dv 61"); + }); + }); + } + + it('applies constructor formatter defaults when method options are omitted', function () { + $utils = new BrUtils( + cpf: [ + 'formatter' => [ + 'hidden' => true, + 'hiddenKey' => '#', + ], + ], + ); + + $result = $utils->cpf->format('80976511061'); + + expect($result)->toContain('#'); + }); + }); + + describe('`generate` method', function () { + /** + * @param ?bool $format + * @param ?string $prefix + */ + $generateWithNamedOptionsInConstructor = function ($format = null, $prefix = null): string { + $utils = new BrUtils(cpf: ['generator' => compact('format', 'prefix')]); + + return $utils->cpf->generate(); + }; + + /** + * @param ?bool $format + * @param ?string $prefix + */ + $generateWithNamedOptionsInMethod = function ($format = null, $prefix = null): string { + $utils = new BrUtils(); + + return $utils->cpf->generate($format, $prefix); + }; + + $generateContexts = [ + ['when options are passed to constructor as an array', $generateWithNamedOptionsInConstructor], + ['when options are passed to the method as named arguments', $generateWithNamedOptionsInMethod], + ]; + + foreach ($generateContexts as $generateContext) { + [$description, $generate] = $generateContext; + + describe($description, function () use ($generate) { + it('matches `CpfGenerator::generate` behavior', function () use ($generate) { + $generator = new CpfGenerator(); + + $result = $generate(); + + expect($result)->toMatch('/^\d{11}$/'); + expect(strlen($result))->toBe(strlen($generator->generate())); + }); + + it('forwards generation options', function () use ($generate) { + $options = [ + 'format' => true, + 'prefix' => '12345', + ]; + + $result = $generate(...$options); + + expect($result)->toMatch('/^123\.45\d\.\d{3}-\d{2}$/'); + }); + + it('returns a deterministic CPF for a full 9-character prefix', function () use ($generate) { + $prefix = '123456789'; + $results = []; + + for ($i = 0; $i < 20; $i++) { + $results[] = $generate(prefix: $prefix); + } + + $uniqueValues = array_unique($results); + + expect($uniqueValues)->toHaveCount(1); + }); + }); + } + }); + }); + + describe('CNPJ utils', function () { + describe('`format` method', function () { + /** + * @param string|list $cnpj + * @param ?string $slashKey + */ + $formatWithNamedOptionsInConstructor = function ($cnpj, $slashKey = null): string { + $utils = new BrUtils(cnpj: ['formatter' => ['slashKey' => $slashKey]]); + + return $utils->cnpj->format($cnpj); + }; + + /** + * @param string|list $cnpj + * @param ?string $slashKey + */ + $formatWithFormatterOptionsInConstructor = function (string $cnpj, $slashKey = null): string { + $options = new CnpjFormatterOptions(slashKey: $slashKey); + $utils = new BrUtils(cnpj: ['formatter' => $options]); + + return $utils->cnpj->format($cnpj); + }; + + /** + * @param string|list $cnpj + * @param ?string $slashKey + */ + $formatWithNamedOptionsInMethod = function (string $cnpj, $slashKey = null): string { + $utils = new BrUtils(); + + return $utils->cnpj->format($cnpj, slashKey: $slashKey); + }; + + /** + * @param string|list $cnpj + * @param ?string $slashKey + */ + $formatWithFormatterOptionsInMethod = function (string $cnpj, $slashKey = null): string { + $utils = new BrUtils(); + $options = new CnpjFormatterOptions(slashKey: $slashKey); + + return $utils->cnpj->format($cnpj, $options); + }; + + $formatContexts = [ + ['when options are passed to constructor as an array', $formatWithNamedOptionsInConstructor], + ['when options are passed to constructor as a `CnpjFormatterOptions` instance', $formatWithFormatterOptionsInConstructor], + ['when options are passed to the method as named arguments', $formatWithNamedOptionsInMethod], + ['when options are passed to the method as a `CnpjFormatterOptions` instance', $formatWithFormatterOptionsInMethod], + ]; + + foreach ($formatContexts as $formatContext) { + [$description, $format] = $formatContext; + + describe($description, function () use ($format) { + it('matches `CnpjFormatter::format` behavior', function () use ($format) { + $input = '91415732000793'; + $formatter = new CnpjFormatter(); + + $result = $format($input); + + expect($result)->toBe($formatter->format($input)); + }); + + it('forwards formatting options', function () use ($format) { + $input = '01ABC234000X56'; + $slashKey = '|'; + + $result = $format($input, $slashKey); + + expect($result)->toBe("01.ABC.234{$slashKey}000X-56"); + }); + }); + } + + it('applies constructor formatter defaults when method options are omitted', function () { + $utils = new BrUtils( + cnpj: [ + 'formatter' => [ + 'hidden' => true, + 'hiddenKey' => '#', + ], + ], + ); + + $result = $utils->cnpj->format('12ABC34500DE99'); + + expect($result)->toContain('#'); + }); + }); + + describe('`generate` method', function () { + /** + * @param ?bool $format + * @param ?string $prefix + * @param ?CnpjGenerationType $type + */ + $generateWithNamedOptionsInConstructor = function ($format = null, $prefix = null, $type = null): string { + $utils = new BrUtils(cnpj: ['generator' => compact('format', 'prefix', 'type')]); + + return $utils->cnpj->generate(); + }; + + /** + * @param ?bool $format + * @param ?string $prefix + * @param ?CnpjGenerationType $type + */ + $generateWithGeneratorOptionsInConstructor = function ($format = null, $prefix = null, $type = null): string { + $options = new CnpjGeneratorOptions(format: $format, prefix: $prefix, type: $type); + $utils = new BrUtils(cnpj: ['generator' => $options]); + + return $utils->cnpj->generate(); + }; + + /** + * @param ?bool $format + * @param ?string $prefix + * @param ?CnpjGenerationType $type + */ + $generateWithNamedOptionsInMethod = function ($format = null, $prefix = null, $type = null): string { + $utils = new BrUtils(); + + return $utils->cnpj->generate(format: $format, prefix: $prefix, type: $type); + }; + + /** + * @param ?bool $format + * @param ?string $prefix + * @param ?CnpjGenerationType $type + */ + $generateWithGeneratorOptionsInMethod = function ($format = null, $prefix = null, $type = null): string { + $utils = new BrUtils(); + $options = new CnpjGeneratorOptions(format: $format, prefix: $prefix, type: $type); + + return $utils->cnpj->generate($options); + }; + + $generateContexts = [ + ['when options are passed to constructor as an array', $generateWithNamedOptionsInConstructor], + ['when options are passed to constructor as a `CnpjGeneratorOptions` instance', $generateWithGeneratorOptionsInConstructor], + ['when options are passed to the method as named arguments', $generateWithNamedOptionsInMethod], + ['when options are passed to the method as a `CnpjGeneratorOptions` instance', $generateWithGeneratorOptionsInMethod], + ]; + + foreach ($generateContexts as $generateContext) { + [$description, $generate] = $generateContext; + + describe($description, function () use ($generate) { + it('matches `CnpjGenerator::generate` behavior', function () use ($generate) { + $generator = new CnpjGenerator(); + + $result = $generate(); + + expect($result)->toMatch('/^[0-9A-Z]{14}$/'); + expect(strlen($result))->toBe(strlen($generator->generate())); + }); + + it('forwards generation options', function () use ($generate) { + $options = [ + 'format' => true, + 'prefix' => '12345', + 'type' => CnpjGenerationType::Numeric, + ]; + + $result = $generate(...$options); + + expect($result)->toMatch('/^12\.345\.\d{3}\/\d{4}-\d{2}$/'); + }); + + it('returns a deterministic CNPJ for a full 12-character prefix', function () use ($generate) { + $prefix = '123456780009'; + $results = []; + + for ($i = 0; $i < 20; $i++) { + $results[] = $generate(prefix: $prefix); + } + + $uniqueValues = array_unique($results); + + expect($uniqueValues)->toHaveCount(1); + }); + }); + } + }); + + describe('`isValid` method', function () { + /** + * @param string|list $cnpj + * @param ?CnpjValidationType $type + * @param ?bool $caseSensitive + */ + $isValidWithNamedOptionsInConstructor = function (string $cnpj, $type = null, $caseSensitive = null): bool { + $utils = new BrUtils(cnpj: ['validator' => compact('type', 'caseSensitive')]); + + return $utils->cnpj->isValid($cnpj); + }; + + /** + * @param string|list $cnpj + * @param ?CnpjValidationType $type + * @param ?bool $caseSensitive + */ + $isValidWithValidatorOptionsInConstructor = function (string $cnpj, $type = null, $caseSensitive = null): bool { + $options = new CnpjValidatorOptions(type: $type, caseSensitive: $caseSensitive); + $utils = new BrUtils(cnpj: ['validator' => $options]); + + return $utils->cnpj->isValid($cnpj); + }; + + /** + * @param string|list $cnpj + * @param ?CnpjValidationType $type + * @param ?bool $caseSensitive + */ + $isValidWithNamedOptionsInMethod = function (string $cnpj, $type = null, $caseSensitive = null): bool { + $utils = new BrUtils(); + + return $utils->cnpj->isValid($cnpj, type: $type, caseSensitive: $caseSensitive); + }; + + /** + * @param string|list $cnpj + * @param ?CnpjValidationType $type + * @param ?bool $caseSensitive + */ + $isValidWithValidatorOptionsInMethod = function (string $cnpj, $type = null, $caseSensitive = null): bool { + $utils = new BrUtils(); + $options = new CnpjValidatorOptions(type: $type, caseSensitive: $caseSensitive); + + return $utils->cnpj->isValid($cnpj, $options); + }; + + $isValidContexts = [ + ['when options are passed to constructor as an array', $isValidWithNamedOptionsInConstructor], + ['when options are passed to constructor as a `CnpjValidatorOptions` instance', $isValidWithValidatorOptionsInConstructor], + ['when options are passed to the method as named arguments', $isValidWithNamedOptionsInMethod], + ['when options are passed to the method as a `CnpjValidatorOptions` instance', $isValidWithValidatorOptionsInMethod], + ]; + + foreach ($isValidContexts as $isValidContext) { + [$description, $isValid] = $isValidContext; + + describe($description, function () use ($isValid) { + it('matches `CnpjValidator::isValid` behavior', function () use ($isValid) { + $input = '91415732000793'; + $validator = new CnpjValidator(); + + $result = $isValid($input); + + expect($result)->toBe($validator->isValid($input)); + }); + + it('forwards validation options', function () use ($isValid) { + $input = '1QB5UKALPYFP59'; + + $result = $isValid($input, CnpjValidationType::Numeric); + expect($result)->toBeFalse(); + + $result = $isValid($input, CnpjValidationType::Alphanumeric); + expect($result)->toBeTrue(); + }); + + it('validates formatted and unformatted CNPJ strings', function () use ($isValid) { + $result = $isValid('1QB5UKALPYFP59'); + expect($result)->toBeTrue(); + + $result = $isValid('1QB5.UKAL.PYF/P59'); + expect($result)->toBeTrue(); + + $result = $isValid('AB123CDE0001555'); + expect($result)->toBeFalse(); + }); + }); + } + }); + }); +}); diff --git a/packages/cnpj-utils/CHANGELOG.md b/packages/cnpj-utils/CHANGELOG.md index a5562b8..9ec95da 100644 --- a/packages/cnpj-utils/CHANGELOG.md +++ b/packages/cnpj-utils/CHANGELOG.md @@ -16,23 +16,35 @@ - **Removed helpers** — Autoloaded `cnpj_fmt()`, `cnpj_gen()`, and `cnpj_val()` were removed from this package; call `CnpjUtils` methods or depend on `lacus/cnpj-fmt`, `lacus/cnpj-gen`, and `lacus/cnpj-val` directly. - **Removed thin subclasses** — Local `CnpjFormatter`, `CnpjGenerator`, and `CnpjValidator` wrappers were removed; import the classes from the respective bundled packages under `Lacus\BrUtils\Cnpj\`. - **Constructor** — Formatter and generator options accept `CnpjFormatterOptions` / `CnpjGeneratorOptions` instances or named arrays (not positional spread arrays); a new `$validator` argument configures default validation behavior. -- **`format()` signature** — Accepts `string|list` and an optional `CnpjFormatterOptions` instance as the second argument; adds `encode` and reorders options to match `lacus/cnpj-fmt` v2 (including alphanumeric normalization and updated `onFail` defaults). -- **`generate()` signature** — Accepts an optional `CnpjGeneratorOptions` instance and a `type` argument for numeric, alphabetic, or alphanumeric generation modes. -- **`isValid()` signature** — Accepts `string|list` plus optional `CnpjValidatorOptions` (or named `type` / `caseSensitive`); default validation is **alphanumeric** — pass `type: 'numeric'` to restore legacy numeric-only behavior. +- **Bundled component API** — Inherits v2 changes from `lacus/cnpj-fmt`, `lacus/cnpj-gen`, and `lacus/cnpj-val`: + - **Alphanumeric CNPJ** — letters are kept during sanitization; default validation is **alphanumeric** (pass `type: 'numeric'` to restore legacy numeric-only behavior); + - **Signatures** — `format()` / `isValid()` accept `string|list`; `format()` adds `encode` and `CnpjFormatterOptions`; `generate()` adds `CnpjGeneratorOptions` and `CnpjType`; `isValid()` adds `CnpjValidatorOptions`; + - **`onFail` default** — formatter `onFail` now returns `''` on invalid length (v1 returned the original input); + - **Options model** — `*Options` use property access and `overrides` merging; `merge()` and getter/setter style removed; + - **Check digits** — generation and validation delegate to `lacus/cnpj-dv` (`CnpjCheckDigits`) instead of inline/`CnpjGeneratorVerifierDigit`; + - **Input errors** — invalid input types throw typed `*InputTypeError` exceptions instead of native `TypeError` or unspecified behavior. ### New Features +- **Alphanumeric CNPJ** — `format()`, `generate()`, and `isValid()` support the new 14-character alphanumeric CNPJ (digits and `A`–`Z`, uppercased on input). - **`encode` option** — `format()` can URL-encode the formatted CNPJ (delegated to the formatter's `encode` option). - **Array input** — `format()` and `isValid()` concatenate a `list` (e.g. grouped or formatted segments). - **`CnpjValidatorOptions`** — Validation is now configurable (v1 validator had no options); set `type` (`CnpjValidationType::Alphanumeric` or `::Numeric`) and `caseSensitive` on the `CnpjUtils` instance, per `isValid()` call, or via `getValidator()->getOptions()`. +- **`CnpjType` generation modes** — `generate()` supports `Numeric`, `Alphabetic`, and `Alphanumeric` output via the `CnpjType` enum (from `lacus/cnpj-gen` ^2.1). +- **Alphanumeric prefix generation** — `generate()` accepts alphanumeric prefixes (stripped, uppercased, capped at 12 base characters). +- **Structured exceptions** — Typed `TypeError` / `Exception` hierarchies from bundled CNPJ packages propagate through `CnpjUtils`. ### Improvements -- **New PT-BR documentation**: New [README in Brazilian Portuguese](./README.pt.md). +- **New PT-BR documentation** — New [README in Brazilian Portuguese](./README.pt.md). +- **Documentation** — README and README.pt.md updated for the v2 API (namespaces, constructor, validator options, bundled-package imports). - **Dependency alignment** — Runtime dependencies updated to: - - `lacus/cnpj-fmt`: `^2.0` - - `lacus/cnpj-gen`: `^2.1` - - `lacus/cnpj-val`: `^2.0`. + - `lacus/cnpj-fmt` ^2.0 + - `lacus/cnpj-gen` ^2.1 + - `lacus/cnpj-val` ^2.0 +- **Generator reliability** — Internal retry when check-digit computation rejects a generated candidate (from `lacus/cnpj-gen` ^2.0). +- **Validator reuse** — `cnpj_val()` keeps the `CnpjValidator` instance alive across calls (from `lacus/cnpj-val` ^2.0). +- **Check-digit performance** — Faster `CnpjCheckDigits` engine used by generation and validation (from `lacus/cnpj-dv` ^1.1). ## 1.0.0