DigiSignClient   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 48
eloc 132
c 1
b 0
f 0
dl 0
loc 288
rs 8.5599

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A createUri() 0 35 5
A jsonDecode() 0 9 3
F createRequest() 0 92 20
A jsonEncode() 0 9 3
A checkResponse() 0 18 6
A normalizeJson() 0 19 4
A parseResponse() 0 22 5
A request() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like DigiSignClient 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 DigiSignClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace DigitalCz\DigiSign;
6
7
use DateTimeInterface;
8
use DigitalCz\DigiSign\Exception\BadRequestException;
9
use DigitalCz\DigiSign\Exception\ClientException;
10
use DigitalCz\DigiSign\Exception\NotFoundException;
11
use DigitalCz\DigiSign\Exception\RuntimeException;
12
use DigitalCz\DigiSign\Exception\ServerException;
13
use DigitalCz\DigiSign\Exception\UnauthorizedException;
14
use DigitalCz\DigiSign\Resource\BaseResource;
15
use DigitalCz\DigiSign\Resource\ResourceInterface;
16
use DigitalCz\DigiSign\Stream\FileStream;
17
use Http\Discovery\Psr17FactoryDiscovery;
18
use Http\Discovery\Psr18ClientDiscovery;
19
use Http\Message\MultipartStream\MultipartStreamBuilder;
20
use InvalidArgumentException;
21
use JsonException;
22
use Psr\Http\Client\ClientInterface;
23
use Psr\Http\Message\RequestFactoryInterface;
24
use Psr\Http\Message\RequestInterface;
25
use Psr\Http\Message\ResponseInterface;
26
use Psr\Http\Message\StreamFactoryInterface;
27
use Psr\Http\Message\StreamInterface;
28
use Psr\Http\Message\UriFactoryInterface;
29
use Psr\Http\Message\UriInterface;
30
31
final class DigiSignClient implements DigiSignClientInterface
32
{
33
    public const HTTP_NO_CONTENT = 204;
34
    public const HTTP_BAD_REQUEST = 400;
35
    public const HTTP_UNAUTHORIZED = 401;
36
    public const HTTP_NOT_FOUND = 404;
37
    public const HTTP_INTERNAL_SERVER_ERROR = 500;
38
39
    /** @var ClientInterface  */
40
    private $httpClient;
41
42
    /** @var RequestFactoryInterface  */
43
    private $requestFactory;
44
45
    /** @var StreamFactoryInterface  */
46
    private $streamFactory;
47
48
    /** @var UriFactoryInterface  */
49
    private $uriFactory;
50
51
    public function __construct(
52
        ?ClientInterface $httpClient = null,
53
        ?RequestFactoryInterface $requestFactory = null,
54
        ?StreamFactoryInterface $streamFactory = null,
55
        ?UriFactoryInterface $uriFactory = null
56
    ) {
57
        $this->httpClient = $httpClient ?? Psr18ClientDiscovery::find();
58
        $this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory();
59
        $this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
60
        $this->uriFactory = $uriFactory ?? Psr17FactoryDiscovery::findUriFactory();
61
    }
62
63
    /**
64
     * @return mixed[]|null
65
     */
66
    public static function parseResponse(ResponseInterface $response): ?array
67
    {
68
        $body = (string)$response->getBody();
69
70
        if ($body === '') {
71
            if ($response->getStatusCode() === self::HTTP_NO_CONTENT) {
72
                return null;
73
            }
74
75
            throw new RuntimeException('Empty response body');
76
        }
77
78
        try {
79
            $decoded = json_decode($body, true);
80
81
            if (!is_array($decoded)) {
82
                throw new JsonException();
83
            }
84
85
            return $decoded;
86
        } catch (JsonException $e) {
87
            throw new RuntimeException('Unable to parse response', 0, $e);
88
        }
89
    }
90
91
    /**
92
     * @param mixed[] $options
93
     */
94
    public function request(string $method, string $uri, array $options = []): ResponseInterface
95
    {
96
        $psrUri = $this->createUri($uri, $options);
97
        $request = $this->createRequest($method, $psrUri, $options);
98
        $response = $this->httpClient->sendRequest($request);
99
100
        $this->checkResponse($response);
101
102
        return $response;
103
    }
104
105
    /**
106
     * @param mixed $value
107
     *
108
     * @throws JsonException When the value cannot be json-encoded
109
     */
110
    public static function jsonEncode($value): string
111
    {
112
        $json = json_encode($value);
113
114
        if ($json === false || json_last_error() !== JSON_ERROR_NONE) {
115
            throw new JsonException(json_last_error_msg(), json_last_error());
116
        }
117
118
        return $json;
119
    }
120
121
    /**
122
     * @return mixed[]
123
     *
124
     * @throws JsonException When the json-string is invalid
125
     */
126
    public static function jsonDecode(string $json): array
127
    {
128
        $value  = json_decode($json, true);
129
130
        if (!is_array($value) || json_last_error() !== JSON_ERROR_NONE) {
131
            throw new JsonException(json_last_error_msg(), json_last_error());
132
        }
133
134
        return $value;
135
    }
136
137
    /**
138
     * @param mixed[] $options
139
     */
140
    private function createUri(string $uri, array $options): UriInterface
141
    {
142
        // replace uri parameters with its values
143
        preg_match_all('/{(\w+)}/', $uri, $matches);
144
        $searches = $matches[1] ?? [];
145
        $replaces = [];
146
147
        foreach ($searches as $search) {
148
            if (!isset($options[$search])) {
149
                throw new RuntimeException(sprintf('Cannot resolve uri parameter %s', $search));
150
            }
151
152
            $param = $options[$search];
153
154
            if ($param instanceof BaseResource) {
155
                $param = $param->id() ?? '';
156
            }
157
158
            $replaces[] = (string)$param;
159
        }
160
161
        $searches = array_map(static function (string $search): string {
162
            return sprintf("{%s}", $search);
163
        }, $searches);
164
165
        $uri = str_replace($searches, $replaces, $uri);
166
167
        // create PSR Uri
168
        $psrUri = $this->uriFactory->createUri($uri);
169
170
        if (isset($options['query'])) {
171
            $psrUri = $psrUri->withQuery(http_build_query($options['query']));
172
        }
173
174
        return $psrUri;
175
    }
176
177
    /**
178
     * @param mixed[] $options
179
     */
180
    private function createRequest(string $method, UriInterface $uri, array $options): RequestInterface // phpcs:ignore
181
    {
182
        $request = $this->requestFactory->createRequest($method, $uri);
183
        $headers = $options['headers'] ?? [];
184
185
        if (!is_array($headers)) {
186
            throw new InvalidArgumentException('Invalid value for "headers" option');
187
        }
188
189
        // default headers
190
        $headers['Accept'] = $headers['Accept'] ?? 'application/json';
191
192
        if (isset($options['user-agent'])) {
193
            $headers['User-Agent'] = (string)$options['user-agent'];
194
        }
195
196
        if (isset($options['auth_basic'])) {
197
            if (is_array($options['auth_basic'])) {
198
                $options['auth_basic'] = implode(':', $options['auth_basic']);
199
            }
200
201
            if (!is_string($options['auth_basic'])) {
202
                throw new InvalidArgumentException('Invalid value for "auth_basic" option');
203
            }
204
205
            $headers['Authorization'] = 'Basic ' . base64_encode($options['auth_basic']);
206
        }
207
208
        if (isset($options['auth_bearer'])) {
209
            if (!is_string($options['auth_bearer'])) {
210
                throw new InvalidArgumentException('Invalid value for "auth_bearer" option');
211
            }
212
213
            $headers['Authorization'] = 'Bearer ' . $options['auth_bearer'];
214
        }
215
216
        if (isset($options['multipart'])) {
217
            if (!is_array($options['multipart'])) {
218
                throw new InvalidArgumentException('Invalid value for "multipart" option');
219
            }
220
221
            $multipartBuilder = new MultipartStreamBuilder($this->streamFactory);
222
            foreach ($options['multipart'] as $name => $resource) {
223
                $resourceOptions = [];
224
225
                if ($resource instanceof FileStream) {
226
                    $resourceOptions['filename'] = $resource->getFilename();
227
                    $resource = $resource->getHandle();
228
                }
229
230
                $multipartBuilder->addResource($name, $resource, $resourceOptions);
231
                $headers['Content-Type'] = sprintf(
232
                    'multipart/form-data; boundary="%s"',
233
                    $multipartBuilder->getBoundary()
234
                );
235
                $options['body'] = $multipartBuilder->build();
236
            }
237
        }
238
239
        if (isset($options['json'])) {
240
            if (!is_array($options['json'])) {
241
                throw new InvalidArgumentException('Invalid value for "json" option');
242
            }
243
244
            $headers['Content-Type'] = 'application/json';
245
            $json = self::normalizeJson($options['json']);
246
            try {
247
                $options['body'] = self::jsonEncode($json);
248
            } catch (JsonException $e) {
249
                throw new InvalidArgumentException('Invalid value for "json" option: ' . $e->getMessage());
250
            }
251
        }
252
253
        if (isset($options['body'])) {
254
            if (is_resource($options['body'])) {
255
                $body = $this->streamFactory->createStreamFromResource($options['body']);
256
            } elseif (is_string($options['body'])) {
257
                $body = $this->streamFactory->createStream($options['body']);
258
            } elseif ($options['body'] instanceof StreamInterface) {
259
                $body = $options['body'];
260
            } else {
261
                throw new InvalidArgumentException('Invalid value for "body" option');
262
            }
263
264
            $request = $request->withBody($body);
265
        }
266
267
        foreach ($headers as $name => $value) {
268
            $request = $request->withHeader($name, $value);
269
        }
270
271
        return $request;
272
    }
273
274
    private function checkResponse(ResponseInterface $response): void
275
    {
276
        $code = $response->getStatusCode();
277
278
        if ($code >= self::HTTP_INTERNAL_SERVER_ERROR) {
279
            throw new ServerException($response);
280
        }
281
282
        if ($code >= self::HTTP_BAD_REQUEST) {
283
            switch ($code) {
284
                case self::HTTP_BAD_REQUEST:
285
                    throw new BadRequestException($response);
286
                case self::HTTP_UNAUTHORIZED:
287
                    throw new UnauthorizedException($response);
288
                case self::HTTP_NOT_FOUND:
289
                    throw new NotFoundException($response);
290
                default:
291
                    throw new ClientException($response);
292
            }
293
        }
294
    }
295
296
    /**
297
     * @param mixed[] $json
298
     * @return mixed[]
299
     */
300
    protected static function normalizeJson(array $json): array
301
    {
302
        $normalize = static function ($value) {
303
            if (is_array($value)) {
304
                return self::normalizeJson($value);
305
            }
306
307
            if ($value instanceof DateTimeInterface) {
308
                return $value->format(DateTimeInterface::ATOM);
309
            }
310
311
            if ($value instanceof ResourceInterface) {
312
                return $value->self();
313
            }
314
315
            return $value;
316
        };
317
318
        return array_map($normalize, $json);
319
    }
320
}
321