Issues (112)

src/Bpost.php (3 issues)

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);
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();
0 ignored issues
show
The method attributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

99
            $attributes = (array) $value->/** @scrutinizer ignore-call */ attributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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);
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);
0 ignored issues
show
The method setLogger() does not exist on Psr\Log\LoggerInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

373
        $this->logger->/** @scrutinizer ignore-call */ 
374
                       setLogger($logger);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The method setLogger() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

373
        $this->logger->/** @scrutinizer ignore-call */ 
374
                       setLogger($logger);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
374
    }
375
376
    public function isValidWeight(int $weight): bool
377
    {
378
        return self::MIN_WEIGHT <= $weight && $weight <= self::MAX_WEIGHT;
379
    }
380
}