Issues (29)

src/Publisher.php (5 issues)

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

523
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
524
                $header
525
            ),
526
            $this->createBodyForRequest(
527
                $this->prepareMultipartData($body)
528
            )
529
        );
530
531
        return (string) $this->sendRequest($request);
532
    }
533
534
    private function prepareMultipartData(array $data = []): array
535
    {
536
        $result = [];
537
        foreach ($data as $key => $value) {
538
            array_push($result, ['name' => $key, 'contents' => $value]);
539
        }
540
        return $result;
541
    }
542
543
    /**
544
     * actually send request created here, separated for easier attempt count and handling exception
545
     *
546
     * @throws \Exception
547
     * @throws \GuzzleHttp\Exception\ClientException
548
     * @throws \GuzzleHttp\Exception\BadResponseException
549
     */
550
    private function sendRequest(RequestInterface $request, int $attempt = 0): \Psr\Http\Message\StreamInterface
551
    {
552
        if ($attempt >= $this->options->get('max_attempt')) {
553
            throw new \Exception('MAX attempt reached for ' . $request->getUri() . ' with payload ' . (string) $request);
554
        }
555
556
        try {
557
            $response = $this->httpClient->send(
558
                $request,
559
                [
560
                    'allow_redirects' => false,
561
                    'synchronous' => true,
562
                    'curl' => [
563
                        CURLOPT_FORBID_REUSE => true,
564
                        CURLOPT_MAXCONNECTS => 30,
565
                        CURLOPT_SSL_VERIFYPEER => false,
566
                        CURLOPT_SSL_VERIFYSTATUS => false,
567
                    ],
568
                ]
569
            );
570
            if ($response->getStatusCode() === 200) {
571
                return $response->getBody();
572
            }
573
574
            return $this->sendRequest($request, $attempt++);
575
        } catch (ClientException $err) {
576
            if ($err->getResponse()->getStatusCode() === 401 && $request->getRequestTarget() !== self::AUTHENTICATION) {
577
                $this->renewAuthToken();
578
                return $this->sendRequest($request, $attempt++);
579
            }
580
581
            throw $err;
582
        } catch (\Throwable $err) {
583
            throw $err;
584
        }
585
    }
586
587
    /**
588
     * createBodyForRequest
589
     */
590
    private function createBodyForRequest(array $body = []): ?\GuzzleHttp\Psr7\MultiPartStream
591
    {
592
        if (empty($body)) {
593
            return null;
594
        }
595
        return new MultipartStream($body);
596
    }
597
598
    /**
599
     * renewing access_token
600
     *
601
     * @throws \Exception
602
     */
603
    private function renewAuthToken(): self
604
    {
605
        $request = new \GuzzleHttp\Psr7\Request(
606
            'POST',
607
            self::AUTHENTICATION,
608
            $this->options->get('default_headers'),
0 ignored issues
show
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

608
            /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
609
            $this->createBodyForRequest([
610
                ['name' => 'grant_type',
611
                    'contents' => 'client_credentials', ],
612
                ['name' => 'client_id',
613
                    'contents' => $this->clientId, ],
614
                ['name' => 'client_secret',
615
                    'contents' => $this->clientSecret, ],
616
            ])
617
        );
618
619
        $token = (string) $this->sendRequest($request);
620
621
        $token = json_decode($token, true);
622
623
        if (empty($token)) {
624
            throw new \Exception('Access token request return empty response');
625
        }
626
627
        if (! empty($this->tokenSaver)) {
628
            $this->getTokenSaver()(
629
                $token['access_token']
630
            );
631
        }
632
633
        return $this->setAuthorizationHeader(
634
            $token['access_token']
635
        );
636
    }
637
638
    /**
639
     * set header for OAuth 2.0
640
     */
641
    private function setAuthorizationHeader(string $accessToken): self
642
    {
643
        $this->accessToken = $accessToken;
644
645
        $this->options->set(
646
            'default_headers',
647
            array_merge(
648
                $this->options->get('default_headers'),
0 ignored issues
show
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

648
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
649
                [
650
                    'Authorization' => 'Bearer ' . $accessToken,
651
                ]
652
            )
653
        );
654
655
        return $this;
656
    }
657
658
    /**
659
     * get Attachment Submission url Endpoint at rest API
660
     */
661
    private function getAttachmentEndPoint(string $idArticle, string $field): string
662
    {
663
        return $this->replaceEndPointId(
664
            $idArticle,
665
            $this->attachmentUrl[$field]
666
        );
667
    }
668
669
    /**
670
     * get article endpoint for deleting api
671
     */
672
    private function getArticleWithIdEndPoint(string $identifier): string
673
    {
674
        return self::ARTICLE_ENDPOINT . "/${identifier}";
675
    }
676
677
    /**
678
     * function that actually replace article_id inside endpoint pattern
679
     */
680
    private function replaceEndPointId(string $identifier, string $url): string
681
    {
682
        return str_replace(
683
            '{article_id}',
684
            $identifier,
685
            $url
686
        );
687
    }
688
689
    /**
690
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
691
     */
692
    private function normalizePayload(Collection $collection): array
693
    {
694
        return $collection->toArray();
695
    }
696
697
    /**
698
     * Checks if Logger instance exists
699
     */
700
    private function hasLogger(): bool
701
    {
702
        return isset($this->logger) && $this->logger !== null;
703
    }
704
}
705
706