Issues (112)

src/Geo6.php (1 issue)

1
<?php
2
declare(strict_types=1);
3
4
namespace Bpost\BpostApiClient;
5
6
use Bpost\BpostApiClient\ApiCaller\ApiCaller;
7
use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostCurlException;
8
use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostInvalidXmlResponseException;
9
use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostTaxipostLocatorException;
10
use Bpost\BpostApiClient\Geo6\Poi;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\NullLogger;
13
use SimpleXMLElement;
14
15
/**
16
 * Geo6 class
17
 *
18
 * @author    Tijs Verkoyen <[email protected]>
19
 *
20
 * @version   3.0.0
21
 *
22
 * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
23
 * @license   BSD License
24
 */
25
class Geo6
26
{
27
    public const API_URL = 'https://pudo.bpost.be/Locator';
28
    public const VERSION = '3';
29
30
    /**
31
     * Types de points (combinaisons via addition binaire)
32
     * @see getPointType()
33
     * @see getServicePointPageUrl()
34
     */
35
    public const POINT_TYPE_POST_OFFICE          = 1;
36
    public const POINT_TYPE_POST_POINT           = 2;
37
    public const POINT_TYPE_BPACK_247            = 4;
38
    public const POINT_TYPE_CLICK_COLLECT_SHOP   = 8;
39
40
    private ?ApiCaller $apiCaller = null;
41
42
    private string $appId;
43
    private string $partner;
44
45
    /** Timeout en secondes */
46
    private int $timeOut = 10;
47
48
    /** Suffixe d’UA applicatif */
49
    private string $userAgent = '';
50
51
    private LoggerInterface $logger;
52
53
    /**
54
     * @param string $partner Paramètre statique de protection/statistiques
55
     * @param string $appId   Paramètre statique de protection/statistiques
56
     * @param LoggerInterface|null $psrLogger Logger PSR optionnel (NullLogger par défaut)
57
     */
58
    public function __construct(string $partner, string $appId, ?LoggerInterface $logger = null)
59
    {
60
        $this->setPartner($partner);
61
        $this->setAppId($appId);
62
        $this->logger = $logger ?? new NullLogger();
63
    }
64
65
    public function getApiCaller(): ApiCaller
66
    {
67
        if ($this->apiCaller === null) {
68
            $this->apiCaller = new ApiCaller($this->logger);
69
        }
70
        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...
71
    }
72
73
    public function setApiCaller(ApiCaller $apiCaller): void
74
    {
75
        $this->apiCaller = $apiCaller;
76
    }
77
78
    /**
79
     * Construit l’URL de page publique (GET)
80
     */
81
    private function buildUrl(string $method, array $parameters = []): string
82
    {
83
        return self::API_URL . '?' . $this->buildParameters($method, $parameters);
84
    }
85
86
    /**
87
     * Construit le payload (url-encoded) pour POST
88
     */
89
    private function buildParameters(string $method, array $parameters = []): string
90
    {
91
        // Ajout des credentials + format
92
        $parameters['Function'] = $method;
93
        $parameters['Partner']  = $this->getPartner();
94
        $parameters['AppId']    = $this->getAppId();
95
        $parameters['Format']   = 'xml';
96
97
        return http_build_query($parameters);
98
    }
99
100
    /**
101
     * Appel réel HTTP (POST) et parsing XML
102
     *
103
     * @throws BpostCurlException
104
     * @throws BpostInvalidXmlResponseException
105
     * @throws BpostTaxipostLocatorException
106
     */
107
    private function doCall(string $method, array $parameters = []): SimpleXMLElement
108
    {
109
        $options = [
110
            CURLOPT_URL            => self::API_URL,
111
            CURLOPT_USERAGENT      => $this->getUserAgent(),
112
            CURLOPT_FOLLOWLOCATION => true,
113
            // Le service n’exige pas de client cert — on garde le comportement historique :
114
            CURLOPT_SSL_VERIFYPEER => false,
115
            CURLOPT_SSL_VERIFYHOST => false,
116
            CURLOPT_RETURNTRANSFER => true,
117
            CURLOPT_TIMEOUT        => $this->getTimeOut(),
118
            CURLOPT_POST           => true,
119
            CURLOPT_POSTFIELDS     => $this->buildParameters($method, $parameters),
120
        ];
121
122
        $this->getApiCaller()->doCall($options);
123
124
        $body = $this->getApiCaller()->getResponseBody();
125
        $xml  = @simplexml_load_string($body);
126
127
        // XML invalide ou structure d’erreur générique
128
        if ($xml === false || (isset($xml->head) && isset($xml->body))) {
129
            throw new BpostInvalidXmlResponseException();
130
        }
131
132
        // Erreur Taxipost
133
        if (isset($xml['type']) && (string) $xml['type'] === 'TaxipostLocatorError') {
134
            throw new BpostTaxipostLocatorException((string) $xml->txt, (int) $xml->status);
135
        }
136
137
        return $xml;
138
    }
139
140
    public function setAppId(string $appId): void
141
    {
142
        $this->appId = $appId;
143
    }
144
145
    public function getAppId(): string
146
    {
147
        return $this->appId;
148
    }
149
150
    public function setPartner(string $partner): void
151
    {
152
        $this->partner = $partner;
153
    }
154
155
    public function getPartner(): string
156
    {
157
        return $this->partner;
158
    }
159
160
    /** Timeout en secondes */
161
    public function setTimeOut(int $seconds): void
162
    {
163
        $this->timeOut = $seconds;
164
    }
165
166
    public function getTimeOut(): int
167
    {
168
        return $this->timeOut;
169
    }
170
171
    /**
172
     * User-Agent complet (lib + suffixe app)
173
     */
174
    public function getUserAgent(): string
175
    {
176
        return 'PHP Bpost Geo6/' . self::VERSION . ' ' . $this->userAgent;
177
    }
178
179
    /**
180
     * Suffixe d’UA (ex: "my-app/1.2.3")
181
     */
182
    public function setUserAgent(string $userAgent): void
183
    {
184
        $this->userAgent = $userAgent;
185
    }
186
187
    // ======================
188
    // Webservice methods
189
    // ======================
190
191
    /**
192
     * The GetNearestServicePoints web service delivers the nearest bpost pick-up points
193
     *
194
     * @param string $street
195
     * @param string $number
196
     * @param string $zone
197
     * @param string $language nl|fr
198
     * @param int    $type     1=Office, 2=Point, 3=Office+Point, 4=24/7, 7=Office+Point+24/7
199
     * @param int    $limit
200
     * @param string $country  e.g. "BE", "FR"
201
     *
202
     * @return array<int, array{poi:Poi, distance:float}>
203
     *
204
     * @throws BpostCurlException
205
     * @throws BpostInvalidXmlResponseException
206
     * @throws BpostTaxipostLocatorException
207
     */
208
    public function getNearestServicePoint(
209
        string $street,
210
        string $number,
211
        string $zone,
212
        string $language = 'nl',
213
        int $type = 3,
214
        int $limit = 10,
215
        string $country = 'BE'
216
    ): array {
217
        $parameters = [
218
            'Street'   => $street,
219
            'Number'   => $number,
220
            'Zone'     => $zone,
221
            'Country'  => $country,
222
            'Language' => $language,
223
            'Type'     => $type,
224
            'Limit'    => $limit,
225
        ];
226
227
        $xml = $this->doCall('search', $parameters);
228
229
        if (!isset($xml->PoiList->Poi)) {
230
            throw new BpostInvalidXmlResponseException();
231
        }
232
233
        $pois = [];
234
        foreach ($xml->PoiList->Poi as $poi) {
235
            $pois[] = [
236
                'poi'       => Poi::createFromXML($poi),
237
                'distance'  => isset($poi->Distance) ? (float) $poi->Distance : 0.0,
238
            ];
239
        }
240
241
        return $pois;
242
    }
243
244
    /**
245
     * The GetServicePointDetails web service delivers the details for a pick-up point.
246
     *
247
     * @throws BpostCurlException
248
     * @throws BpostInvalidXmlResponseException
249
     * @throws BpostTaxipostLocatorException
250
     */
251
    public function getServicePointDetails(
252
        string $id,
253
        string $language = 'nl',
254
        int $type = 3,
255
        string $country = 'BE'
256
    ): Poi {
257
        $parameters = [
258
            'Id'       => $id,
259
            'Language' => $language,
260
            'Type'     => $type,
261
            'Country'  => $country,
262
        ];
263
264
        $xml = $this->doCall('info', $parameters);
265
266
        if (!isset($xml->Poi)) {
267
            throw new BpostInvalidXmlResponseException();
268
        }
269
270
        return Poi::createFromXML($xml->Poi);
271
    }
272
273
    /**
274
     * URL publique de la page bpost d’un point.
275
     *
276
     * @see getPointType() pour calculer $type
277
     */
278
    public function getServicePointPageUrl(
279
        string $id,
280
        string $language = 'nl',
281
        int $type = 3,
282
        string $country = 'BE'
283
    ): string {
284
        $parameters = [
285
            'Id'       => $id,
286
            'Language' => $language,
287
            'Type'     => $type,
288
            'Country'  => $country,
289
        ];
290
291
        return $this->buildUrl('page', $parameters);
292
    }
293
294
    /** @deprecated Renommé en getServicePointPageUrl() */
295
    public function getServicePointPage(
296
        string $id,
297
        string $language = 'nl',
298
        int $type = 3,
299
        string $country = 'BE'
300
    ): string {
301
        return $this->getServicePointPageUrl($id, $language, $type, $country);
302
    }
303
304
    /**
305
     * Calcule le « type » combiné (bitmask) pour filtrer les points
306
     */
307
    public function getPointType(
308
        bool $withPostOffice = true,
309
        bool $withPostPoint = true,
310
        bool $withBpack247 = false,
311
        bool $withClickAndCollectShop = false
312
    ): int {
313
        return
314
            ($withPostOffice ? self::POINT_TYPE_POST_OFFICE : 0)
315
            + ($withPostPoint ? self::POINT_TYPE_POST_POINT : 0)
316
            + ($withBpack247 ? self::POINT_TYPE_BPACK_247 : 0)
317
            + ($withClickAndCollectShop ? self::POINT_TYPE_CLICK_COLLECT_SHOP : 0);
318
    }
319
}
320