1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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;
}
}
|