Passed
Pull Request — master (#93)
by Yasin
03:19
created

Publisher::normalizePayload()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php declare(strict_types=1);
2
3
namespace One;
4
5
use GuzzleHttp\Exception\ClientException;
6
use GuzzleHttp\Psr7\MultipartStream;
7
use GuzzleHttp\Client;
8
use One\Model\Livestreaming;
9
use One\Model\Headline;
10
use One\Model\Article;
11
use One\Model\Model;
12
use One\Model\Tag;
13
use Psr\Http\Message\RequestInterface;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
17
/**
18
 * Publisher class
19
 * main class to be used that interfacing to the API
20
 */
21
class Publisher implements LoggerAwareInterface
22
{
23
    public const DEFAULT_MAX_ATTEMPT = 4;
24
25
    public const AUTHENTICATION = '/oauth/token';
26
27
    public const ARTICLE_CHECK_ENDPOINT = '/api/article';
28
29
    public const ARTICLE_ENDPOINT = '/api/publisher/article';
30
31
    public const PUBLISHER_ENDPOINT = '/api/article/publisher';
32
33
    public const TAG_ENDPOINT = '/api/publisher/tag';
34
35
    public const POST_HEADLINE_ENDPOINT = '/api/publisher/headline';
36
37
    public const POST_LIVESTREAMING_ENDPOINT = '/api/publisher/livestreaming';
38
39
    /**
40
     *  base url REST API
41
     * @var array<string>
42
     */
43
    private $restServer = 'https://www.one.co.id';
44
45
46
    /**
47
     * attachment url destination
48
     * @var array<string>
49
     */
50
    private $attachmentUrl;
51
52
    /**
53
     * Logger variable, if set log activity to this obejct each time sending request and receiving response
54
     *
55
     * @var \Psr\Log\LoggerInterface
56
     */
57
    private $logger = null;
58
59
    /**
60
     * credentials props
61
     *
62
     * @var string
63
     */
64
    private $clientId;
65
66
    /**
67
     * client secret
68
     * @var string
69
     */
70
    private $clientSecret;
71
72
    /**
73
     * Oauth access token response
74
     *
75
     * @var string
76
     */
77
    private $accessToken = null;
78
79
    /**
80
     * publisher custom options
81
     *
82
     * @var \One\Collection
83
     */
84
    private $options;
85
86
    /**
87
     * http transaction Client
88
     *
89
     * @var \GuzzleHttp\Client;
90
     */
91
    private $httpClient;
92
93
    /**
94
     * token saver storage
95
     *
96
     * @var \Closure
97
     */
98
    private $tokenSaver = null;
99
100
    /**
101
     * constructor
102
     */
103
    public function __construct(string $clientId, string $clientSecret, array $options = [], bool $development = false)
104
    {
105
        $this->clientId = $clientId;
106
        $this->clientSecret = $clientSecret;
107
108
        if ($development) {
109
            $this->restServer = 'https://dev.one.co.id';
0 ignored issues
show
Documentation Bug introduced by
It seems like 'https://dev.one.co.id' of type string is incompatible with the declared type string[] of property $restServer.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
110
        }
111
112
        $this->assessOptions($options);
113
114
        $this->attachmentUrl = [
115
            Article::ATTACHMENT_FIELD_GALLERY => self::ARTICLE_ENDPOINT . '/{article_id}/gallery',
116
            Article::ATTACHMENT_FIELD_PAGE => self::ARTICLE_ENDPOINT . '/{article_id}/page',
117
            Article::ATTACHMENT_FIELD_PHOTO => self::ARTICLE_ENDPOINT . '/{article_id}/photo',
118
            Article::ATTACHMENT_FIELD_VIDEO => self::ARTICLE_ENDPOINT . '/{article_id}/video',
119
120
        ];
121
    }
122
123
    /**
124
     * recycleToken from callback. If use external token storage could leveraged on this
125
     */
126
    public function recycleToken(\Closure $tokenProducer): self
127
    {
128
        return $this->setAuthorizationHeader(
129
            $tokenProducer()
130
        );
131
    }
132
133
    /**
134
     * set Token Saver
135
     */
136
    public function setTokenSaver(\Closure $tokenSaver): self
137
    {
138
        $this->tokenSaver = $tokenSaver;
139
140
        return $this;
141
    }
142
143
    public function getTokenSaver(): \Closure
144
    {
145
        return $this->tokenSaver;
146
    }
147
148
149
    /**
150
     * submitting article here, return new Object cloned from original
151
     */
152
    public function submitLivestreaming(Livestreaming $livestreaming): Livestreaming
153
    {
154
        $responseLivestreaming = $this->post(
155
            self::POST_LIVESTREAMING_ENDPOINT,
156
            $this->normalizePayload(
157
                $article->getCollection()
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $article seems to be never defined.
Loading history...
158
            )
159
        );
160
161
        $responseLivestreaming = json_decode($responseLivestreaming, true);
162
        
163
        $id = isset($responseLivestreaming['data']['livestreaming_id']) ?
164
            $responseLivestreaming['data']['livestreaming_id'] : null;
165
166
        $id = isset($responseLivestreaming['data'][0]['livestreaming_id']) ?
167
            $responseLivestreaming['data'][0]['livestreaming_id'] : $id;
168
169
        $livestreaming->setId((string) $id);
170
171
        return $livestreaming;
172
    }
173
174
    /**
175
     * submitting headline here, return new Object cloned from original
176
     */
177
    public function submitHeadline(Headline $headline): Headline
178
    {
179
        $responseHeadline = $this->post(
180
            self::POST_HEADLINE_ENDPOINT,
181
            $this->normalizePayload(
182
                $headline->getCollection()
183
            )
184
        );
185
186
        $responseHeadline = json_decode($responseHeadline, true);
187
        $id = isset($responseHeadline['data']['headline_id']) ?
188
            $responseHeadline['data']['headline_id'] : null;
189
190
        $id = isset($responseHeadline['data'][0]['headline_id']) ?
191
            $responseHeadline['data'][0]['headline_id'] : $id;
192
193
        $headline->setId((string) $id);
194
195
        return $headline;
196
    }
197
198
    /**
199
     * submitting article here, return new Object cloned from original
200
     */
201
    public function submitArticle(Article $article): Article
202
    {
203
        $responseArticle = $this->post(
204
            self::ARTICLE_ENDPOINT,
205
            $this->normalizePayload(
206
                $article->getCollection()
207
            )
208
        );
209
210
        $responseArticle = json_decode($responseArticle, true);
211
        $article->setId((string) $responseArticle['data']['id']);
212
213
        foreach ($article->getPossibleAttachment() as $field) {
214
            if ($article->hasAttachment($field)) {
215
                foreach ($article->getAttachmentByField($field) as $attachment) {
216
                    $this->submitAttachment(
217
                        $article->getId(),
218
                        $attachment,
219
                        $field
220
                    );
221
                }
222
            }
223
        }
224
225
        return $article;
226
    }
227
228
    /**
229
     * submit each attachment of an article here
230
     */
231
    public function submitAttachment(string $idArticle, Model $attachment, string $field): array
232
    {
233
        return json_decode(
234
            $this->post(
235
                $this->getAttachmentEndPoint($idArticle, $field),
236
                $this->normalizePayload(
237
                    $attachment->getCollection()
238
                )
239
            ),
240
            true
241
        );
242
    }
243
244
    /**
245
     * submit tag here
246
     */
247
    public function submitTag(Tag $tag): Tag
248
    {
249
        $responseTag = $this->post(
250
            self::TAG_ENDPOINT,
251
            $this->normalizePayload(
252
                $tag->getCollection()
253
            )
254
        );
255
256
        $responseTag = json_decode($responseTag, true);
257
258
        $id = isset($responseTag['data']['tag_id']) ?
259
            $responseTag['data']['tag_id'] : null;
260
261
        $id = isset($responseTag['data'][0]['tag_id']) ?
262
            $responseTag['data'][0]['tag_id'] : $id;
263
264
        $tag->setId((string) $id);
265
266
        return $tag;
267
    }
268
269
    
270
    /**
271
     * get article from rest API
272
     *
273
     * @return string json
274
     */
275
    public function getArticle(string $idArticle): string
276
    {
277
        return $this->get(
278
            self::ARTICLE_CHECK_ENDPOINT . "/${idArticle}"
279
        );
280
    }
281
282
    /**
283
     * get list article by publisher
284
     *
285
     * @return string json
286
     */
287
    public function listArticle(): string
288
    {
289
        return $this->get(
290
            self::ARTICLE_ENDPOINT
291
        );
292
    }
293
294
    /**
295
     * get list visual story by publisher
296
     *
297
     * @return string json
298
     */
299
    public function listStories(int $page = 1): string
300
    {
301
        $params = [
302
            'filter[type_id]' => 4,
303
            'page' => $page,
304
            'apps' => 0
305
        ];
306
307
        return $this->get(
308
            self::PUBLISHER_ENDPOINT.'/'. $this->clientId .'/?'. http_build_query($params)
309
        );
310
    }
311
312
    /**
313
     * delete article based on id
314
     */
315
    public function deleteArticle(string $idArticle): ?string
316
    {
317
        $articleOnRest = $this->getArticle($idArticle);
318
319
        if (! empty($articleOnRest)) {
320
            $articleOnRest = json_decode($articleOnRest, true);
321
322
            if (isset($articleOnRest['data'])) {
323
                foreach (Article::getDeleteableAttachment() as $field) {
324
                    if (isset($articleOnRest['data'][$field])) {
325
                        foreach ($articleOnRest['data'][$field] as $attachment) {
326
                            if (isset($attachment[$field . '_order'])) {
327
                                $this->deleteAttachment($idArticle, $field, $attachment[$field . '_order']);
328
                            }
329
                        }
330
                    }
331
                }
332
            }
333
334
            return $this->delete(
335
                $this->getArticleWithIdEndPoint($idArticle)
336
            );
337
        }
338
    }
339
340
    /**
341
     * delete attachment of an article
342
     */
343
    public function deleteAttachment(string $idArticle, string $field, string $order): string
344
    {
345
        return $this->delete(
346
            $this->getAttachmentEndPoint($idArticle, $field) . "/${order}"
347
        );
348
    }
349
350
    /**
351
     * get proxy
352
     */
353
    final public function get(string $path, array $header = [], array $options = []): string
354
    {
355
        return $this->requestGate(
356
            'GET',
357
            $path,
358
            $header,
359
            [],
360
            $options
361
        );
362
    }
363
364
    /**
365
     * post proxy
366
     *
367
     * @param \One\Collection|array $body
368
     */
369
    final public function post(string $path, array $body = [], array $header = [], array $options = []): string
370
    {
371
        if ($this->hasLogger()) {
372
            $this->logger->info('Post to ' . $path);
373
        }
374
375
        return $this->requestGate(
376
            'POST',
377
            $path,
378
            $header,
379
            $body,
380
            $options
381
        );
382
    }
383
384
    /**
385
     * delete proxy
386
     *
387
     * @param \One\Collection|array $body
388
     * @param array $body
389
     */
390
    final public function delete(string $path, array $body = [], array $header = [], array $options = []): string
391
    {
392
        return $this->requestGate(
393
            'DELETE',
394
            $path,
395
            $header,
396
            $body,
397
            $options
398
        );
399
    }
400
401
    /**
402
     * @inheritDoc
403
     */
404
    public function setLogger(LoggerInterface $logger): void
405
    {
406
        $this->logger = $logger;
407
    }
408
409
    public function getRestServer(): string
410
    {
411
        return $this->options->get('rest_server');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->options->get('rest_server') could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
412
    }
413
414
    /**
415
     * assessing and custom option
416
     */
417
    private function assessOptions(array $options): void
418
    {
419
        $defaultOptions = [
420
            'rest_server' => $this->restServer,
421
            'auth_url' => self::AUTHENTICATION,
422
            'max_attempt' => self::DEFAULT_MAX_ATTEMPT,
423
            'default_headers' => [
424
                'Accept' => 'application/json',
425
            ],
426
        ];
427
428
        $this->options = new Collection(
429
            array_merge(
430
                $defaultOptions,
431
                $options
432
            )
433
        );
434
435
        if (isset($options['access_token'])) {
436
            $this->setAuthorizationHeader($options['access_token']);
437
        }
438
439
        if (isset($options['recycle_token']) && is_callable($options['recycle_token'])) {
440
            $this->recycleToken(
441
                $options['recycle_token']
442
            );
443
        }
444
445
        if (isset($options['token_saver']) && is_callable($options['token_saver'])) {
446
            $this->setTokenSaver(
447
                $options['token_saver']
448
            );
449
        }
450
451
        $this->httpClient = new Client([
452
            'base_uri' => $this->options->get('rest_server'),
453
        ]);
454
    }
455
456
    /**
457
     * one gate menu for request creation.
458
     *
459
     * @param \One\Collection|array $body
460
     */
461
    private function requestGate(string $method, string $path, array $header = [], array $body = [], array $options = []): string
462
    {
463
        if (empty($this->accessToken)) {
464
            $this->renewAuthToken();
465
        }
466
467
        $request = new \GuzzleHttp\Psr7\Request(
468
            $method,
469
            $path,
470
            array_merge(
471
                $this->options->get('default_headers'),
0 ignored issues
show
Bug introduced by
It seems like $this->options->get('default_headers') can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

471
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
472
                $header
473
            ),
474
            $this->createBodyForRequest(
475
                $this->prepareMultipartData($body)
476
            )
477
        );
478
479
        return (string) $this->sendRequest($request);
480
    }
481
482
    private function prepareMultipartData(array $data = []): array
483
    {
484
        $result = [];
485
        foreach ($data as $key => $value) {
486
            array_push($result, ['name' => $key, 'contents' => $value]);
487
        }
488
        return $result;
489
    }
490
491
    /**
492
     * actually send request created here, separated for easier attempt count and handling exception
493
     *
494
     * @throws \Exception
495
     * @throws \GuzzleHttp\Exception\ClientException
496
     * @throws \GuzzleHttp\Exception\BadResponseException
497
     */
498
    private function sendRequest(RequestInterface $request, int $attempt = 0): \Psr\Http\Message\StreamInterface
499
    {
500
        if ($attempt >= $this->options->get('max_attempt')) {
501
            throw new \Exception('MAX attempt reached for ' . $request->getUri() . ' with payload ' . (string) $request);
502
        }
503
504
        try {
505
            $response = $this->httpClient->send(
506
                $request,
507
                [
508
                    'allow_redirects' => false,
509
                    'synchronous' => true,
510
                    'curl' => [
511
                        CURLOPT_FORBID_REUSE => true,
512
                        CURLOPT_MAXCONNECTS => 30,
513
                        CURLOPT_SSL_VERIFYPEER => false,
514
                        CURLOPT_SSL_VERIFYSTATUS => false,
515
                    ],
516
                ]
517
            );
518
            if ($response->getStatusCode() === 200) {
519
                return $response->getBody();
520
            }
521
522
            return $this->sendRequest($request, $attempt++);
523
        } catch (ClientException $err) {
524
            if ($err->getResponse()->getStatusCode() === 401 && $request->getRequestTarget() !== self::AUTHENTICATION) {
525
                $this->renewAuthToken();
526
                return $this->sendRequest($request, $attempt++);
527
            }
528
529
            throw $err;
530
        } catch (\Throwable $err) {
531
            throw $err;
532
        }
533
    }
534
535
    /**
536
     * createBodyForRequest
537
     */
538
    private function createBodyForRequest(array $body = []): ?\GuzzleHttp\Psr7\MultiPartStream
539
    {
540
        if (empty($body)) {
541
            return null;
542
        }
543
        return new MultipartStream($body);
544
    }
545
546
    /**
547
     * renewing access_token
548
     *
549
     * @throws \Exception
550
     */
551
    private function renewAuthToken(): self
552
    {
553
        $request = new \GuzzleHttp\Psr7\Request(
554
            'POST',
555
            self::AUTHENTICATION,
556
            $this->options->get('default_headers'),
0 ignored issues
show
Bug introduced by
It seems like $this->options->get('default_headers') can also be of type null; however, parameter $headers of GuzzleHttp\Psr7\Request::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

556
            /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
557
            $this->createBodyForRequest([
558
                ['name' => 'grant_type',
559
                    'contents' => 'client_credentials', ],
560
                ['name' => 'client_id',
561
                    'contents' => $this->clientId, ],
562
                ['name' => 'client_secret',
563
                    'contents' => $this->clientSecret, ],
564
            ])
565
        );
566
567
        $token = (string) $this->sendRequest($request);
568
569
        $token = json_decode($token, true);
570
571
        if (empty($token)) {
572
            throw new \Exception('Access token request return empty response');
573
        }
574
575
        if (! empty($this->tokenSaver)) {
576
            $this->getTokenSaver()(
577
                $token['access_token']
578
            );
579
        }
580
581
        return $this->setAuthorizationHeader(
582
            $token['access_token']
583
        );
584
    }
585
586
    /**
587
     * set header for OAuth 2.0
588
     */
589
    private function setAuthorizationHeader(string $accessToken): self
590
    {
591
        $this->accessToken = $accessToken;
592
593
        $this->options->set(
594
            'default_headers',
595
            array_merge(
596
                $this->options->get('default_headers'),
0 ignored issues
show
Bug introduced by
It seems like $this->options->get('default_headers') can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

596
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
597
                [
598
                    'Authorization' => 'Bearer ' . $accessToken,
599
                ]
600
            )
601
        );
602
603
        return $this;
604
    }
605
606
    /**
607
     * get Attachment Submission url Endpoint at rest API
608
     */
609
    private function getAttachmentEndPoint(string $idArticle, string $field): string
610
    {
611
        return $this->replaceEndPointId(
612
            $idArticle,
613
            $this->attachmentUrl[$field]
614
        );
615
    }
616
617
    /**
618
     * get article endpoint for deleting api
619
     */
620
    private function getArticleWithIdEndPoint(string $identifier): string
621
    {
622
        return self::ARTICLE_ENDPOINT . "/${identifier}";
623
    }
624
625
    /**
626
     * function that actually replace article_id inside endpoint pattern
627
     */
628
    private function replaceEndPointId(string $identifier, string $url): string
629
    {
630
        return str_replace(
631
            '{article_id}',
632
            $identifier,
633
            $url
634
        );
635
    }
636
637
    /**
638
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
639
     */
640
    private function normalizePayload(Collection $collection): array
641
    {
642
        return $collection->toArray();
643
    }
644
645
    /**
646
     * Checks if Logger instance exists
647
     */
648
    private function hasLogger(): bool
649
    {
650
        return isset($this->logger) && $this->logger !== null;
651
    }
652
}
653
654