diff options
Diffstat (limited to 'src/Faixa.php')
| -rw-r--r-- | src/Faixa.php | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/src/Faixa.php b/src/Faixa.php new file mode 100644 index 0000000..3f4be84 --- /dev/null +++ b/src/Faixa.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +namespace Cepis; + +/** + * The municipality a CEP *would* belong to, when the CEP itself does not exist. + * + * It arrives on the 404 of `GET /{cep}` and is carried by + * {@see CepNotFoundException::$faixa}. **It is context for an error, never an + * address.** A CEP with a `faixa` is still a CEP that is not in the DNE: the + * answer to the lookup remains "does not exist". + * + * It exists because a failing CEP is rarely random. It is usually someone who + * knows the city and guessed the number — and the classic guess is the `-000`, + * which 957 municipalities never received. Knowing that `25845000` "is Areal" + * is what lets you say *"we could not find that CEP in Areal/RJ"* instead of + * the useless *"CEP not found"*. + * + * {@see Faixa::$cep_ini} and {@see Faixa::$cep_fim} are the smallest and the + * largest CEP that **actually exist** in that municipality, not the bounds the + * Correios publish for it. The DNE's own start for Areal is `25845000` — + * exactly the CEP that just failed — and it is not `cep_ini` precisely because + * it does not exist. + * + * **The range is sparse, and the pair is not a list of valid CEPs.** Areal's + * 118 CEPs are spread across the ~4,900 numbers between `25845001` and + * `25849899`; the overwhelming majority of that interval is empty. That is what + * {@see Faixa::$ceps} is for. Never iterate the interval, never present it as + * "the CEPs of this city", and never conclude from `cep_ini <= x <= cep_fim` + * that `x` exists. + * + * The whole object is **omitted** by the API — never sent as null — when the + * CEP falls in a band no municipality owns, so absence is normal and + * {@see CepNotFoundException::$faixa} is null for it. + */ +final class Faixa +{ + /** + * Property names are the API's own JSON keys, `cep_ini` and `cep_fim` + * included. See {@see Address} for why they are not translated or + * camel-cased. + * + * @param string $cep_ini Smallest CEP that exists in the municipality, not + * the start the Correios publish for it. + * @param string $cep_fim Largest CEP that exists in the municipality. + * @param int $ceps How many CEPs the municipality actually has. Read it + * together with the pair above: the interval is sparse, and this is the + * only number that says how sparse. + */ + public function __construct( + public readonly string $localidade, + public readonly string $uf = '', + public readonly string $estado = '', + public readonly string $regiao = '', + public readonly string $ibge = '', + public readonly string $cep_ini = '', + public readonly string $cep_fim = '', + public readonly int $ceps = 0, + ) { + } + + /** + * Build from the decoded `faixa` block, or `null` when it is missing, + * empty or malformed. + * + * Deliberately forgiving. A `faixa` this client cannot read must never cost + * the caller the answer it came for: the CEP does not exist either way, and + * turning a clean not-found into a parse failure would trade a useful + * exception for a useless one. Only {@see Faixa::$localidade} is required — + * without a municipality name there is nothing to tell the user — and every + * other field falls back the way {@see Address} does, to an empty string or + * to zero. + */ + public static function fromArray(mixed $data): ?self + { + if (!is_array($data) || $data === []) { + return null; + } + + $localidade = self::asString($data['localidade'] ?? null); + if ($localidade === '') { + return null; + } + + return new self( + localidade: $localidade, + uf: self::asString($data['uf'] ?? null), + estado: self::asString($data['estado'] ?? null), + regiao: self::asString($data['regiao'] ?? null), + ibge: self::asString($data['ibge'] ?? null), + cep_ini: self::asString($data['cep_ini'] ?? null), + cep_fim: self::asString($data['cep_fim'] ?? null), + ceps: self::asInt($data['ceps'] ?? null), + ); + } + + /** + * The municipality as you would show it to a person: `"Areal/RJ"`, or just + * `"Areal"` when the API sent no UF. + */ + public function label(): string + { + return $this->uf === '' ? $this->localidade : $this->localidade . '/' . $this->uf; + } + + /** + * @return array{ + * localidade: string, uf: string, estado: string, regiao: string, + * ibge: string, cep_ini: string, cep_fim: string, ceps: int + * } + */ + public function toArray(): array + { + return [ + 'localidade' => $this->localidade, + 'uf' => $this->uf, + 'estado' => $this->estado, + 'regiao' => $this->regiao, + 'ibge' => $this->ibge, + 'cep_ini' => $this->cep_ini, + 'cep_fim' => $this->cep_fim, + 'ceps' => $this->ceps, + ]; + } + + private static function asString(mixed $value): string + { + return is_scalar($value) ? (string) $value : ''; + } + + private static function asInt(mixed $value): int + { + return is_numeric($value) ? (int) (float) $value : 0; + } +} |