Bpost   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 560
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 51
eloc 132
dl 0
loc 560
rs 7.92
c 5
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A fetchProductConfig() 0 7 1
A setTimeOut() 0 3 1
A __destruct() 0 5 2
A createLabelForBox() 0 11 1
A setLogger() 0 3 1
C decodeResponse() 0 53 14
A setUserAgent() 0 3 1
A createLabelInBulkForOrders() 0 18 1
A getAccountId() 0 3 1
A fetchOrder() 0 7 1
A modifyOrderStatus() 0 5 1
A getApiCaller() 0 7 2
A getPassPhrase() 0 3 1
A createLabelForOrder() 0 11 1
A createOrReplaceOrder() 0 5 1
A getUserAgent() 0 3 1
C doCall() 0 70 12
A __construct() 0 6 1
A getAuthorizationHeader() 0 3 1
A getPossibleLabelFormatValues() 0 5 1
A getPort() 0 3 1
A setApiCaller() 0 3 1
A getTimeOut() 0 3 1
A isValidWeight() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Bpost often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Bpost, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Bpost\BpostApiClient;
4
5
use Bpost\BpostApiClient\ApiCaller\ApiCaller;
6
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateLabelForBoxBuilder;
7
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateLabelForOrderBuilder;
8
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateLabelInBulkForOrdersBuilder;
9
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\CreateOrReplaceOrderBuilder;
10
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\FetchOrderBuilder;
11
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\FetchProductConfigBuilder;
12
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\HttpRequestBuilderInterface;
13
use Bpost\BpostApiClient\Bpost\HttpRequestBuilder\ModifyOrderBuilder;
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\BpostInvalidValueException;
25
use Bpost\BpostApiClient\Exception\BpostNotImplementedException;
26
use Bpost\BpostApiClient\Exception\XmlException\BpostXmlInvalidItemException;
27
use Bpost\BpostApiClient\Exception\XmlException\BpostXmlNoReferenceFoundException;
28
use Psr\Log\LoggerInterface;
29
use SimpleXMLElement;
30
31
/**
32
 * Bpost class
33
 *
34
 * @author    Tijs Verkoyen <[email protected]>
35
 *
36
 * @version   3.0.0
37
 *
38
 * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
39
 * @license   BSD License
40
 */
41
class Bpost
42
{
43
    const LABEL_FORMAT_A4 = 'A4';
44
    const LABEL_FORMAT_A6 = 'A6';
45
46
    // URL for the api
47
    const API_URL = 'https://shm-rest.bpost.cloud/services/shm';
48
49
    // current version
50
    const VERSION = '3.3.0';
51
52
    /** Min weight, in grams, for a shipping */
53
    const MIN_WEIGHT = 0;
54
55
    /** Max weight, in grams, for a shipping */
56
    const MAX_WEIGHT = 30000;
57
58
    /** @var ApiCaller */
59
    private $apiCaller;
60
61
    /**
62
     * The account id
63
     *
64
     * @var string
65
     */
66
    private $accountId;
67
68
    /**
69
     * A cURL instance
70
     *
71
     * @var resource
72
     */
73
    private $curl;
74
75
    /**
76
     * The passPhrase
77
     *
78
     * @var string
79
     */
80
    private $passPhrase;
81
82
    /**
83
     * The port to use.
84
     *
85
     * @var int
86
     */
87
    private $port;
88
89
    /**
90
     * The timeout
91
     *
92
     * @var int
93
     */
94
    private $timeOut = 30;
95
96
    /**
97
     * The user agent
98
     *
99
     * @var string
100
     */
101
    private $userAgent;
102
103
    private $apiUrl;
104
105
    /** @var Logger */
106
    private $logger;
107
108
    /**
109
     * Create Bpost instance
110
     *
111
     * @param string $accountId
112
     * @param string $passPhrase
113
     * @param string $apiUrl
114
     */
115
    public function __construct($accountId, $passPhrase, $apiUrl = self::API_URL)
116
    {
117
        $this->accountId = (string) $accountId;
118
        $this->passPhrase = (string) $passPhrase;
119
        $this->apiUrl = (string) $apiUrl;
120
        $this->logger = new Logger();
121
    }
122
123
    /**
124
     * @return ApiCaller
125
     */
126
    public function getApiCaller()
127
    {
128
        if ($this->apiCaller === null) {
129
            $this->apiCaller = new ApiCaller($this->logger);
130
        }
131
132
        return $this->apiCaller;
133
    }
134
135
    /**
136
     * @param ApiCaller $apiCaller
137
     */
138
    public function setApiCaller(ApiCaller $apiCaller)
139
    {
140
        $this->apiCaller = $apiCaller;
141
    }
142
143
    /**
144
     * Destructor
145
     */
146
    public function __destruct()
147
    {
148
        if ($this->curl !== null) {
149
            curl_close($this->curl);
150
            $this->curl = null;
151
        }
152
    }
153
154
    /**
155
     * Decode the response
156
     *
157
     * @param SimpleXMLElement $item   The item to decode.
158
     * @param array            $return Just a placeholder.
159
     * @param int              $i      A internal counter.
160
     *
161
     * @return array
162
     *
163
     * @throws BpostXmlInvalidItemException
164
     */
165
    private static function decodeResponse($item, $return = null, $i = 0)
166
    {
167
        if (!$item instanceof SimpleXMLElement) {
0 ignored issues
show
introduced by
$item is always a sub-type of SimpleXMLElement.
Loading history...
168
            throw new BpostXmlInvalidItemException();
169
        }
170
171
        $arrayKeys = array(
172
            'barcode',
173
            'orderLine',
174
            Insured::INSURANCE_TYPE_ADDITIONAL_INSURANCE,
175
            Box\Option\Messaging::MESSAGING_TYPE_INFO_DISTRIBUTED,
176
            'infoPugo',
177
        );
178
        $integerKeys = array('totalPrice');
179
180
        /** @var SimpleXMLElement $value */
181
        foreach ($item as $key => $value) {
182
            $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

182
            $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...
183
184
            if (!empty($attributes) && isset($attributes['@attributes'])) {
185
                $return[$key]['@attributes'] = $attributes['@attributes'];
186
            }
187
188
            // empty
189
            if (isset($value['nil']) && (string) $value['nil'] === 'true') {
190
                $return[$key] = null;
191
            } // empty
192
            elseif (isset($value[0]) && (string) $value == '') {
193
                if (in_array($key, $arrayKeys)) {
194
                    $return[$key][] = self::decodeResponse($value);
195
                } else {
196
                    $return[$key] = self::decodeResponse($value, null, 1);
197
                }
198
            } else {
199
                // arrays
200
                if (in_array($key, $arrayKeys)) {
201
                    $return[$key][] = (string) $value;
202
                } // booleans
203
                elseif ((string) $value == 'true') {
204
                    $return[$key] = true;
205
                } elseif ((string) $value == 'false') {
206
                    $return[$key] = false;
207
                } // integers
208
                elseif (in_array($key, $integerKeys)) {
209
                    $return[$key] = (int) $value;
210
                } // fallback to string
211
                else {
212
                    $return[$key] = (string) $value;
213
                }
214
            }
215
        }
216
217
        return $return;
218
    }
219
220
    /**
221
     * Make the call
222
     *
223
     * @return string|SimpleXMLElement
224
     *
225
     * @throws BpostCurlException
226
     * @throws BpostInvalidResponseException
227
     * @throws BpostInvalidSelectionException
228
     * @throws BpostInvalidXmlResponseException
229
     */
230
    private function doCall(HttpRequestBuilderInterface $builder)
231
    {
232
        $headers = $builder->getHeaders();
233
234
        // build Authorization header
235
        $headers[] = 'Authorization: Basic ' . $this->getAuthorizationHeader();
236
237
        // set options
238
        $options = array();
239
        $options[CURLOPT_URL] = $this->apiUrl . '/' . $this->accountId . $builder->getUrl();
240
        if ($this->getPort() != 0) {
241
            $options[CURLOPT_PORT] = $this->getPort();
242
        }
243
        $options[CURLOPT_USERAGENT] = $this->getUserAgent();
244
        $options[CURLOPT_RETURNTRANSFER] = true;
245
        $options[CURLOPT_TIMEOUT] = (int) $this->getTimeOut();
246
        $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
247
        $options[CURLOPT_HTTPHEADER] = $headers;
248
249
        if ($builder->getMethod() == 'POST') {
250
            $options[CURLOPT_POST] = true;
251
            $options[CURLOPT_POSTFIELDS] = $builder->getXml();
252
        }
253
254
        $this->getApiCaller()->doCall($options);
255
256
        $response = $this->getApiCaller()->getResponseBody();
257
        $httpCode = $this->getApiCaller()->getResponseHttpCode();
258
        $contentType = $this->getApiCaller()->getResponseContentType();
259
260
        // valid HTTP-code
261
        if (!in_array($httpCode, array(0, 200, 201))) {
262
            // convert into XML
263
            $xml = @simplexml_load_string($response);
264
265
            // validate
266
            if ($xml !== false && (substr($xml->getName(), 0, 7) == 'invalid')
267
            ) {
268
                // message
269
                $message = (string) $xml->error;
270
                $code = isset($xml->code) ? (int) $xml->code : null;
271
272
                // throw exception
273
                throw new BpostInvalidSelectionException($message, $code);
274
            }
275
276
            $message = '';
277
            if (
278
                ($contentType !== null && substr_count($contentType, 'text/plain') > 0) ||
279
                in_array($httpCode, array(400, 404))
280
            ) {
281
                $message = $response;
282
            }
283
284
            throw new BpostInvalidResponseException($message, $httpCode);
285
        }
286
287
        // if we don't expect XML we can return the content here
288
        if (!$builder->isExpectXml()) {
289
            return $response;
290
        }
291
292
        // convert into XML
293
        $xml = @simplexml_load_string($response);
294
        if ($xml === false) {
295
            throw new BpostInvalidXmlResponseException();
296
        }
297
298
        // return the response
299
        return $xml;
300
    }
301
302
    /**
303
     * Get the account id
304
     *
305
     * @return string
306
     */
307
    public function getAccountId()
308
    {
309
        return $this->accountId;
310
    }
311
312
    /**
313
     * Generate the secret string for the Authorization header
314
     *
315
     * @return string
316
     */
317
    private function getAuthorizationHeader()
318
    {
319
        return base64_encode($this->accountId . ':' . $this->passPhrase);
320
    }
321
322
    /**
323
     * Get the passPhrase
324
     *
325
     * @return string
326
     */
327
    public function getPassPhrase()
328
    {
329
        return $this->passPhrase;
330
    }
331
332
    /**
333
     * Get the port
334
     *
335
     * @return int
336
     */
337
    public function getPort()
338
    {
339
        return (int) $this->port;
340
    }
341
342
    /**
343
     * Get the timeout that will be used
344
     *
345
     * @return int
346
     */
347
    public function getTimeOut()
348
    {
349
        return (int) $this->timeOut;
350
    }
351
352
    /**
353
     * Get the useragent that will be used.
354
     * Our version will be prepended to yours.
355
     * It will look like: "PHP Bpost/<version> <your-user-agent>"
356
     *
357
     * @return string
358
     */
359
    public function getUserAgent()
360
    {
361
        return (string) 'PHP Bpost/' . self::VERSION . ' ' . $this->userAgent;
362
    }
363
364
    /**
365
     * Set the timeout
366
     * After this time the request will stop. You should handle any errors triggered by this.
367
     *
368
     * @param int $seconds The timeout in seconds.
369
     */
370
    public function setTimeOut($seconds)
371
    {
372
        $this->timeOut = (int) $seconds;
373
    }
374
375
    /**
376
     * Set the user-agent for you application
377
     * It will be appended to ours, the result will look like: "PHP Bpost/<version> <your-user-agent>"
378
     *
379
     * @param string $userAgent Your user-agent, it should look like <app-name>/<app-version>.
380
     */
381
    public function setUserAgent($userAgent)
382
    {
383
        $this->userAgent = (string) $userAgent;
384
    }
385
386
    // webservice methods
387
    // orders
388
    /**
389
     * Creates a new order. If an order with the same orderReference already exists
390
     *
391
     * @param Order $order
392
     *
393
     * @return bool
394
     *
395
     * @throws BpostCurlException
396
     * @throws BpostInvalidResponseException
397
     * @throws BpostInvalidSelectionException
398
     * @throws BpostInvalidXmlResponseException
399
     */
400
    public function createOrReplaceOrder(Order $order)
401
    {
402
        $builder = new CreateOrReplaceOrderBuilder($order, $this->accountId);
403
404
        return $this->doCall($builder) == '';
405
    }
406
407
    /**
408
     * Fetch an order
409
     *
410
     * @param string $reference
411
     *
412
     * @return Order
413
     *
414
     * @throws BpostCurlException
415
     * @throws BpostInvalidResponseException
416
     * @throws BpostInvalidSelectionException
417
     * @throws BpostInvalidXmlResponseException
418
     * @throws BpostNotImplementedException
419
     * @throws BpostXmlNoReferenceFoundException
420
     */
421
    public function fetchOrder($reference)
422
    {
423
        $builder = new FetchOrderBuilder($reference);
424
425
        $xml = $this->doCall($builder);
426
427
        return Order::createFromXML($xml);
0 ignored issues
show
Bug introduced by
It seems like $xml can also be of type string; however, parameter $xml of Bpost\BpostApiClient\Bpost\Order::createFromXML() does only seem to accept SimpleXMLElement, 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

427
        return Order::createFromXML(/** @scrutinizer ignore-type */ $xml);
Loading history...
428
    }
429
430
    /**
431
     * Get the products configuration
432
     *
433
     * @return ProductConfiguration
434
     *
435
     * @throws BpostCurlException
436
     * @throws BpostInvalidResponseException
437
     * @throws BpostInvalidSelectionException
438
     * @throws BpostInvalidXmlResponseException
439
     */
440
    public function fetchProductConfig()
441
    {
442
        $builder = new FetchProductConfigBuilder();
443
444
        $xml = $this->doCall($builder);
445
446
        return ProductConfiguration::createFromXML($xml);
0 ignored issues
show
Bug introduced by
It seems like $xml can also be of type string; however, parameter $xml of Bpost\BpostApiClient\Bpo...ration::createFromXML() does only seem to accept SimpleXMLElement, 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

446
        return ProductConfiguration::createFromXML(/** @scrutinizer ignore-type */ $xml);
Loading history...
447
    }
448
449
    /**
450
     * Modify the status for an order.
451
     *
452
     * @param string $reference The reference for an order
453
     * @param string $status    The new status, allowed values are: OPEN, PENDING, CANCELLED, COMPLETED, ON-HOLD or PRINTED
454
     *
455
     * @return bool
456
     *
457
     * @throws BpostCurlException
458
     * @throws BpostInvalidResponseException
459
     * @throws BpostInvalidSelectionException
460
     * @throws BpostInvalidValueException
461
     * @throws BpostInvalidXmlResponseException
462
     */
463
    public function modifyOrderStatus($reference, $status)
464
    {
465
        $builder = new ModifyOrderBuilder($reference, $status);
466
467
        return $this->doCall($builder) == '';
468
    }
469
470
    // labels
471
472
    /**
473
     * Get the possible label formats
474
     *
475
     * @return array
476
     */
477
    public static function getPossibleLabelFormatValues()
478
    {
479
        return array(
480
            self::LABEL_FORMAT_A4,
481
            self::LABEL_FORMAT_A6,
482
        );
483
    }
484
485
    /**
486
     * Create the labels for all unprinted boxes in an order.
487
     * The service will return labels for all unprinted boxes for that order.
488
     * Boxes that were unprinted will get the status PRINTED, the boxes that
489
     * had already been printed will remain the same.
490
     *
491
     * @param string $reference        The reference for an order
492
     * @param string $format           The desired format, allowed values are: A4, A6
493
     * @param bool   $withReturnLabels Should return labels be returned?
494
     * @param bool   $asPdf            Should we retrieve the PDF-version instead of PNG
495
     *
496
     * @return Bpost\Label[]
497
     *
498
     * @throws BpostCurlException
499
     * @throws BpostInvalidResponseException
500
     * @throws BpostInvalidSelectionException
501
     * @throws BpostInvalidXmlResponseException
502
     */
503
    public function createLabelForOrder(
504
        $reference,
505
        $format = self::LABEL_FORMAT_A6,
506
        $withReturnLabels = false,
507
        $asPdf = false
508
    ) {
509
        $builder = new CreateLabelForOrderBuilder($reference, new LabelFormat($format), $asPdf, $withReturnLabels);
510
511
        $xml = $this->doCall($builder);
512
513
        return Labels::createFromXML($xml);
0 ignored issues
show
Bug introduced by
It seems like $xml can also be of type string; however, parameter $xml of Bpost\BpostApiClient\Bpost\Labels::createFromXML() does only seem to accept SimpleXMLElement, 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

513
        return Labels::createFromXML(/** @scrutinizer ignore-type */ $xml);
Loading history...
514
    }
515
516
    /**
517
     * Create a label for a known barcode.
518
     *
519
     * @param string $barcode          The barcode of the parcel
520
     * @param string $format           The desired format, allowed values are: A4, A6
521
     * @param bool   $withReturnLabels Should return labels be returned?
522
     * @param bool   $asPdf            Should we retrieve the PDF-version instead of PNG
523
     *
524
     * @return Bpost\Label[]
525
     *
526
     * @throws BpostCurlException
527
     * @throws BpostInvalidResponseException
528
     * @throws BpostInvalidSelectionException
529
     * @throws BpostInvalidXmlResponseException
530
     */
531
    public function createLabelForBox(
532
        $barcode,
533
        $format = self::LABEL_FORMAT_A6,
534
        $withReturnLabels = false,
535
        $asPdf = false
536
    ) {
537
        $builder = new CreateLabelForBoxBuilder($barcode, new LabelFormat($format), $asPdf, $withReturnLabels);
538
539
        $xml = $this->doCall($builder);
540
541
        return Labels::createFromXML($xml);
0 ignored issues
show
Bug introduced by
It seems like $xml can also be of type string; however, parameter $xml of Bpost\BpostApiClient\Bpost\Labels::createFromXML() does only seem to accept SimpleXMLElement, 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

541
        return Labels::createFromXML(/** @scrutinizer ignore-type */ $xml);
Loading history...
542
    }
543
544
    /**
545
     * Create labels in bulk, according to the list of order references and the
546
     * list of barcodes. When there is an order reference specified in the
547
     * request, the service will return a label of every box of that order. If
548
     * a certain box was not yet printed, it will have the status PRINTED
549
     *
550
     * @param array  $references       The references for the order
551
     * @param string $format           The desired format, allowed values are: A4, A6
552
     * @param bool   $withReturnLabels Should return labels be returned?
553
     * @param bool   $asPdf            Should we retrieve the PDF-version instead of PNG
554
     * @param bool   $forcePrinting    Reprint a already printed label
555
     *
556
     * @return Bpost\Label[]
557
     *
558
     * @throws BpostCurlException
559
     * @throws BpostInvalidResponseException
560
     * @throws BpostInvalidSelectionException
561
     * @throws BpostInvalidXmlResponseException
562
     */
563
    public function createLabelInBulkForOrders(
564
        array $references,
565
        $format = LabelFormat::FORMAT_A6,
566
        $withReturnLabels = false,
567
        $asPdf = false,
568
        $forcePrinting = false
569
    ) {
570
        $builder = new CreateLabelInBulkForOrdersBuilder(
571
            $references,
572
            new LabelFormat($format),
573
            $asPdf,
574
            $withReturnLabels,
575
            $forcePrinting
576
        );
577
578
        $xml = $this->doCall($builder);
579
580
        return Labels::createFromXML($xml);
0 ignored issues
show
Bug introduced by
It seems like $xml can also be of type string; however, parameter $xml of Bpost\BpostApiClient\Bpost\Labels::createFromXML() does only seem to accept SimpleXMLElement, 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

580
        return Labels::createFromXML(/** @scrutinizer ignore-type */ $xml);
Loading history...
581
    }
582
583
    /**
584
     * Set a logger to permit to the plugin to log events
585
     *
586
     * @param LoggerInterface $logger
587
     */
588
    public function setLogger(LoggerInterface $logger)
589
    {
590
        $this->logger->setLogger($logger);
591
    }
592
593
    /**
594
     * @param int $weight in grams
595
     *
596
     * @return bool
597
     */
598
    public function isValidWeight($weight)
599
    {
600
        return self::MIN_WEIGHT <= $weight && $weight <= self::MAX_WEIGHT;
601
    }
602
}
603