Test Failed
Branch master (62528d)
by Zangra
14:14 queued 12:04
created

Bpost::createOrReplaceOrder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 2
c 2
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 1
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 Psr\Log\LoggerInterface;
30
use Psr\Log\NullLogger;
31
use SimpleXMLElement;
32
33
/**
34
 * Bpost class
35
 *
36
 * @author    Tijs Verkoyen <[email protected]>
37
 *
38
 * @version   3.0.0
39
 *
40
 * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
41
 * @license   BSD License
42
 */
43
class Bpost
44
{
45
    public const LABEL_FORMAT_A4 = 'A4';
46
    public const LABEL_FORMAT_A6 = 'A6';
47
    public const API_URL = 'https://shm-rest.bpost.cloud/services/shm';
48
    public const VERSION = '3.3.0';
49
    public const MIN_WEIGHT = 0;
50
    public const MAX_WEIGHT = 30000;
51
    private ?ApiCaller $apiCaller = null;
52
    private string $accountId;
53
    private string $passPhrase;
54
    private int $port = 0;
55
    private int $timeOut = 30;
56
    private ?string $userAgent = null;
57
    private string $apiUrl;
58
    private ?LoggerInterface $logger;
59
60
    public function __construct(string $accountId, string $passPhrase, string $apiUrl = self::API_URL, ?LoggerInterface $logger = null)
61
    {
62
        $this->accountId  = $accountId;
63
        $this->passPhrase = $passPhrase;
64
        $this->apiUrl     = $apiUrl;
65
        $this->logger     = $logger ?? new NullLogger();
66
    }
67
68
    public function getApiCaller(): ApiCaller
69
    {
70
        if ($this->apiCaller === null) {
71
            $this->apiCaller = new ApiCaller($this->logger);
0 ignored issues
show
Bug introduced by
It seems like $this->logger can also be of type null; however, parameter $logger of Bpost\BpostApiClient\Api...piCaller::__construct() does only seem to accept Psr\Log\LoggerInterface, 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 ignore-type  annotation

71
            $this->apiCaller = new ApiCaller(/** @scrutinizer ignore-type */ $this->logger);
Loading history...
72
        }
73
74
        return $this->apiCaller;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->apiCaller could return the type null which is incompatible with the type-hinted return Bpost\BpostApiClient\ApiCaller\ApiCaller. Consider adding an additional type-check to rule them out.
Loading history...
75
    }
76
77
    public function setApiCaller(ApiCaller $apiCaller): void
78
    {
79
        $this->apiCaller = $apiCaller;
80
    }
81
82
    /**
83
     * @throws BpostXmlInvalidItemException
84
     */
85
    private static function decodeResponse(SimpleXMLElement $item, ?array $return = null, int $i = 0): array
86
    {
87
        $arrayKeys   = [
88
            'barcode',
89
            'orderLine',
90
            Insured::INSURANCE_TYPE_ADDITIONAL_INSURANCE,
91
            Box\Option\Messaging::MESSAGING_TYPE_INFO_DISTRIBUTED,
92
            'infoPugo',
93
        ];
94
        $integerKeys = ['totalPrice'];
95
96
        foreach ($item as $key => $value) {
97
            $key = (string) $key;
98
            $attributes = (array) $value->attributes();
0 ignored issues
show
Bug introduced by
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

98
            $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...
99
100
            if (!empty($attributes) && isset($attributes['@attributes'])) {
101
                $return[$key]['@attributes'] = $attributes['@attributes'];
102
            }
103
104
            if (isset($value['nil']) && (string) $value['nil'] === 'true') {
105
                $return[$key] = null;
106
            } elseif (isset($value[0]) && (string) $value == '') {
107
                if (in_array($key, $arrayKeys, true)) {
108
                    $return[$key][] = self::decodeResponse($value);
109
                } else {
110
                    $return[$key] = self::decodeResponse($value, null, 1);
111
                }
112
            } else {
113
                if (in_array($key, $arrayKeys, true)) {
114
                    $return[$key][] = (string) $value;
115
                } elseif ((string) $value === 'true') {
116
                    $return[$key] = true;
117
                } elseif ((string) $value === 'false') {
118
                    $return[$key] = false;
119
                } elseif (in_array($key, $integerKeys, true)) {
120
                    $return[$key] = (int) $value;
121
                } else {
122
                    $return[$key] = (string) $value;
123
                }
124
            }
125
        }
126
127
        return $return ?? [];
128
    }
129
130
    /**
131
     * @throws BpostCurlException
132
     * @throws BpostInvalidResponseException
133
     * @throws BpostInvalidSelectionException
134
     * @throws BpostInvalidXmlResponseException
135
     */
136
    private function doCall(HttpRequestBuilderInterface $builder): string|SimpleXMLElement
137
    {
138
        $headers   = $builder->getHeaders();
139
        $headers[] = 'Authorization: Basic ' . $this->getAuthorizationHeader();
140
141
        $options = [
142
            CURLOPT_URL            => rtrim($this->apiUrl, '/') . '/' . rawurlencode($this->accountId) . $builder->getUrl(),
143
            CURLOPT_USERAGENT      => $this->getUserAgent(),
144
            CURLOPT_RETURNTRANSFER => true,
145
            CURLOPT_TIMEOUT        => $this->getTimeOut(),
146
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
147
            CURLOPT_HTTPHEADER     => $headers,
148
        ];
149
150
        if ($this->getPort() !== 0) {
151
            $options[CURLOPT_PORT] = $this->getPort();
152
        }
153
        if ($builder->getMethod() === 'POST') {
154
            $options[CURLOPT_POST]       = true;
155
            $options[CURLOPT_POSTFIELDS] = $builder->getXml();
156
        }
157
158
        $this->getApiCaller()->doCall($options);
159
160
        $response    = $this->getApiCaller()->getResponseBody();
161
        $httpCode    = $this->getApiCaller()->getResponseHttpCode();
162
        $contentType = $this->getApiCaller()->getResponseContentType();
163
164
        if (!in_array($httpCode, [0, 200, 201], true)) {
165
            $xml = @simplexml_load_string($response);
166
167
            if ($xml !== false && str_starts_with($xml->getName(), 'invalid')) {
168
                $message = (string) $xml->error;
169
                $code    = isset($xml->code) ? (int) $xml->code : null;
170
                throw new BpostInvalidSelectionException($message, $code);
171
            }
172
173
            $message = '';
174
            if (($contentType !== null && str_contains($contentType, 'text/plain')) || in_array($httpCode, [400, 404], true)) {
175
                $message = $response;
176
            }
177
178
            throw new BpostInvalidResponseException($message, $httpCode);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

178
            throw new BpostInvalidResponseException($message, /** @scrutinizer ignore-type */ $httpCode);
Loading history...
179
        }
180
181
        if (!$builder->isExpectXml()) {
182
            return $response;
183
        }
184
185
        $xml = @simplexml_load_string($response);
186
        if ($xml === false) {
187
            throw new BpostInvalidXmlResponseException();
188
        }
189
190
        return $xml;
191
    }
192
193
    public function getAccountId(): string
194
    {
195
        return $this->accountId;
196
    }
197
198
    private function getAuthorizationHeader(): string
199
    {
200
        return base64_encode($this->accountId . ':' . $this->passPhrase);
201
    }
202
203
    public function getPassPhrase(): string
204
    {
205
        return $this->passPhrase;
206
    }
207
208
    public function getPort(): int
209
    {
210
        return $this->port;
211
    }
212
213
    public function getTimeOut(): int
214
    {
215
        return $this->timeOut;
216
    }
217
218
    public function getUserAgent(): string
219
    {
220
        return 'PHP Bpost/' . self::VERSION . ' ' . ($this->userAgent ?? '');
221
    }
222
223
    public function setTimeOut(int $seconds): void
224
    {
225
        $this->timeOut = $seconds;
226
    }
227
228
    public function setUserAgent(string $userAgent): void
229
    {
230
        $this->userAgent = $userAgent;
231
    }
232
233
    // ========== Webservice methods ==========
234
235
    /**
236
     * @throws BpostCurlException
237
     * @throws BpostInvalidResponseException
238
     * @throws BpostInvalidSelectionException
239
     * @throws BpostInvalidXmlResponseException
240
     */
241
    public function createOrReplaceOrder(Order $order): bool
242
    {
243
        $builder = new CreateOrReplaceOrderBuilder($order, $this->accountId);
244
        return $this->doCall($builder) === '';
245
    }
246
247
248
    /**
249
     * @throws BpostNotImplementedException
250
     * @throws BpostXmlNoReferenceFoundException
251
     * @throws BpostInvalidSelectionException
252
     * @throws BpostInvalidValueException
253
     * @throws BpostInvalidResponseException
254
     * @throws BpostCurlException
255
     * @throws BpostInvalidXmlResponseException
256
     */
257
    public function fetchOrder(string $reference): Order
258
    {
259
        $builder = new FetchOrderBuilder($reference);
260
        $xml     = $this->doCall($builder);
261
        \assert($xml instanceof SimpleXMLElement);
262
263
        return Order::createFromXML($xml);
264
    }
265
266
    /**
267
     * @throws BpostCurlException
268
     * @throws BpostInvalidResponseException
269
     * @throws BpostInvalidSelectionException
270
     * @throws BpostInvalidXmlResponseException
271
     */
272
    public function fetchProductConfig(): ProductConfiguration
273
    {
274
        $builder = new FetchProductConfigBuilder();
275
        $xml     = $this->doCall($builder);
276
        \assert($xml instanceof SimpleXMLElement);
277
278
        return ProductConfiguration::createFromXML($xml);
279
    }
280
281
    /**
282
     * @throws BpostCurlException
283
     * @throws BpostInvalidResponseException
284
     * @throws BpostInvalidSelectionException
285
     * @throws BpostInvalidValueException
286
     * @throws BpostInvalidXmlResponseException
287
     */
288
    public function modifyOrderStatus(string $reference, string $status): bool
289
    {
290
        $builder = new ModifyOrderBuilder($reference, $status);
0 ignored issues
show
Bug introduced by
The type Bpost\BpostApiClient\ModifyOrderBuilder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
291
        return $this->doCall($builder) === '';
292
    }
293
294
    /** @return string[] */
295
    public static function getPossibleLabelFormatValues(): array
296
    {
297
        return [self::LABEL_FORMAT_A4, self::LABEL_FORMAT_A6];
298
    }
299
300
    /**
301
     * @throws BpostInvalidResponseException
302
     * @throws BpostLogicException
303
     * @throws BpostCurlException
304
     * @throws BpostInvalidSelectionException
305
     * @throws BpostInvalidXmlResponseException
306
     * @throws BpostInvalidValueException
307
     */
308
    public function createLabelForOrder(
309
        string $reference,
310
        string $format = self::LABEL_FORMAT_A6,
311
        bool $withReturnLabels = false,
312
        bool $asPdf = false
313
    ): array {
314
        $builder = new CreateLabelForOrderBuilder($reference, new LabelFormat($format), $asPdf, $withReturnLabels);
315
        $xml     = $this->doCall($builder);
316
        \assert($xml instanceof SimpleXMLElement);
317
318
        return Labels::createFromXML($xml);
319
    }
320
321
    /**
322
     * @throws BpostInvalidResponseException
323
     * @throws BpostLogicException
324
     * @throws BpostCurlException
325
     * @throws BpostInvalidXmlResponseException
326
     * @throws BpostInvalidSelectionException
327
     * @throws BpostInvalidValueException
328
     */
329
    public function createLabelForBox(
330
        string $barcode,
331
        string $format = self::LABEL_FORMAT_A6,
332
        bool $withReturnLabels = false,
333
        bool $asPdf = false
334
    ): array {
335
        $builder = new CreateLabelForBoxBuilder($barcode, new LabelFormat($format), $asPdf, $withReturnLabels);
336
        $xml     = $this->doCall($builder);
337
        \assert($xml instanceof SimpleXMLElement);
338
339
        return Labels::createFromXML($xml);
340
    }
341
342
    /**
343
     * @throws BpostInvalidResponseException
344
     * @throws BpostLogicException
345
     * @throws BpostCurlException
346
     * @throws BpostInvalidXmlResponseException
347
     * @throws BpostInvalidSelectionException
348
     * @throws BpostInvalidValueException
349
     */
350
    public function createLabelInBulkForOrders(
351
        array $references,
352
        string $format = LabelFormat::FORMAT_A6,
353
        bool $withReturnLabels = false,
354
        bool $asPdf = false,
355
        bool $forcePrinting = false
356
    ): array {
357
        $builder = new CreateLabelInBulkForOrdersBuilder(
358
            $references,
359
            new LabelFormat($format),
360
            $asPdf,
361
            $withReturnLabels,
362
            $forcePrinting
363
        );
364
        $xml = $this->doCall($builder);
365
        \assert($xml instanceof SimpleXMLElement);
366
367
        return Labels::createFromXML($xml);
368
    }
369
370
    public function setLogger(LoggerInterface $logger): void
371
    {
372
        $this->logger->setLogger($logger);
0 ignored issues
show
Bug introduced by
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

372
        $this->logger->/** @scrutinizer ignore-call */ 
373
                       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...
Bug introduced by
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

372
        $this->logger->/** @scrutinizer ignore-call */ 
373
                       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...
373
    }
374
375
    public function isValidWeight(int $weight): bool
376
    {
377
        return self::MIN_WEIGHT <= $weight && $weight <= self::MAX_WEIGHT;
378
    }
379
}