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
|
<?php
declare(strict_types=1);
namespace Cepis;
/**
* A CEP and everything the database knows about it.
*
* **Property names are the API's own JSON keys, in Portuguese** —
* `logradouro`, `bairro`, `localidade`. Everything else in this library is
* English; the data fields are the one deliberate exception, so that what you
* read in the JSON at https://cep.is/01001000 is exactly what you read on the
* object, with no translation layer and no field-by-field mapping to keep in
* your head. The values are Portuguese either way — they come from the
* Correios database.
*
* Fields the database does not have for that CEP come back as empty strings,
* never `null` — that is what the API returns.
*
* {@see Address::$geo} is the exception: it is `null` when there is no
* coordinate. There the difference is real, because the API **omits** the
* whole field in that case (about 43% of CEPs) rather than sending null.
*
* {@see Address::$raw} holds the response exactly as it arrived,
* uninterpreted, so a field the API grows before this library is updated is
* not lost.
*/
final class Address
{
/**
* The JSON keys mapped onto properties, in the API's own order.
*/
public const FIELDS = [
'cep', 'logradouro', 'complemento', 'unidade', 'bairro', 'localidade',
'uf', 'estado', 'regiao', 'ibge', 'gia', 'ddd', 'siafi',
];
/**
* @param array<string, mixed> $raw
*/
public function __construct(
public readonly string $cep = '',
public readonly string $logradouro = '',
public readonly string $complemento = '',
public readonly string $unidade = '',
public readonly string $bairro = '',
public readonly string $localidade = '',
public readonly string $uf = '',
public readonly string $estado = '',
public readonly string $regiao = '',
public readonly string $ibge = '',
public readonly string $gia = '',
public readonly string $ddd = '',
public readonly string $siafi = '',
public readonly ?Geo $geo = null,
public readonly array $raw = [],
) {
}
/**
* Build from the decoded response.
*
* Unknown fields are ignored during construction but preserved in
* {@see Address::$raw}. A new field in the API therefore cannot break this
* library.
*
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
$values = [];
foreach (self::FIELDS as $name) {
$value = $data[$name] ?? '';
$values[$name] = is_scalar($value) ? (string) $value : '';
}
return new self(
cep: $values['cep'],
logradouro: $values['logradouro'],
complemento: $values['complemento'],
unidade: $values['unidade'],
bairro: $values['bairro'],
localidade: $values['localidade'],
uf: $values['uf'],
estado: $values['estado'],
regiao: $values['regiao'],
ibge: $values['ibge'],
gia: $values['gia'],
ddd: $values['ddd'],
siafi: $values['siafi'],
geo: Geo::fromArray($data['geo'] ?? null),
raw: $data,
);
}
/**
* Read any field of the raw response, including one the API grew after
* this release, or `null` when it is absent.
*/
public function field(string $name): ?string
{
$value = $this->raw[$name] ?? null;
return is_scalar($value) ? (string) $value : null;
}
/**
* The CEP with its mask: `01001-000`.
*
* The API always returns it unmasked; formatting is presentation, and it
* lives here so it does not become a repeated `sprintf` in every project
* that uses this.
*/
public function formattedCep(): string
{
return substr($this->cep, 0, 5) . '-' . substr($this->cep, 5, 3);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$out = [];
foreach (self::FIELDS as $name) {
$out[$name] = $this->{$name};
}
$out['geo'] = $this->geo?->toArray();
return $out;
}
}
|