summaryrefslogtreecommitdiffstats
path: root/src/Faixa.php
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:31:30 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:31:30 -0300
commit67276c6444a70b61030ab3ee52b84dc5014af0e8 (patch)
treeac0246ed287c6aec57e335403b20b23214ec4eb0 /src/Faixa.php
parent64764fb9fac0227f8e0443e7c7519d73aec275b9 (diff)
downloadphp-0.2.0.tar.gz
Carry the municipality of a not-found CEP on the exceptionHEADv0.2.0main
The API now sends an optional faixa block on the 404 of GET /{cep}, naming the municipality the CEP would have belonged to. That failing CEP is rarely random: it is usually someone who knows the city and guessed the number, most often the -000 that 957 municipalities never received. Knowing the lookup failed "in Areal/RJ" is the difference between a message a user can act on and a bare "CEP not found". It stays an error. Faixa hangs off CepNotFoundException, never off Address, and parsing is deliberately forgiving: a missing, empty or malformed block yields null rather than turning a clean not-found into a parse failure. Its cep_ini/cep_fim are the CEPs that exist, not the bounds the Correios publish, and the interval between them is sparse -- hence ceps, and hence the docblocks insisting the pair is not a list of valid CEPs.
Diffstat (limited to 'src/Faixa.php')
-rw-r--r--src/Faixa.php137
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;
+ }
+}