Skip to content

Commit c9a1cc0

Browse files
committed
refactor(client): throw typed exceptions based on HTTP gateway status
Signed-off-by: Vitor Mattos <vitor@php.rio>
1 parent 57726a1 commit c9a1cc0

File tree

1 file changed

+87
-22
lines changed

1 file changed

+87
-22
lines changed

src/Http/NfseClient.php

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface;
1313
use LibreCodeCoop\NfsePHP\Dto\DpsData;
1414
use LibreCodeCoop\NfsePHP\Dto\ReceiptData;
15-
use LibreCodeCoop\NfsePHP\Exception\NfseException;
15+
use LibreCodeCoop\NfsePHP\Exception\CancellationException;
16+
use LibreCodeCoop\NfsePHP\Exception\IssuanceException;
17+
use LibreCodeCoop\NfsePHP\Exception\NetworkException;
18+
use LibreCodeCoop\NfsePHP\Exception\NfseErrorCode;
19+
use LibreCodeCoop\NfsePHP\Exception\QueryException;
1620
use LibreCodeCoop\NfsePHP\Xml\DpsSigner;
1721
use LibreCodeCoop\NfsePHP\Xml\XmlBuilder;
1822

@@ -48,21 +52,48 @@ public function emit(DpsData $dps): ReceiptData
4852
$xml = (new XmlBuilder())->buildDps($dps);
4953
$signed = $this->signer->sign($xml, $dps->cnpjPrestador);
5054

51-
$response = $this->post('/dps', $signed);
55+
[$httpStatus, $body] = $this->post('/dps', $signed);
5256

53-
return $this->parseReceiptResponse($response);
57+
if ($httpStatus >= 400) {
58+
throw new IssuanceException(
59+
'SEFIN gateway rejected issuance (HTTP ' . $httpStatus . ')',
60+
NfseErrorCode::IssuanceRejected,
61+
$httpStatus,
62+
$body,
63+
);
64+
}
65+
66+
return $this->parseReceiptResponse($body);
5467
}
5568

5669
public function query(string $chaveAcesso): ReceiptData
5770
{
58-
$response = $this->get('/dps/' . $chaveAcesso);
71+
[$httpStatus, $body] = $this->get('/dps/' . $chaveAcesso);
72+
73+
if ($httpStatus >= 400) {
74+
throw new QueryException(
75+
'SEFIN gateway returned error for query (HTTP ' . $httpStatus . ')',
76+
NfseErrorCode::QueryFailed,
77+
$httpStatus,
78+
$body,
79+
);
80+
}
5981

60-
return $this->parseReceiptResponse($response);
82+
return $this->parseReceiptResponse($body);
6183
}
6284

6385
public function cancel(string $chaveAcesso, string $motivo): bool
6486
{
65-
$this->delete('/dps/' . $chaveAcesso, $motivo);
87+
[$httpStatus, $body] = $this->delete('/dps/' . $chaveAcesso, $motivo);
88+
89+
if ($httpStatus >= 400) {
90+
throw new CancellationException(
91+
'SEFIN gateway rejected cancellation (HTTP ' . $httpStatus . ')',
92+
NfseErrorCode::CancellationRejected,
93+
$httpStatus,
94+
$body,
95+
);
96+
}
6697

6798
return true;
6899
}
@@ -72,24 +103,24 @@ public function cancel(string $chaveAcesso, string $motivo): bool
72103
// -------------------------------------------------------------------------
73104

74105
/**
75-
* @return array<string, mixed>
106+
* @return array{int, array<string, mixed>}
76107
*/
77108
private function post(string $path, string $xmlPayload): array
78109
{
79110
$context = stream_context_create([
80111
'http' => [
81-
'method' => 'POST',
82-
'header' => "Content-Type: application/xml\r\nAccept: application/json\r\n",
83-
'content' => $xmlPayload,
112+
'method' => 'POST',
113+
'header' => "Content-Type: application/xml\r\nAccept: application/json\r\n",
114+
'content' => $xmlPayload,
84115
'ignore_errors' => true,
85116
],
86117
]);
87118

88-
return $this->request($path, $context);
119+
return $this->fetchAndDecode($path, $context);
89120
}
90121

91122
/**
92-
* @return array<string, mixed>
123+
* @return array{int, array<string, mixed>}
93124
*/
94125
private function get(string $path): array
95126
{
@@ -101,10 +132,13 @@ private function get(string $path): array
101132
],
102133
]);
103134

104-
return $this->request($path, $context);
135+
return $this->fetchAndDecode($path, $context);
105136
}
106137

107-
private function delete(string $path, string $motivo): void
138+
/**
139+
* @return array{int, array<string, mixed>}
140+
*/
141+
private function delete(string $path, string $motivo): array
108142
{
109143
$payload = json_encode(['motivo' => $motivo], JSON_THROW_ON_ERROR);
110144
$context = stream_context_create([
@@ -116,28 +150,59 @@ private function delete(string $path, string $motivo): void
116150
],
117151
]);
118152

119-
$this->request($path, $context);
153+
return $this->fetchAndDecode($path, $context);
120154
}
121155

122156
/**
123-
* @return array<string, mixed>
157+
* Perform the raw HTTP request and decode the JSON body.
158+
*
159+
* PHP sets $http_response_header in the calling scope when file_get_contents
160+
* uses an HTTP wrapper. We initialize it to [] so static analysers have a
161+
* typed baseline; the HTTP wrapper will overwrite it on a successful
162+
* connection, even when the server responds with 4xx/5xx.
163+
*
164+
* @return array{int, array<string, mixed>}
124165
*/
125-
private function request(string $path, mixed $context): array
166+
private function fetchAndDecode(string $path, mixed $context): array
126167
{
127-
$url = $this->baseUrl . $path;
128-
$body = file_get_contents($url, false, $context);
168+
$url = $this->baseUrl . $path;
169+
170+
$http_response_header = [];
171+
$body = file_get_contents($url, false, $context);
172+
$httpStatus = $this->parseHttpStatus($http_response_header);
129173

130174
if ($body === false) {
131-
throw new NfseException('Failed to connect to SEFIN gateway at ' . $url);
175+
throw new NetworkException('Failed to connect to SEFIN gateway at ' . $url);
132176
}
133177

134178
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
135179

136180
if (!is_array($decoded)) {
137-
throw new NfseException('Unexpected response format from SEFIN gateway');
181+
throw new NetworkException(
182+
'Unexpected response format from SEFIN gateway',
183+
NfseErrorCode::InvalidResponse,
184+
);
185+
}
186+
187+
return [$httpStatus, $decoded];
188+
}
189+
190+
/**
191+
* Extract the HTTP status code from the first response header line.
192+
*
193+
* @param list<string> $headers
194+
*/
195+
private function parseHttpStatus(array $headers): int
196+
{
197+
if (!isset($headers[0])) {
198+
return 0;
199+
}
200+
201+
if (preg_match('/HTTP\/[\d.]+ (\d{3})/', $headers[0], $m)) {
202+
return (int) $m[1];
138203
}
139204

140-
return $decoded;
205+
return 0;
141206
}
142207

143208
/**

0 commit comments

Comments
 (0)