Passed
Push — master ( f7fa6f...41a79e )
by Charis
02:22
created

Publisher::replaceEndPointId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace One;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\Exception\ClientException;
7
use GuzzleHttp\Psr7\MultipartStream;
8
use One\Model\Article;
9
use One\Model\Model;
10
use Psr\Http\Message\RequestInterface;
11
use Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerInterface;
13
14
/**
15
 * Publisher class
16
 * main class to be used that interfacing to the API
17
 */
18
class Publisher implements LoggerAwareInterface
19
{
20
    public const DEFAULT_MAX_ATTEMPT = 4;
21
22
    public const REST_SERVER = 'https://dev.one.co.id';
23
24
    public const AUTHENTICATION = '/oauth/token';
25
26
    public const ARTICLE_CHECK_ENDPOINT = '/api/article';
27
28
    public const ARTICLE_ENDPOINT = '/api/publisher/article';
29
30
    /**
31
     * attachment url destination
32
     * @var array<string>
33
     */
34
    private $attachmentUrl;
35
36
    /**
37
     * Logger variable, if set log activity to this obejct each time sending request and receiving response
38
     *
39
     * @var \Psr\Log\LoggerInterface
40
     */
41
    private $logger = null;
42
43
    /**
44
     * credentials props
45
     *
46
     * @var string
47
     */
48
    private $clientId;
49
50
    /**
51
     * client secret
52
     * @var string
53
     */
54
    private $clientSecret;
55
56
    /**
57
     * Oauth access token response
58
     *
59
     * @var string
60
     */
61
    private $accessToken = null;
62
63
    /**
64
     * publisher custom options
65
     *
66
     * @var \One\Collection
67
     */
68
    private $options;
69
70
    /**
71
     * http transaction Client
72
     *
73
     * @var \GuzzleHttp\Client;
74
     */
75
    private $httpClient;
76
77
    /**
78
     * constructor
79
     */
80
    public function __construct(string $clientId, string $clientSecret, array $options = [])
81
    {
82
        $this->clientId = $clientId;
83
        $this->clientSecret = $clientSecret;
84
85
        $this->assessOptions($options);
86
87
        $this->attachmentUrl = [
88
            Article::ATTACHMENT_FIELD_GALLERY => self::ARTICLE_ENDPOINT . '/{article_id}/gallery',
89
            Article::ATTACHMENT_FIELD_PAGE => self::ARTICLE_ENDPOINT . '/{article_id}/page',
90
            Article::ATTACHMENT_FIELD_PHOTO => self::ARTICLE_ENDPOINT . '/{article_id}/photo',
91
            Article::ATTACHMENT_FIELD_VIDEO => self::ARTICLE_ENDPOINT . '/{article_id}/video',
92
93
        ];
94
    }
95
96
    /**
97
     * recycleToken from callback. If use external token storage could leveraged on this
98
     */
99
    public function recycleToken(\Closure $tokenProducer): self
100
    {
101
        return $this->setAuthorizationHeader($tokenProducer());
102
    }
103
104
    /**
105
     * submitting article here, return new Object cloned from original
106
     */
107
    public function submitArticle(Article $article): \One\Model\Article
108
    {
109
        $responseArticle = $this->post(
110
            self::ARTICLE_ENDPOINT,
111
            $this->normalizePayload(
112
                $article->getCollection()
113
            )
114
        );
115
116
        $responseArticle = json_decode($responseArticle, true);
117
        $article->setId((string) $responseArticle['data']['id']);
118
119
        foreach ($article->getPossibleAttachment() as $field) {
120
            if ($article->hasAttachment($field)) {
121
                foreach ($article->getAttachmentByField($field) as $attachment) {
122
                    $this->submitAttachment(
123
                        $article->getId(),
124
                        $attachment,
125
                        $field
126
                    );
127
                }
128
            }
129
        }
130
131
        return $article;
132
    }
133
134
    /**
135
     * submit each attachment of an article here
136
     */
137
    public function submitAttachment(string $idArticle, Model $attachment, string $field): array
138
    {
139
        return json_decode(
140
            $this->post(
141
                $this->getAttachmentEndPoint($idArticle, $field),
142
                $this->normalizePayload(
143
                    $attachment->getCollection()
144
                )
145
            ),
146
            true
147
        );
148
    }
149
150
    /**
151
     * get article from rest API
152
     *
153
     * @return string json
154
     */
155
    public function getArticle(string $idArticle): string
156
    {
157
        return $this->get(
158
            self::ARTICLE_CHECK_ENDPOINT . "/${idArticle}"
159
        );
160
    }
161
162
    /**
163
     * get list article by publisher
164
     *
165
     * @return string json
166
     */
167
    public function listArticle(): string
168
    {
169
        return $this->get(
170
            self::ARTICLE_ENDPOINT
171
        );
172
    }
173
174
    /**
175
     * delete article based on id
176
     */
177
    public function deleteArticle(string $idArticle): ?string
178
    {
179
        $articleOnRest = $this->getArticle($idArticle);
180
181
        if (! empty($articleOnRest)) {
182
            $articleOnRest = json_decode($articleOnRest, true);
183
184
            if (isset($articleOnRest['data'])) {
185
                foreach (Article::getDeleteableAttachment() as $field) {
186
                    if (isset($articleOnRest['data'][$field])) {
187
                        foreach ($articleOnRest['data'][$field] as $attachment) {
188
                            if (isset($attachment[$field . '_order'])) {
189
                                $this->deleteAttachment($idArticle, $field, $attachment[$field . '_order']);
190
                            }
191
                        }
192
                    }
193
                }
194
            }
195
196
            return $this->delete(
197
                $this->getArticleWithIdEndPoint($idArticle)
198
            );
199
        }
200
    }
201
202
    /**
203
     * delete attachment of an article
204
     */
205
    public function deleteAttachment(string $idArticle, string $field, string $order): string
206
    {
207
        return $this->delete(
208
            $this->getAttachmentEndPoint($idArticle, $field) . "/${order}"
209
        );
210
    }
211
212
    /**
213
     * get proxy
214
     */
215
    final public function get(string $path, array $header = [], array $options = []): string
216
    {
217
        return $this->requestGate(
218
            'GET',
219
            $path,
220
            $header,
221
            [],
222
            $options
223
        );
224
    }
225
226
    /**
227
     * post proxy
228
     *
229
     * @param \One\Collection|array $body
230
     */
231
    final public function post(string $path, array $body = [], array $header = [], array $options = []): string
232
    {
233
        if ($this->hasLogger()) {
234
            $this->logger->info('Post to ' . $path);
235
        }
236
237
        return $this->requestGate(
238
            'POST',
239
            $path,
240
            $header,
241
            $body,
242
            $options
243
        );
244
    }
245
246
    /**
247
     * delete proxy
248
     *
249
     * @param \One\Collection|array $body
250
     * @param array $body
251
     */
252
    final public function delete(string $path, array $body = [], array $header = [], array $options = []): string
253
    {
254
        return $this->requestGate(
255
            'DELETE',
256
            $path,
257
            $header,
258
            $body,
259
            $options
260
        );
261
    }
262
263
    /**
264
     * @inheritDoc
265
     */
266
    public function setLogger(LoggerInterface $logger): void
267
    {
268
        $this->logger = $logger;
269
    }
270
271
    /**
272
     * assessing and custom option
273
     */
274
    private function assessOptions(array $options): void
275
    {
276
        $defaultOptions = [
277
            'rest_server' => self::REST_SERVER,
278
            'auth_url' => self::AUTHENTICATION,
279
            'max_attempt' => self::DEFAULT_MAX_ATTEMPT,
280
            'default_headers' => [
281
                'Accept' => 'application/json',
282
            ],
283
        ];
284
285
        $this->options = new Collection(
286
            array_merge(
287
                $defaultOptions,
288
                $options
289
            )
290
        );
291
292
        if (isset($options['access_token'])) {
293
            $this->setAuthorizationHeader($options['access_token']);
294
        }
295
296
        $this->httpClient = new Client([
297
            'base_uri' => $this->options->get('rest_server'),
298
        ]);
299
    }
300
301
    /**
302
     * one gate menu for request creation.
303
     *
304
     * @param \One\Collection|array $body
305
     */
306
    private function requestGate(string $method, string $path, array $header = [], array $body = [], array $options = []): string
307
    {
308
        if (empty($this->accessToken)) {
309
            $this->renewAuthToken();
310
        }
311
312
        $request = new \GuzzleHttp\Psr7\Request(
313
            $method,
314
            $path,
315
            array_merge(
316
                $this->options->get('default_headers'),
317
                $header
318
            ),
319
            $this->createBodyForRequest(
320
                $this->prepareMultipartData($body)
321
            )
322
        );
323
324
        return (string) $this->httpClient->send($request, $options)->getBody();
325
    }
326
327
    private function prepareMultipartData(array $data = []): array
328
    {
329
        $result = [];
330
        foreach ($data as $key => $value) {
331
            array_push($result, ['name' => $key, 'contents' => $value]);
332
        }
333
        return $result;
334
    }
335
336
    /**
337
     * actually send request created here, separated for easier attempt count and handling exception
338
     *
339
     * @throws \Exception
340
     * @throws \GuzzleHttp\Exception\ClientException
341
     * @throws \GuzzleHttp\Exception\BadResponseException
342
     */
343
    private function sendRequest(RequestInterface $request, int $attempt = 0): \Psr\Http\Message\StreamInterface
344
    {
345
        if ($attempt >= $this->options->get('max_attempt')) {
346
            throw new \Exception('MAX attempt reached for ' . $request->getUri() . ' with payload ' . (string) $request);
347
        }
348
349
        try {
350
            $response = $this->httpClient->send($request);
351
            if ($response->getStatusCode() === 200) {
352
                return $response->getBody();
353
            }
354
355
            return $this->sendRequest($request, $attempt++);
356
        } catch (ClientException $err) {
357
            if ($err->getResponse()->getStatusCode() === 429) {
358
                $this->renewAuthToken();
359
                return $this->sendRequest($err->getRequest(), $attempt++);
360
            }
361
362
            throw $err;
363
        } catch (\Throwable $err) {
364
            throw $err;
365
        }
366
    }
367
368
    /**
369
     * createBodyForRequest
370
     */
371
    private function createBodyForRequest(array $body = []): ?\GuzzleHttp\Psr7\MultiPartStream
372
    {
373
        if (empty($body)) {
374
            return null;
375
        }
376
        return new MultipartStream($body);
377
    }
378
379
    /**
380
     * renewing access_token
381
     *
382
     * @throws \Exception
383
     */
384
    private function renewAuthToken(): self
385
    {
386
        $request = new \GuzzleHttp\Psr7\Request(
387
            'POST',
388
            self::AUTHENTICATION,
389
            $this->options->get('default_headers'),
390
            $this->createBodyForRequest([
391
                ['name' => 'grant_type',
392
                    'contents' => 'client_credentials', ],
393
                ['name' => 'client_id',
394
                    'contents' => $this->clientId, ],
395
                ['name' => 'client_secret',
396
                    'contents' => $this->clientSecret, ],
397
            ])
398
        );
399
400
        $token = (string) $this->sendRequest($request);
401
402
        $token = json_decode($token, true);
403
404
        if (empty($token)) {
405
            throw new \Exception('Access token request return empty response');
406
        }
407
408
        return $this->setAuthorizationHeader(
409
            $token['access_token']
410
        );
411
    }
412
413
    /**
414
     * set header for OAuth 2.0
415
     */
416
    private function setAuthorizationHeader(string $accessToken): self
417
    {
418
        $this->accessToken = $accessToken;
419
420
        $this->options->set(
421
            'default_headers',
422
            array_merge(
423
                $this->options->get('default_headers'),
424
                [
425
                    'Authorization' => 'Bearer ' . $accessToken,
426
                ]
427
            )
428
        );
429
430
        return $this;
431
    }
432
433
    /**
434
     * get Attachment Submission url Endpoint at rest API
435
     */
436
    private function getAttachmentEndPoint(string $idArticle, string $field): string
437
    {
438
        return $this->replaceEndPointId(
439
440
            $idArticle,
441
            $this->attachmentUrl[$field]
442
        );
443
    }
444
445
    /**
446
     * get article endpoint for deleting api
447
     */
448
    private function getArticleWithIdEndPoint(string $identifier): string
449
    {
450
        return self::ARTICLE_ENDPOINT . "/${identifier}";
451
    }
452
453
    /**
454
     * function that actually replace article_id inside endpoint pattern
455
     */
456
    private function replaceEndPointId(string $identifier, string $url): string
457
    {
458
        return str_replace(
459
            '{article_id}',
460
            $identifier,
461
            $url
462
        );
463
    }
464
465
    /**
466
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
467
     */
468
    private function normalizePayload(Collection $collection): array
469
    {
470
        return $collection->toArray();
471
    }
472
473
    /**
474
     * Checks if Logger instance exists
475
     */
476
    private function hasLogger(): bool
477
    {
478
        return isset($this->logger) && $this->logger !== null;
479
    }
480
}
481