1 | <?php |
||||
2 | declare(strict_types=1); |
||||
3 | |||||
4 | namespace Bpost\BpostApiClient; |
||||
5 | |||||
6 | use Bpost\BpostApiClient\ApiCaller\ApiCaller; |
||||
7 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateLabelForBoxBuilder; |
||||
8 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateLabelForOrderBuilder; |
||||
9 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateLabelInBulkForOrdersBuilder; |
||||
10 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateOrReplaceOrderBuilder; |
||||
11 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\FetchOrderBuilder; |
||||
12 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\FetchProductConfigBuilder; |
||||
13 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\HttpRequestBuilderInterface; |
||||
14 | use Bpost\BpostApiClient\Bpost\Labels; |
||||
15 | use Bpost\BpostApiClient\Bpost\Order; |
||||
16 | use Bpost\BpostApiClient\Bpost\Order\Box; |
||||
17 | use Bpost\BpostApiClient\Bpost\Order\Box\Option\Insured; |
||||
18 | use Bpost\BpostApiClient\Bpost\ProductConfiguration; |
||||
19 | use Bpost\BpostApiClient\Common\ValidatedValue\LabelFormat; |
||||
20 | use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostCurlException; |
||||
21 | use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostInvalidResponseException; |
||||
22 | use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostInvalidSelectionException; |
||||
23 | use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostInvalidXmlResponseException; |
||||
24 | use Bpost\BpostApiClient\Exception\BpostLogicException; |
||||
25 | use Bpost\BpostApiClient\Exception\BpostLogicException\BpostInvalidValueException; |
||||
26 | use Bpost\BpostApiClient\Exception\BpostNotImplementedException; |
||||
27 | use Bpost\BpostApiClient\Exception\XmlException\BpostXmlInvalidItemException; |
||||
28 | use Bpost\BpostApiClient\Exception\XmlException\BpostXmlNoReferenceFoundException; |
||||
29 | use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\ModifyOrderBuilder; |
||||
30 | use Psr\Log\LoggerInterface; |
||||
31 | use Psr\Log\NullLogger; |
||||
32 | use SimpleXMLElement; |
||||
33 | |||||
34 | /** |
||||
35 | * Bpost class |
||||
36 | * |
||||
37 | * @author Tijs Verkoyen <[email protected]> |
||||
38 | * |
||||
39 | * @version 3.0.0 |
||||
40 | * |
||||
41 | * @copyright Copyright (c), Tijs Verkoyen. All rights reserved. |
||||
42 | * @license BSD License |
||||
43 | */ |
||||
44 | class Bpost |
||||
45 | { |
||||
46 | public const LABEL_FORMAT_A4 = 'A4'; |
||||
47 | public const LABEL_FORMAT_A6 = 'A6'; |
||||
48 | public const API_URL = 'https://shm-rest.bpost.cloud/services/shm'; |
||||
49 | public const VERSION = '3.3.0'; |
||||
50 | public const MIN_WEIGHT = 0; |
||||
51 | public const MAX_WEIGHT = 30000; |
||||
52 | private ?ApiCaller $apiCaller = null; |
||||
53 | private string $accountId; |
||||
54 | private string $passPhrase; |
||||
55 | private int $port = 0; |
||||
56 | private int $timeOut = 30; |
||||
57 | private ?string $userAgent = null; |
||||
58 | private string $apiUrl; |
||||
59 | private ?LoggerInterface $logger; |
||||
60 | |||||
61 | public function __construct(string $accountId, string $passPhrase, string $apiUrl = self::API_URL, ?LoggerInterface $logger = null) |
||||
62 | { |
||||
63 | $this->accountId = $accountId; |
||||
64 | $this->passPhrase = $passPhrase; |
||||
65 | $this->apiUrl = $apiUrl; |
||||
66 | $this->logger = $logger ?? new NullLogger(); |
||||
67 | } |
||||
68 | |||||
69 | public function getApiCaller(): ApiCaller |
||||
70 | { |
||||
71 | if ($this->apiCaller === null) { |
||||
72 | $this->apiCaller = new ApiCaller($this->logger); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
73 | } |
||||
74 | |||||
75 | return $this->apiCaller; |
||||
76 | } |
||||
77 | |||||
78 | public function setApiCaller(ApiCaller $apiCaller): void |
||||
79 | { |
||||
80 | $this->apiCaller = $apiCaller; |
||||
81 | } |
||||
82 | |||||
83 | /** |
||||
84 | * @throws BpostXmlInvalidItemException |
||||
85 | */ |
||||
86 | private static function decodeResponse(SimpleXMLElement $item, ?array $return = null, int $i = 0): array |
||||
87 | { |
||||
88 | $arrayKeys = [ |
||||
89 | 'barcode', |
||||
90 | 'orderLine', |
||||
91 | Insured::INSURANCE_TYPE_ADDITIONAL_INSURANCE, |
||||
92 | Box\Option\Messaging::MESSAGING_TYPE_INFO_DISTRIBUTED, |
||||
93 | 'infoPugo', |
||||
94 | ]; |
||||
95 | $integerKeys = ['totalPrice']; |
||||
96 | |||||
97 | foreach ($item as $key => $value) { |
||||
98 | $key = (string) $key; |
||||
99 | $attributes = (array) $value->attributes(); |
||||
100 | |||||
101 | if (!empty($attributes) && isset($attributes['@attributes'])) { |
||||
102 | $return[$key]['@attributes'] = $attributes['@attributes']; |
||||
103 | } |
||||
104 | |||||
105 | if (isset($value['nil']) && (string) $value['nil'] === 'true') { |
||||
106 | $return[$key] = null; |
||||
107 | } elseif (isset($value[0]) && (string) $value == '') { |
||||
108 | if (in_array($key, $arrayKeys, true)) { |
||||
109 | $return[$key][] = self::decodeResponse($value); |
||||
110 | } else { |
||||
111 | $return[$key] = self::decodeResponse($value, null, 1); |
||||
112 | } |
||||
113 | } else { |
||||
114 | if (in_array($key, $arrayKeys, true)) { |
||||
115 | $return[$key][] = (string) $value; |
||||
116 | } elseif ((string) $value === 'true') { |
||||
117 | $return[$key] = true; |
||||
118 | } elseif ((string) $value === 'false') { |
||||
119 | $return[$key] = false; |
||||
120 | } elseif (in_array($key, $integerKeys, true)) { |
||||
121 | $return[$key] = (int) $value; |
||||
122 | } else { |
||||
123 | $return[$key] = (string) $value; |
||||
124 | } |
||||
125 | } |
||||
126 | } |
||||
127 | |||||
128 | return $return ?? []; |
||||
129 | } |
||||
130 | |||||
131 | /** |
||||
132 | * @throws BpostCurlException |
||||
133 | * @throws BpostInvalidResponseException |
||||
134 | * @throws BpostInvalidSelectionException |
||||
135 | * @throws BpostInvalidXmlResponseException |
||||
136 | */ |
||||
137 | private function doCall(HttpRequestBuilderInterface $builder): string|SimpleXMLElement |
||||
138 | { |
||||
139 | $headers = $builder->getHeaders(); |
||||
140 | $headers[] = 'Authorization: Basic ' . $this->getAuthorizationHeader(); |
||||
141 | |||||
142 | $options = [ |
||||
143 | CURLOPT_URL => rtrim($this->apiUrl, '/') . '/' . rawurlencode($this->accountId) . $builder->getUrl(), |
||||
144 | CURLOPT_USERAGENT => $this->getUserAgent(), |
||||
145 | CURLOPT_RETURNTRANSFER => true, |
||||
146 | CURLOPT_TIMEOUT => $this->getTimeOut(), |
||||
147 | CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, |
||||
148 | CURLOPT_HTTPHEADER => $headers, |
||||
149 | ]; |
||||
150 | |||||
151 | if ($this->getPort() !== 0) { |
||||
152 | $options[CURLOPT_PORT] = $this->getPort(); |
||||
153 | } |
||||
154 | if ($builder->getMethod() === 'POST') { |
||||
155 | $options[CURLOPT_POST] = true; |
||||
156 | $options[CURLOPT_POSTFIELDS] = $builder->getXml(); |
||||
157 | } |
||||
158 | |||||
159 | $this->getApiCaller()->doCall($options); |
||||
160 | |||||
161 | $response = $this->getApiCaller()->getResponseBody(); |
||||
162 | $httpCode = $this->getApiCaller()->getResponseHttpCode(); |
||||
163 | $contentType = $this->getApiCaller()->getResponseContentType(); |
||||
164 | |||||
165 | if (!in_array($httpCode, [0, 200, 201], true)) { |
||||
166 | $xml = @simplexml_load_string($response); |
||||
167 | |||||
168 | if ($xml !== false && str_starts_with($xml->getName(), 'invalid')) { |
||||
169 | $message = (string) $xml->error; |
||||
170 | $code = isset($xml->code) ? (int) $xml->code : null; |
||||
171 | throw new BpostInvalidSelectionException($message, $code); |
||||
172 | } |
||||
173 | |||||
174 | $message = ''; |
||||
175 | if (($contentType !== null && str_contains($contentType, 'text/plain')) || in_array($httpCode, [400, 404], true)) { |
||||
176 | $message = $response; |
||||
177 | } |
||||
178 | |||||
179 | throw new BpostInvalidResponseException($message, $httpCode); |
||||
0 ignored issues
–
show
It seems like
$httpCode can also be of type null ; however, parameter $code of Bpost\BpostApiClient\Exc...xception::__construct() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
180 | } |
||||
181 | |||||
182 | if (!$builder->isExpectXml()) { |
||||
183 | return $response; |
||||
184 | } |
||||
185 | |||||
186 | $xml = @simplexml_load_string($response); |
||||
187 | if ($xml === false) { |
||||
188 | throw new BpostInvalidXmlResponseException(); |
||||
189 | } |
||||
190 | |||||
191 | return $xml; |
||||
192 | } |
||||
193 | |||||
194 | public function getAccountId(): string |
||||
195 | { |
||||
196 | return $this->accountId; |
||||
197 | } |
||||
198 | |||||
199 | private function getAuthorizationHeader(): string |
||||
200 | { |
||||
201 | return base64_encode($this->accountId . ':' . $this->passPhrase); |
||||
202 | } |
||||
203 | |||||
204 | public function getPassPhrase(): string |
||||
205 | { |
||||
206 | return $this->passPhrase; |
||||
207 | } |
||||
208 | |||||
209 | public function getPort(): int |
||||
210 | { |
||||
211 | return $this->port; |
||||
212 | } |
||||
213 | |||||
214 | public function getTimeOut(): int |
||||
215 | { |
||||
216 | return $this->timeOut; |
||||
217 | } |
||||
218 | |||||
219 | public function getUserAgent(): string |
||||
220 | { |
||||
221 | return 'PHP Bpost/' . self::VERSION . ' ' . ($this->userAgent ?? ''); |
||||
222 | } |
||||
223 | |||||
224 | public function setTimeOut(int $seconds): void |
||||
225 | { |
||||
226 | $this->timeOut = $seconds; |
||||
227 | } |
||||
228 | |||||
229 | public function setUserAgent(string $userAgent): void |
||||
230 | { |
||||
231 | $this->userAgent = $userAgent; |
||||
232 | } |
||||
233 | |||||
234 | // ========== Webservice methods ========== |
||||
235 | |||||
236 | /** |
||||
237 | * @throws BpostCurlException |
||||
238 | * @throws BpostInvalidResponseException |
||||
239 | * @throws BpostInvalidSelectionException |
||||
240 | * @throws BpostInvalidXmlResponseException |
||||
241 | */ |
||||
242 | public function createOrReplaceOrder(Order $order): bool |
||||
243 | { |
||||
244 | $builder = new CreateOrReplaceOrderBuilder($order, $this->accountId); |
||||
245 | return $this->doCall($builder) === ''; |
||||
246 | } |
||||
247 | |||||
248 | |||||
249 | /** |
||||
250 | * @throws BpostNotImplementedException |
||||
251 | * @throws BpostXmlNoReferenceFoundException |
||||
252 | * @throws BpostInvalidSelectionException |
||||
253 | * @throws BpostInvalidValueException |
||||
254 | * @throws BpostInvalidResponseException |
||||
255 | * @throws BpostCurlException |
||||
256 | * @throws BpostInvalidXmlResponseException |
||||
257 | */ |
||||
258 | public function fetchOrder(string $reference): Order |
||||
259 | { |
||||
260 | $builder = new FetchOrderBuilder($reference); |
||||
261 | $xml = $this->doCall($builder); |
||||
262 | \assert($xml instanceof SimpleXMLElement); |
||||
263 | |||||
264 | return Order::createFromXML($xml); |
||||
265 | } |
||||
266 | |||||
267 | /** |
||||
268 | * @throws BpostCurlException |
||||
269 | * @throws BpostInvalidResponseException |
||||
270 | * @throws BpostInvalidSelectionException |
||||
271 | * @throws BpostInvalidXmlResponseException |
||||
272 | */ |
||||
273 | public function fetchProductConfig(): ProductConfiguration |
||||
274 | { |
||||
275 | $builder = new FetchProductConfigBuilder(); |
||||
276 | $xml = $this->doCall($builder); |
||||
277 | \assert($xml instanceof SimpleXMLElement); |
||||
278 | |||||
279 | return ProductConfiguration::createFromXML($xml); |
||||
280 | } |
||||
281 | |||||
282 | /** |
||||
283 | * @throws BpostCurlException |
||||
284 | * @throws BpostInvalidResponseException |
||||
285 | * @throws BpostInvalidSelectionException |
||||
286 | * @throws BpostInvalidValueException |
||||
287 | * @throws BpostInvalidXmlResponseException |
||||
288 | */ |
||||
289 | public function modifyOrderStatus(string $reference, string $status): bool |
||||
290 | { |
||||
291 | $builder = new ModifyOrderBuilder($reference, $status); |
||||
292 | return $this->doCall($builder) === ''; |
||||
293 | } |
||||
294 | |||||
295 | /** @return string[] */ |
||||
296 | public static function getPossibleLabelFormatValues(): array |
||||
297 | { |
||||
298 | return [self::LABEL_FORMAT_A4, self::LABEL_FORMAT_A6]; |
||||
299 | } |
||||
300 | |||||
301 | /** |
||||
302 | * @throws BpostInvalidResponseException |
||||
303 | * @throws BpostLogicException |
||||
304 | * @throws BpostCurlException |
||||
305 | * @throws BpostInvalidSelectionException |
||||
306 | * @throws BpostInvalidXmlResponseException |
||||
307 | * @throws BpostInvalidValueException |
||||
308 | */ |
||||
309 | public function createLabelForOrder( |
||||
310 | string $reference, |
||||
311 | string $format = self::LABEL_FORMAT_A6, |
||||
312 | bool $withReturnLabels = false, |
||||
313 | bool $asPdf = false |
||||
314 | ): array { |
||||
315 | $builder = new CreateLabelForOrderBuilder($reference, new LabelFormat($format), $asPdf, $withReturnLabels); |
||||
316 | $xml = $this->doCall($builder); |
||||
317 | \assert($xml instanceof SimpleXMLElement); |
||||
318 | |||||
319 | return Labels::createFromXML($xml); |
||||
320 | } |
||||
321 | |||||
322 | /** |
||||
323 | * @throws BpostInvalidResponseException |
||||
324 | * @throws BpostLogicException |
||||
325 | * @throws BpostCurlException |
||||
326 | * @throws BpostInvalidXmlResponseException |
||||
327 | * @throws BpostInvalidSelectionException |
||||
328 | * @throws BpostInvalidValueException |
||||
329 | */ |
||||
330 | public function createLabelForBox( |
||||
331 | string $barcode, |
||||
332 | string $format = self::LABEL_FORMAT_A6, |
||||
333 | bool $withReturnLabels = false, |
||||
334 | bool $asPdf = false |
||||
335 | ): array { |
||||
336 | $builder = new CreateLabelForBoxBuilder($barcode, new LabelFormat($format), $asPdf, $withReturnLabels); |
||||
337 | $xml = $this->doCall($builder); |
||||
338 | \assert($xml instanceof SimpleXMLElement); |
||||
339 | |||||
340 | return Labels::createFromXML($xml); |
||||
341 | } |
||||
342 | |||||
343 | /** |
||||
344 | * @throws BpostInvalidResponseException |
||||
345 | * @throws BpostLogicException |
||||
346 | * @throws BpostCurlException |
||||
347 | * @throws BpostInvalidXmlResponseException |
||||
348 | * @throws BpostInvalidSelectionException |
||||
349 | * @throws BpostInvalidValueException |
||||
350 | */ |
||||
351 | public function createLabelInBulkForOrders( |
||||
352 | array $references, |
||||
353 | string $format = LabelFormat::FORMAT_A6, |
||||
354 | bool $withReturnLabels = false, |
||||
355 | bool $asPdf = false, |
||||
356 | bool $forcePrinting = false |
||||
357 | ): array { |
||||
358 | $builder = new CreateLabelInBulkForOrdersBuilder( |
||||
359 | $references, |
||||
360 | new LabelFormat($format), |
||||
361 | $asPdf, |
||||
362 | $withReturnLabels, |
||||
363 | $forcePrinting |
||||
364 | ); |
||||
365 | $xml = $this->doCall($builder); |
||||
366 | \assert($xml instanceof SimpleXMLElement); |
||||
367 | |||||
368 | return Labels::createFromXML($xml); |
||||
369 | } |
||||
370 | |||||
371 | public function setLogger(LoggerInterface $logger): void |
||||
372 | { |
||||
373 | $this->logger->setLogger($logger); |
||||
374 | } |
||||
375 | |||||
376 | public function isValidWeight(int $weight): bool |
||||
377 | { |
||||
378 | return self::MIN_WEIGHT <= $weight && $weight <= self::MAX_WEIGHT; |
||||
379 | } |
||||
380 | } |