Passed
Push — main ( a33de3...4370d9 )
by Artem
02:12
created

AbstractApi   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 209
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 64
dl 0
loc 209
rs 10
c 1
b 0
f 0
wmc 29

11 Methods

Rating   Name   Duplication   Size   Complexity  
A httpDelete() 0 9 2
A httpGet() 0 3 1
A httpPut() 0 9 2
A hydrateResponse() 0 21 4
A prepareJsonBody() 0 3 1
A httpPost() 0 9 2
A getRestfulUri() 0 9 2
B handleErrors() 0 29 11
A addJsonContentType() 0 3 1
A prepareUri() 0 11 2
A __construct() 0 3 1
1
<?php
2
declare(strict_types=1);
3
4
namespace CloudPlayDev\ConfluenceClient\Api;
5
6
7
use CloudPlayDev\ConfluenceClient\ConfluenceClient;
8
use CloudPlayDev\ConfluenceClient\Entity\Hydratable;
9
use CloudPlayDev\ConfluenceClient\Exception\ConfluencePhpClientException;
10
use CloudPlayDev\ConfluenceClient\Exception\HttpClientException;
11
use CloudPlayDev\ConfluenceClient\Exception\HttpServerException;
12
use CloudPlayDev\ConfluenceClient\Exception\HydrationException;
13
use Http\Client\Exception;
14
use JsonException;
15
use Psr\Http\Message\ResponseInterface;
16
use Webmozart\Assert\Assert;
17
use function array_filter;
18
use function array_merge;
19
use function get_class;
20
use function in_array;
21
use function json_encode;
22
use function sprintf;
23
24
abstract class AbstractApi
25
{
26
    /**
27
     * default rest API prefix for confluence
28
     */
29
    private const URI_PREFIX = '/rest/api/';
30
31
    private ConfluenceClient $client;
32
33
    public function __construct(ConfluenceClient $client)
34
    {
35
        $this->client = $client;
36
    }
37
38
    /**
39
     * @param string $uri
40
     * @param mixed[] $params
41
     * @param array<string, string> $headers
42
     * @return ResponseInterface
43
     * @throws Exception
44
     */
45
    protected function httpGet(string $uri, array $params = [], array $headers = []): ResponseInterface
46
    {
47
        return $this->client->getHttpClient()->get(self::prepareUri($uri, $params), $headers);
48
    }
49
50
    /**
51
     * @param string $uri
52
     * @param mixed[] $params
53
     * @param array<string, string> $headers
54
     * @return ResponseInterface
55
     * @throws Exception
56
     * @throws JsonException
57
     */
58
    protected function httpPut(string $uri, array $params = [], array $headers = []): ResponseInterface
59
    {
60
        $body = self::prepareJsonBody($params);
61
62
        if ($body !== '') {
63
            $headers = self::addJsonContentType($headers);
64
        }
65
66
        return $this->client->getHttpClient()->put(self::prepareUri($uri), $headers, $body);
67
    }
68
69
    /**
70
     * @param string $uri
71
     * @param mixed[] $queryParams
72
     * @param mixed[] $bodyData
73
     * @param array<string, string> $headers
74
     * @return ResponseInterface
75
     * @throws Exception
76
     * @throws JsonException
77
     */
78
    protected function httpPost(string $uri, array $queryParams = [], array $bodyData = [], array $headers = []): ResponseInterface
79
    {
80
        $body = self::prepareJsonBody($bodyData);
81
82
        if ($body !== '') {
83
            $headers = self::addJsonContentType($headers);
84
        }
85
86
        return $this->client->getHttpClient()->post(self::prepareUri($uri, $queryParams), $headers, $body);
87
    }
88
89
    /**
90
     * @param string $uri
91
     * @param mixed[] $params
92
     * @param array<string, string> $headers
93
     * @return ResponseInterface
94
     * @throws Exception
95
     * @throws JsonException
96
     */
97
    protected function httpDelete(string $uri, array $params = [], array $headers = []): ResponseInterface
98
    {
99
        $body = self::prepareJsonBody($params);
100
101
        if ($body !== '') {
102
            $headers = self::addJsonContentType($headers);
103
        }
104
105
        return $this->client->getHttpClient()->delete(self::prepareUri($uri), $headers, $body);
106
    }
107
108
    /**
109
     * @param array<string, string> $headers
110
     * @return array<string, string>
111
     */
112
    private static function addJsonContentType(array $headers): array
113
    {
114
        return array_merge(['Content-Type' => 'application/json'], $headers);
115
    }
116
117
    /**
118
     * @param mixed[] $params
119
     * @return string
120
     * @throws JsonException
121
     */
122
    private static function prepareJsonBody(array $params): string
123
    {
124
        return json_encode($params, JSON_THROW_ON_ERROR);
125
    }
126
127
    /**
128
     * @param string $uri
129
     * @param mixed[] $query
130
     * @return string
131
     */
132
    private static function prepareUri(string $uri, array $query = []): string
133
    {
134
        $query = array_filter($query, static function($value): bool {
135
            return null !== $value;
136
        });
137
138
        $httpQueryParameter = http_build_query($query);
139
        if ($httpQueryParameter !== '') {
140
            $uri .= '?';
141
        }
142
        return sprintf('%s%s%s', self::URI_PREFIX, $uri, $httpQueryParameter);
143
    }
144
145
    /**
146
     * @psalm-template ExpectedType of Hydratable
147
     * @psalm-param class-string<ExpectedType> $class
148
     * @psalm-return ExpectedType
149
     *
150
     * @psalm-suppress InvalidReturnType https://psalm.dev/r/46c8264450
151
     * @psalm-suppress InvalidReturnStatement https://psalm.dev/r/46c8264450
152
     *
153
     * @param ResponseInterface $response
154
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
155
     * @return Hydratable
156
     * @throws ConfluencePhpClientException
157
     * @throws HttpClientException
158
     * @throws HttpServerException
159
     * @throws HydrationException
160
     * @throws JsonException
161
     */
162
    protected function hydrateResponse(ResponseInterface $response, string $class): Hydratable
163
    {
164
        $this->handleErrors($response);
165
166
        $contentType = $response->getHeaderLine('Content-Type');
167
168
        if (!str_starts_with($contentType, 'application/json')) {
169
            throw new HydrationException('The ModelHydrator cannot hydrate response with Content-Type: ' . $contentType);
170
        }
171
        if (!is_subclass_of($class, Hydratable::class)) {
172
            throw new HydrationException('This class can not be hydrated: ' . $class);
173
        }
174
175
        $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
176
        Assert::isArray($data);
177
178
        $entity = $class::load($data);
179
        if (!$entity instanceof $class) {
180
            throw new HydrationException('An unexpected class was created: ' . get_class($entity));
181
        }
182
        return $entity;
183
    }
184
185
    /**
186
     * Throw the correct exception for this error.
187
     */
188
    protected function handleErrors(ResponseInterface $response): void
189
    {
190
        $statusCode = $response->getStatusCode();
191
192
        if (in_array($response->getStatusCode(), [200, 201, 202, 204], true)) {
193
            return;
194
        }
195
196
        switch ($statusCode) {
197
            case 400:
198
                throw HttpClientException::badRequest($response);
199
            case 401:
200
                throw HttpClientException::unauthorized($response);
201
            case 402:
202
                throw HttpClientException::requestFailed($response);
203
            case 403:
204
                throw HttpClientException::forbidden($response);
205
            case 404:
206
                throw HttpClientException::notFound($response);
207
            case 409:
208
                throw HttpClientException::conflict($response);
209
            case 413:
210
                throw HttpClientException::payloadTooLarge($response);
211
            case 429:
212
                throw HttpClientException::tooManyRequests($response);
213
            case 500 <= $statusCode:
214
                throw HttpServerException::serverError($response);
215
            default:
216
                throw new ConfluencePhpClientException($response->getBody()->getContents(), $response->getStatusCode());
217
        }
218
    }
219
220
    /**
221
     * @param string|int|null ...$parameter
222
     * @return string
223
     */
224
    protected static function getRestfulUri(...$parameter): string
225
    {
226
        $parameterString = implode('/', array_filter($parameter));
227
228
        if (!empty($parameterString)) {
229
            return '/' . $parameterString;
230
        }
231
232
        return '';
233
    }
234
}
235