Passed
Push — master ( 7c4e47...1ba277 )
by
unknown
02:58
created

Publisher::submitBreakingnews()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 11
c 1
b 0
f 1
nc 4
nop 1
dl 0
loc 19
rs 9.9
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\Breakingnews;
9
use One\Model\Livestreaming;
10
use One\Model\Headline;
11
use One\Model\Article;
12
use One\Model\Model;
13
use One\Model\Tag;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Log\LoggerAwareInterface;
16
use Psr\Log\LoggerInterface;
17
18
/**
19
 * Publisher class
20
 * main class to be used that interfacing to the API
21
 */
22
class Publisher implements LoggerAwareInterface
23
{
24
    public const DEFAULT_MAX_ATTEMPT = 4;
25
26
    public const AUTHENTICATION = '/oauth/token';
27
28
    public const ARTICLE_CHECK_ENDPOINT = '/api/article';
29
30
    public const ARTICLE_ENDPOINT = '/api/publisher/article';
31
32
    public const PUBLISHER_ENDPOINT = '/api/article/publisher';
33
34
    public const TAG_ENDPOINT = '/api/publisher/tag';
35
36
    public const POST_HEADLINE_ENDPOINT = '/api/publisher/headline';
37
38
    public const POST_BREAKINGNEWS_ENDPOINT = '/api/publisher/breaking_news';
39
40
    public const POST_LIVESTREAMING_ENDPOINT = '/api/publisher/livestreaming';
41
42
    /**
43
     *  base url REST API
44
     * @var array<string>
45
     */
46
    private $restServer = 'https://www.one.co.id';
47
48
49
    /**
50
     * attachment url destination
51
     * @var array<string>
52
     */
53
    private $attachmentUrl;
54
55
    /**
56
     * Logger variable, if set log activity to this obejct each time sending request and receiving response
57
     *
58
     * @var \Psr\Log\LoggerInterface
59
     */
60
    private $logger = null;
61
62
    /**
63
     * credentials props
64
     *
65
     * @var string
66
     */
67
    private $clientId;
68
69
    /**
70
     * client secret
71
     * @var string
72
     */
73
    private $clientSecret;
74
75
    /**
76
     * Oauth access token response
77
     *
78
     * @var string
79
     */
80
    private $accessToken = null;
81
82
    /**
83
     * publisher custom options
84
     *
85
     * @var \One\Collection
86
     */
87
    private $options;
88
89
    /**
90
     * http transaction Client
91
     *
92
     * @var \GuzzleHttp\Client;
93
     */
94
    private $httpClient;
95
96
    /**
97
     * token saver storage
98
     *
99
     * @var \Closure
100
     */
101
    private $tokenSaver = null;
102
103
    /**
104
     * constructor
105
     */
106
    public function __construct(string $clientId, string $clientSecret, array $options = [], bool $development = false)
107
    {
108
        $this->clientId = $clientId;
109
        $this->clientSecret = $clientSecret;
110
111
        if ($development) {
112
            $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...
113
        }
114
115
        $this->assessOptions($options);
116
117
        $this->attachmentUrl = [
118
            Article::ATTACHMENT_FIELD_GALLERY => self::ARTICLE_ENDPOINT . '/{article_id}/gallery',
119
            Article::ATTACHMENT_FIELD_PAGE => self::ARTICLE_ENDPOINT . '/{article_id}/page',
120
            Article::ATTACHMENT_FIELD_PHOTO => self::ARTICLE_ENDPOINT . '/{article_id}/photo',
121
            Article::ATTACHMENT_FIELD_VIDEO => self::ARTICLE_ENDPOINT . '/{article_id}/video',
122
123
        ];
124
    }
125
126
    /**
127
     * recycleToken from callback. If use external token storage could leveraged on this
128
     */
129
    public function recycleToken(\Closure $tokenProducer): self
130
    {
131
        return $this->setAuthorizationHeader(
132
            $tokenProducer()
133
        );
134
    }
135
136
    /**
137
     * set Token Saver
138
     */
139
    public function setTokenSaver(\Closure $tokenSaver): self
140
    {
141
        $this->tokenSaver = $tokenSaver;
142
143
        return $this;
144
    }
145
146
    public function getTokenSaver(): \Closure
147
    {
148
        return $this->tokenSaver;
149
    }
150
151
152
    /**
153
     * submitting article here, return new Object cloned from original
154
     */
155
    public function submitLivestreaming(Livestreaming $livestreaming): Livestreaming
156
    {
157
        $responseLivestreaming = $this->post(
158
            self::POST_LIVESTREAMING_ENDPOINT,
159
            $this->normalizePayload(
160
                $livestreaming->getCollection()
161
            )
162
        );
163
164
        $responseLivestreaming = json_decode($responseLivestreaming, true);
165
        
166
        $id = isset($responseLivestreaming['data'][0]['id']) ?
167
            $responseLivestreaming['data'][0]['id'] : null;
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 breaking news here, return new Object cloned from original
200
     */
201
    public function submitBreakingnews(BreakingNews $breakingNews): BreakingNews 
202
    {
203
        $responseBreakingnews = $this->post(
204
            self::POST_BREAKINGNEWS_ENDPOINT,
205
            $this->normalizePayload(
206
                $breakingNews->getCollection()
207
            )
208
        );
209
210
        $responseBreakingnews = json_decode($responseBreakingnews, true);
211
        $id = isset($responseBreakingnews['data']['id']) ? 
212
            $responseBreakingnews['data']['id'] : null;
213
        
214
        $id = isset($responseBreakingnews['data'][0]['id']) ? 
215
            $responseBreakingnews['data'][0]['id'] : $id;
216
217
        $breakingNews->setId((string) $id);
218
219
        return $breakingNews;
220
    }
221
222
    /**
223
     * submitting article here, return new Object cloned from original
224
     */
225
    public function submitArticle(Article $article): Article
226
    {
227
        $responseArticle = $this->post(
228
            self::ARTICLE_ENDPOINT,
229
            $this->normalizePayload(
230
                $article->getCollection()
231
            )
232
        );
233
234
        $responseArticle = json_decode($responseArticle, true);
235
        $article->setId((string) $responseArticle['data']['id']);
236
237
        foreach ($article->getPossibleAttachment() as $field) {
238
            if ($article->hasAttachment($field)) {
239
                foreach ($article->getAttachmentByField($field) as $attachment) {
240
                    $this->submitAttachment(
241
                        $article->getId(),
242
                        $attachment,
243
                        $field
244
                    );
245
                }
246
            }
247
        }
248
249
        return $article;
250
    }
251
252
    /**
253
     * submit each attachment of an article here
254
     */
255
    public function submitAttachment(string $idArticle, Model $attachment, string $field): array
256
    {
257
        return json_decode(
258
            $this->post(
259
                $this->getAttachmentEndPoint($idArticle, $field),
260
                $this->normalizePayload(
261
                    $attachment->getCollection()
262
                )
263
            ),
264
            true
265
        );
266
    }
267
268
    /**
269
     * submit tag here
270
     */
271
    public function submitTag(Tag $tag): Tag
272
    {
273
        $responseTag = $this->post(
274
            self::TAG_ENDPOINT,
275
            $this->normalizePayload(
276
                $tag->getCollection()
277
            )
278
        );
279
280
        $responseTag = json_decode($responseTag, true);
281
282
        $id = isset($responseTag['data']['tag_id']) ?
283
            $responseTag['data']['tag_id'] : null;
284
285
        $id = isset($responseTag['data'][0]['tag_id']) ?
286
            $responseTag['data'][0]['tag_id'] : $id;
287
288
        $tag->setId((string) $id);
289
290
        return $tag;
291
    }
292
293
    
294
    /**
295
     * get article from rest API
296
     *
297
     * @return string json
298
     */
299
    public function getArticle(string $idArticle): string
300
    {
301
        return $this->get(
302
            self::ARTICLE_CHECK_ENDPOINT . "/${idArticle}"
303
        );
304
    }
305
306
    /**
307
     * get list article by publisher
308
     *
309
     * @return string json
310
     */
311
    public function listArticle(): string
312
    {
313
        return $this->get(
314
            self::ARTICLE_ENDPOINT
315
        );
316
    }
317
318
    /**
319
     * get list visual story by publisher
320
     *
321
     * @return string json
322
     */
323
    public function listStories(int $page = 1): string
324
    {
325
        $params = [
326
            'filter[type_id]' => 4,
327
            'page' => $page,
328
            'apps' => 0
329
        ];
330
331
        return $this->get(
332
            self::PUBLISHER_ENDPOINT.'/'. $this->clientId .'/?'. http_build_query($params)
333
        );
334
    }
335
336
    /**
337
     * delete article based on id
338
     */
339
    public function deleteArticle(string $idArticle): ?string
340
    {
341
        $articleOnRest = $this->getArticle($idArticle);
342
343
        if (! empty($articleOnRest)) {
344
            $articleOnRest = json_decode($articleOnRest, true);
345
346
            if (isset($articleOnRest['data'])) {
347
                foreach (Article::getDeleteableAttachment() as $field) {
348
                    if (isset($articleOnRest['data'][$field])) {
349
                        foreach ($articleOnRest['data'][$field] as $attachment) {
350
                            if (isset($attachment[$field . '_order'])) {
351
                                $this->deleteAttachment($idArticle, $field, $attachment[$field . '_order']);
352
                            }
353
                        }
354
                    }
355
                }
356
            }
357
358
            return $this->delete(
359
                $this->getArticleWithIdEndPoint($idArticle)
360
            );
361
        }
362
    }
363
364
    /**
365
     * delete attachment of an article
366
     */
367
    public function deleteAttachment(string $idArticle, string $field, string $order): string
368
    {
369
        return $this->delete(
370
            $this->getAttachmentEndPoint($idArticle, $field) . "/${order}"
371
        );
372
    }
373
374
    /**
375
     * get proxy
376
     */
377
    final public function get(string $path, array $header = [], array $options = []): string
378
    {
379
        return $this->requestGate(
380
            'GET',
381
            $path,
382
            $header,
383
            [],
384
            $options
385
        );
386
    }
387
388
    /**
389
     * post proxy
390
     *
391
     * @param \One\Collection|array $body
392
     */
393
    final public function post(string $path, array $body = [], array $header = [], array $options = []): string
394
    {
395
        if ($this->hasLogger()) {
396
            $this->logger->info('Post to ' . $path);
397
        }
398
399
        return $this->requestGate(
400
            'POST',
401
            $path,
402
            $header,
403
            $body,
404
            $options
405
        );
406
    }
407
408
    /**
409
     * delete proxy
410
     *
411
     * @param \One\Collection|array $body
412
     * @param array $body
413
     */
414
    final public function delete(string $path, array $body = [], array $header = [], array $options = []): string
415
    {
416
        return $this->requestGate(
417
            'DELETE',
418
            $path,
419
            $header,
420
            $body,
421
            $options
422
        );
423
    }
424
425
    /**
426
     * @inheritDoc
427
     */
428
    public function setLogger(LoggerInterface $logger): void
429
    {
430
        $this->logger = $logger;
431
    }
432
433
    public function getRestServer(): string
434
    {
435
        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...
436
    }
437
438
    /**
439
     * assessing and custom option
440
     */
441
    private function assessOptions(array $options): void
442
    {
443
        $defaultOptions = [
444
            'rest_server' => $this->restServer,
445
            'auth_url' => self::AUTHENTICATION,
446
            'max_attempt' => self::DEFAULT_MAX_ATTEMPT,
447
            'default_headers' => [
448
                'Accept' => 'application/json',
449
            ],
450
        ];
451
452
        $this->options = new Collection(
453
            array_merge(
454
                $defaultOptions,
455
                $options
456
            )
457
        );
458
459
        if (isset($options['access_token'])) {
460
            $this->setAuthorizationHeader($options['access_token']);
461
        }
462
463
        if (isset($options['recycle_token']) && is_callable($options['recycle_token'])) {
464
            $this->recycleToken(
465
                $options['recycle_token']
466
            );
467
        }
468
469
        if (isset($options['token_saver']) && is_callable($options['token_saver'])) {
470
            $this->setTokenSaver(
471
                $options['token_saver']
472
            );
473
        }
474
475
        $this->httpClient = new Client([
476
            'base_uri' => $this->options->get('rest_server'),
477
        ]);
478
    }
479
480
    /**
481
     * one gate menu for request creation.
482
     *
483
     * @param \One\Collection|array $body
484
     */
485
    private function requestGate(string $method, string $path, array $header = [], array $body = [], array $options = []): string
486
    {
487
        if (empty($this->accessToken)) {
488
            $this->renewAuthToken();
489
        }
490
491
        $request = new \GuzzleHttp\Psr7\Request(
492
            $method,
493
            $path,
494
            array_merge(
495
                $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

495
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
496
                $header
497
            ),
498
            $this->createBodyForRequest(
499
                $this->prepareMultipartData($body)
500
            )
501
        );
502
503
        return (string) $this->sendRequest($request);
504
    }
505
506
    private function prepareMultipartData(array $data = []): array
507
    {
508
        $result = [];
509
        foreach ($data as $key => $value) {
510
            array_push($result, ['name' => $key, 'contents' => $value]);
511
        }
512
        return $result;
513
    }
514
515
    /**
516
     * actually send request created here, separated for easier attempt count and handling exception
517
     *
518
     * @throws \Exception
519
     * @throws \GuzzleHttp\Exception\ClientException
520
     * @throws \GuzzleHttp\Exception\BadResponseException
521
     */
522
    private function sendRequest(RequestInterface $request, int $attempt = 0): \Psr\Http\Message\StreamInterface
523
    {
524
        if ($attempt >= $this->options->get('max_attempt')) {
525
            throw new \Exception('MAX attempt reached for ' . $request->getUri() . ' with payload ' . (string) $request);
526
        }
527
528
        try {
529
            $response = $this->httpClient->send(
530
                $request,
531
                [
532
                    'allow_redirects' => false,
533
                    'synchronous' => true,
534
                    'curl' => [
535
                        CURLOPT_FORBID_REUSE => true,
536
                        CURLOPT_MAXCONNECTS => 30,
537
                        CURLOPT_SSL_VERIFYPEER => false,
538
                        CURLOPT_SSL_VERIFYSTATUS => false,
539
                    ],
540
                ]
541
            );
542
            if ($response->getStatusCode() === 200) {
543
                return $response->getBody();
544
            }
545
546
            return $this->sendRequest($request, $attempt++);
547
        } catch (ClientException $err) {
548
            if ($err->getResponse()->getStatusCode() === 401 && $request->getRequestTarget() !== self::AUTHENTICATION) {
549
                $this->renewAuthToken();
550
                return $this->sendRequest($request, $attempt++);
551
            }
552
553
            throw $err;
554
        } catch (\Throwable $err) {
555
            throw $err;
556
        }
557
    }
558
559
    /**
560
     * createBodyForRequest
561
     */
562
    private function createBodyForRequest(array $body = []): ?\GuzzleHttp\Psr7\MultiPartStream
563
    {
564
        if (empty($body)) {
565
            return null;
566
        }
567
        return new MultipartStream($body);
568
    }
569
570
    /**
571
     * renewing access_token
572
     *
573
     * @throws \Exception
574
     */
575
    private function renewAuthToken(): self
576
    {
577
        $request = new \GuzzleHttp\Psr7\Request(
578
            'POST',
579
            self::AUTHENTICATION,
580
            $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

580
            /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
581
            $this->createBodyForRequest([
582
                ['name' => 'grant_type',
583
                    'contents' => 'client_credentials', ],
584
                ['name' => 'client_id',
585
                    'contents' => $this->clientId, ],
586
                ['name' => 'client_secret',
587
                    'contents' => $this->clientSecret, ],
588
            ])
589
        );
590
591
        $token = (string) $this->sendRequest($request);
592
593
        $token = json_decode($token, true);
594
595
        if (empty($token)) {
596
            throw new \Exception('Access token request return empty response');
597
        }
598
599
        if (! empty($this->tokenSaver)) {
600
            $this->getTokenSaver()(
601
                $token['access_token']
602
            );
603
        }
604
605
        return $this->setAuthorizationHeader(
606
            $token['access_token']
607
        );
608
    }
609
610
    /**
611
     * set header for OAuth 2.0
612
     */
613
    private function setAuthorizationHeader(string $accessToken): self
614
    {
615
        $this->accessToken = $accessToken;
616
617
        $this->options->set(
618
            'default_headers',
619
            array_merge(
620
                $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

620
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
621
                [
622
                    'Authorization' => 'Bearer ' . $accessToken,
623
                ]
624
            )
625
        );
626
627
        return $this;
628
    }
629
630
    /**
631
     * get Attachment Submission url Endpoint at rest API
632
     */
633
    private function getAttachmentEndPoint(string $idArticle, string $field): string
634
    {
635
        return $this->replaceEndPointId(
636
            $idArticle,
637
            $this->attachmentUrl[$field]
638
        );
639
    }
640
641
    /**
642
     * get article endpoint for deleting api
643
     */
644
    private function getArticleWithIdEndPoint(string $identifier): string
645
    {
646
        return self::ARTICLE_ENDPOINT . "/${identifier}";
647
    }
648
649
    /**
650
     * function that actually replace article_id inside endpoint pattern
651
     */
652
    private function replaceEndPointId(string $identifier, string $url): string
653
    {
654
        return str_replace(
655
            '{article_id}',
656
            $identifier,
657
            $url
658
        );
659
    }
660
661
    /**
662
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
663
     */
664
    private function normalizePayload(Collection $collection): array
665
    {
666
        return $collection->toArray();
667
    }
668
669
    /**
670
     * Checks if Logger instance exists
671
     */
672
    private function hasLogger(): bool
673
    {
674
        return isset($this->logger) && $this->logger !== null;
675
    }
676
}
677
678