Skip to content

Commit 1e01a08

Browse files
add Bpost and PostNL support
1 parent eeacac9 commit 1e01a08

35 files changed

+1646
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"require": {
1111
"php": ">=7.4",
1212
"ext-mbstring": "*",
13+
"ext-simplexml": "*",
1314
"behat/transliterator": "^1.3",
1415
"doctrine/event-manager": "^1.1",
1516
"doctrine/orm": "^2.7",

src/Client/Bpost/Client.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusPickupPointPlugin\Client\Bpost;
6+
7+
use Psr\Http\Client\ClientExceptionInterface;
8+
use Psr\Http\Client\ClientInterface as HttpClientInterface;
9+
use Psr\Http\Message\RequestFactoryInterface;
10+
use Psr\Http\Message\StreamFactoryInterface;
11+
use Safe\Exceptions\JsonException;
12+
use Setono\SyliusPickupPointPlugin\Client\ClientInterface;
13+
use Setono\SyliusPickupPointPlugin\Exception\RequestFailedException;
14+
use Setono\SyliusPickupPointPlugin\Model\Query\ServicePointQueryInterface;
15+
use function Safe\json_decode;
16+
use function Safe\json_encode;
17+
use const PHP_QUERY_RFC3986;
18+
19+
final class Client implements ClientInterface
20+
{
21+
private HttpClientInterface $httpClient;
22+
23+
private RequestFactoryInterface $requestFactory;
24+
25+
private StreamFactoryInterface $streamFactory;
26+
27+
private string $baseUrl;
28+
29+
public function __construct(
30+
HttpClientInterface $httpClient,
31+
RequestFactoryInterface $requestFactory,
32+
StreamFactoryInterface $streamFactory,
33+
string $baseUrl = 'https://pudo.bpost.be'
34+
) {
35+
$this->httpClient = $httpClient;
36+
$this->requestFactory = $requestFactory;
37+
$this->streamFactory = $streamFactory;
38+
$this->baseUrl = $baseUrl;
39+
}
40+
41+
/**
42+
* @throws ClientExceptionInterface
43+
* @throws JsonException
44+
*/
45+
private function get(string $endpoint, array $params = []): array
46+
{
47+
return $this->sendRequest('GET', $endpoint, $params);
48+
}
49+
50+
/**
51+
* @throws ClientExceptionInterface|JsonException
52+
*/
53+
private function sendRequest(string $method, string $endpoint, array $params = [], array $body = []): array
54+
{
55+
$url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
56+
57+
$request = $this->requestFactory->createRequest($method, $url);
58+
59+
if (count($body) > 0) {
60+
$request = $request->withBody($this->streamFactory->createStream(json_encode($body)));
61+
}
62+
63+
$response = $this->httpClient->sendRequest($request);
64+
65+
66+
if (200 !== $response->getStatusCode()) {
67+
throw new RequestFailedException($request, $response, $response->getStatusCode());
68+
}
69+
70+
$xml = simplexml_load_string($response->getBody()->getContents());
71+
72+
$data = [];
73+
$poiList = $xml->PoiList->Poi ?? $xml->PickupPointList->Point ?? $xml->Poi;
74+
75+
if ($poiList !== null) {
76+
foreach ($poiList as $poi) {
77+
$poiData = json_decode(json_encode($poi), true);
78+
$data[] = $poiData['Record'] ?? $poiData;
79+
}
80+
}
81+
82+
return $data;
83+
}
84+
85+
/**
86+
* @throws ClientExceptionInterface|JsonException
87+
*/
88+
public function locate(ServicePointQueryInterface $servicePointQuery): iterable
89+
{
90+
return $this->get($servicePointQuery->getEndPoint(), $servicePointQuery->toArray());
91+
}
92+
}

src/Client/ClientInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusPickupPointPlugin\Client;
6+
7+
use Setono\SyliusPickupPointPlugin\Model\Query\ServicePointQueryInterface;
8+
9+
interface ClientInterface
10+
{
11+
public function locate(ServicePointQueryInterface $servicePointQuery): iterable;
12+
}

src/Client/PostNL/Client.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusPickupPointPlugin\Client\PostNL;
6+
7+
use Psr\Http\Client\ClientExceptionInterface;
8+
use Psr\Http\Client\ClientInterface as HttpClientInterface;
9+
use Psr\Http\Message\RequestFactoryInterface;
10+
use Psr\Http\Message\StreamFactoryInterface;
11+
use Safe\Exceptions\JsonException;
12+
use Setono\SyliusPickupPointPlugin\Client\ClientInterface;
13+
use Setono\SyliusPickupPointPlugin\Exception\RequestFailedException;
14+
use Setono\SyliusPickupPointPlugin\Model\Query\ServicePointQueryInterface;
15+
use function Safe\json_decode;
16+
use function Safe\json_encode;
17+
use const PHP_QUERY_RFC3986;
18+
19+
final class Client implements ClientInterface
20+
{
21+
private HttpClientInterface $httpClient;
22+
23+
private RequestFactoryInterface $requestFactory;
24+
25+
private StreamFactoryInterface $streamFactory;
26+
27+
private string $baseUrl;
28+
29+
private string $apiKey;
30+
31+
public function __construct(
32+
HttpClientInterface $httpClient,
33+
RequestFactoryInterface $requestFactory,
34+
StreamFactoryInterface $streamFactory,
35+
string $apiKey,
36+
string $baseUrl = 'https://api-sandbox.postnl.nl'
37+
) {
38+
$this->httpClient = $httpClient;
39+
$this->requestFactory = $requestFactory;
40+
$this->streamFactory = $streamFactory;
41+
$this->baseUrl = $baseUrl;
42+
$this->apiKey = $apiKey;
43+
}
44+
45+
/**
46+
* @throws ClientExceptionInterface
47+
* @throws JsonException
48+
*/
49+
private function get(string $endpoint, array $params = []): array
50+
{
51+
return $this->sendRequest('GET', $endpoint, $params);
52+
}
53+
54+
/**
55+
* @throws ClientExceptionInterface|JsonException
56+
*/
57+
private function sendRequest(string $method, string $endpoint, array $params = [], array $body = []): array
58+
{
59+
$url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
60+
61+
$request = $this->requestFactory->createRequest($method, $url);
62+
63+
if (count($body) > 0) {
64+
$request = $request->withBody($this->streamFactory->createStream(json_encode($body)));
65+
}
66+
67+
$request = $request->withHeader('apikey', $this->apiKey);
68+
$request = $request->withHeader('accept', 'application/json');
69+
70+
$response = $this->httpClient->sendRequest($request);
71+
72+
if (200 !== $response->getStatusCode()) {
73+
throw new RequestFailedException($request, $response, $response->getStatusCode());
74+
}
75+
76+
$data = json_decode($response->getBody()->getContents(), true);
77+
78+
if (!isset($data['GetLocationsResult']['ResponseLocation']))
79+
{
80+
throw new \UnexpectedValueException(
81+
"Expected field '['GetLocationsResult']['ResponseLocation']' to be set."
82+
);
83+
}
84+
85+
return $data['GetLocationsResult']['ResponseLocation'];
86+
}
87+
88+
/**
89+
* @throws ClientExceptionInterface|JsonException
90+
*/
91+
public function locate(ServicePointQueryInterface $servicePointQuery): iterable
92+
{
93+
return $this->get($servicePointQuery->getEndPoint(), $servicePointQuery->toArray());
94+
}
95+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusPickupPointPlugin\DependencyInjection\Compiler;
6+
7+
use Nyholm\Psr7\Factory\Psr17Factory;
8+
use Psr\Http\Message\RequestFactoryInterface;
9+
use Psr\Http\Message\StreamFactoryInterface;
10+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
11+
use Symfony\Component\DependencyInjection\ContainerBuilder;
12+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
13+
14+
final class RegisterFactoriesPass implements CompilerPassInterface
15+
{
16+
private const REQUEST_FACTORY_SUFFIX = 'request_factory';
17+
private const STREAM_FACTORY_SUFFIX = 'stream_factory';
18+
private const PSR17_FACTORY_SUFFIX = 'psr17_factory';
19+
20+
private const SERVICE_ID_FORMAT = '%s.%s';
21+
22+
private const SETONO_BPOST_PROVIDER = 'setono_bpost';
23+
private const SETONO_POSTNL_PROVIDER = 'setono_postnl';
24+
25+
private const PROVIDERS = [
26+
self::SETONO_BPOST_PROVIDER,
27+
self::SETONO_POSTNL_PROVIDER,
28+
];
29+
30+
public function process(ContainerBuilder $container): void
31+
{
32+
foreach (self::PROVIDERS as $PROVIDER) {
33+
if (class_exists(Psr17Factory::class)) {
34+
// this service is used later if the Psr17Factory exists. Else it will be automatically removed by Symfony
35+
$container->register(sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::PSR17_FACTORY_SUFFIX), Psr17Factory::class);
36+
}
37+
38+
$factoryId = sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::PSR17_FACTORY_SUFFIX);
39+
$requestFactoryAlias = sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::REQUEST_FACTORY_SUFFIX);
40+
$this->registerFactory(
41+
$container,
42+
$requestFactoryAlias,
43+
$requestFactoryAlias,
44+
$factoryId,
45+
RequestFactoryInterface::class
46+
);
47+
48+
$streamFactoryAlias = sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::STREAM_FACTORY_SUFFIX);
49+
$this->registerFactory($container,
50+
$streamFactoryAlias,
51+
$streamFactoryAlias,
52+
$factoryId,
53+
StreamFactoryInterface::class
54+
);
55+
}
56+
}
57+
58+
private function registerFactory(ContainerBuilder $container, string $parameter, string $service, string $factoryId, string $factoryInterface): void
59+
{
60+
if ($container->hasParameter($parameter)) {
61+
if (!$container->has($container->getParameter($parameter))) {
62+
throw new ServiceNotFoundException($container->getParameter($parameter));
63+
}
64+
65+
$container->setAlias($service, $container->getParameter($parameter));
66+
} elseif ($container->has($factoryInterface)) {
67+
$container->setAlias($service, $factoryInterface);
68+
} elseif ($container->has('nyholm.psr7.psr17_factory')) {
69+
$container->setAlias($service, 'nyholm.psr7.psr17_factory');
70+
} elseif (class_exists(Psr17Factory::class)) {
71+
$container->setAlias($service, $factoryId);
72+
}
73+
}
74+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusPickupPointPlugin\DependencyInjection\Compiler;
6+
7+
use Buzz\Client\BuzzClientInterface;
8+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
11+
12+
final class RegisterHttpClientPass implements CompilerPassInterface
13+
{
14+
private const HTTP_CLIENT_PARAMETER_SERVICE_IDS = [
15+
'setono_bpost.http_client' => 'setono_bpost.http_client',
16+
'setono_postnl.http_client' => 'setono_postnl.http_client',
17+
];
18+
19+
public function process(ContainerBuilder $container): void
20+
{
21+
foreach (self::HTTP_CLIENT_PARAMETER_SERVICE_IDS as $PARAMETER => $SERVICE_ID) {
22+
if ($container->hasParameter($PARAMETER)) {
23+
if (!$container->has($container->getParameter($PARAMETER))) {
24+
throw new ServiceNotFoundException($container->getParameter($PARAMETER));
25+
}
26+
27+
$container->setAlias($SERVICE_ID, $container->getParameter($SERVICE_ID));
28+
} elseif ($container->has(BuzzClientInterface::class)) {
29+
$container->setAlias($SERVICE_ID, BuzzClientInterface::class);
30+
}
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)