Passed
Push — php-5x ( 6a77e3...d37a56 )
by Charis
02:21
created

Publisher::setTokenSaver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace One;
4
5
use Guzzle\Http\Client;
6
use Guzzle\Http\Exception\BadResponseException;
7
use Guzzle\Http\Exception\ClientErrorResponseException;
8
use Guzzle\Http\Message\RequestInterface;
9
use One\Model\Article;
10
use One\Model\Model;
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
    const DEFAULT_MAX_ATTEMPT = 4;
21
22
    const REST_SERVER = 'https://www.one.co.id';
23
    const AUTHENTICATION = '/oauth/token';
24
    const ARTICLE_CHECK_ENDPOINT = '/api/article';
25
    const ARTICLE_ENDPOINT = '/api/publisher/article';
26
27
    /*
28
     * attachment url destination
29
     *
30
     * @var array
31
     */
32
    private $attachmentUrl;
33
34
    /**
35
     * Logger variable, if set log activity to this obejct each time sending request and receiving response
36
     *
37
     * @var \Psr\Log\LoggerInterface
38
     */
39
    private $logger = null;
40
41
    /**
42
     * credentials props
43
     *
44
     * @var string $clientId
45
     * @var string $clientSecret
46
     */
47
    private $clientId;
48
    private $clientSecret;
49
50
    /**
51
     * Oauth access token response
52
     *
53
     * @var string $accessToken
54
     */
55
    private $accessToken = null;
56
57
    /**
58
     * publisher custom options
59
     *
60
     * @var \One\Collection $options
61
     */
62
    private $options;
63
64
    /**
65
     * http transaction Client
66
     *
67
     * @var \Guzzle\Http\Client
68
     */
69
    private $httpClient;
70
71
    /**
72
     * token saver storage
73
     *
74
     * @var \Closure
75
     */
76
    private $tokenSaver = null;
77
78
    /**
79
     * constructor
80
     *
81
     * @param string $clientId
82
     * @param string $clientSecret
83
     * @param array $options
84
     */
85
    public function __construct($clientId, $clientSecret, $options = array())
86
    {
87
        $this->clientId = $clientId;
88
        $this->clientSecret = $clientSecret;
89
90
        $this->assessOptions($options);
91
92
        $this->attachmentUrl = array(
93
            Article::ATTACHMENT_FIELD_GALLERY => self::ARTICLE_ENDPOINT . '/{article_id}/gallery',
94
            Article::ATTACHMENT_FIELD_PAGE    => self::ARTICLE_ENDPOINT . '/{article_id}/page',
95
            Article::ATTACHMENT_FIELD_PHOTO   => self::ARTICLE_ENDPOINT . '/{article_id}/photo',
96
            Article::ATTACHMENT_FIELD_VIDEO   => self::ARTICLE_ENDPOINT . '/{article_id}/video'
97
98
        );
99
    }
100
101
    /**
102
     * recycleToken from callback. If use external token storage could leveraged on this
103
     *
104
     * @param \Closure $tokenProducer
105
     * @return self
106
     */
107
    public function recycleToken(\Closure $tokenProducer)
108
    {
109
        return $this->setAuthorizationHeader($tokenProducer());
110
    }
111
112
    /**
113
     * set Token Saver
114
     */
115
    public function setTokenSaver(\Closure $tokenSaver): self
116
    {
117
        $this->tokenSaver = $tokenSaver;
118
        return $this;
119
    }
120
121
    /**
122
     * get Token Saver
123
     */
124
    public function getTokenSaver(): \Closure
125
    {
126
        return $this->tokenSaver;
127
    }
128
129
    /**
130
     * assessing and custom option
131
     *
132
     * @param array $options
133
     * @return void
134
     */
135
    private function assessOptions($options)
136
    {
137
        $defaultOptions = array(
138
            'rest_server' => self::REST_SERVER,
139
            'auth_url' => self::AUTHENTICATION,
140
            'max_attempt' => self::DEFAULT_MAX_ATTEMPT,
141
            'default_headers' => array(
142
                "Accept" => "application/json",
143
            ),
144
        );
145
146
        $this->options = new Collection(
147
            array_merge(
148
                $defaultOptions,
149
                $options
150
            )
151
        );
152
153
        if (isset($options['access_token'])) {
154
            $this->setAuthorizationHeader($options['access_token']);
155
        }
156
157
        if (isset($options['recyle_token']) && is_callable($options['recyle_token'])) {
158
            $this->recycleToken(
159
                $options['recyle_token']
160
            );
161
        }
162
163
        if (isset($options['token_saver']) && is_callable($options['token_saver'])) {
164
            $this->setTokenSaver(
165
                $options['token_saver']
166
            );
167
        }
168
169
        $this->httpClient = new Client(
170
            $this->options->get('rest_server')
171
        );
172
    }
173
174
    /**
175
     * one gate menu for request creation.
176
     *
177
     * @param string $method
178
     * @param string $path
179
     * @param array $header
180
     * @param \One\Collection|array $body
181
     * @param array $options
182
     * @return string
183
     */
184
    private function requestGate($method, $path, $header = array(), $body = array(), $options = array())
185
    {
186
        if (empty($this->accessToken)) {
187
            $this->renewAuthToken();
188
        }
189
190
        return (string) $this->sendRequest(
191
            $this->httpClient->createRequest(
192
                $method,
193
                $path,
194
                array_merge(
195
                    $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 $array1 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

195
                    /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
196
                    $header
197
                ),
198
                $body,
199
                $options
200
            )
201
        );
202
    }
203
204
    /**
205
     * actually send request created here, separated for easier attempt count and handling exception
206
     *
207
     * @param \Guzzle\Http\Message\RequestInterface $request
208
     * @param integer $attempt
209
     * @return \Guzzle\Http\EntityBodyInterface|string|null
210
     * @throws \Exception
211
     * @throws \Guzzle\Http\Exception\ClientErrorResponseException
212
     * @throws \Guzzle\Http\Exception\BadResponseException
213
     */
214
    private function sendRequest(RequestInterface $request, $attempt = 0)
215
    {
216
        if ($attempt >= $this->options->get('max_attempt')) {
217
            throw new \Exception("MAX attempt reached for " . $request->getUrl() . " with payload " . (string) $request);
218
        }
219
220
        try {
221
            $response = $request->send();
222
            if ($response->getStatusCode() == 200) {
223
                return $response->getBody();
224
            }
225
            if ($response->getStatusCode() == 429) {
226
                $this->renewAuthToken();
227
            }
228
229
            return $this->sendRequest($request, $attempt++);
230
        } catch (ClientErrorResponseException $err) {
231
            if ($err->getResponse()->getStatusCode() == 429) {
232
                $this->renewAuthToken();
233
                return $this->sendRequest($err->getRequest(), $attempt++);
234
            }
235
236
            throw $err;
237
        } catch (\Exception $err) {
238
            throw $err;
239
        }
240
    }
241
242
    /**
243
     * renewing access_token
244
     *
245
     * @return self
246
     * @throws \Exception
247
     */
248
    private function renewAuthToken()
249
    {
250
        $token = (string) $this->sendRequest(
251
            $this->httpClient->post(
252
                self::AUTHENTICATION,
253
                $this->options->get('default_headers'),
254
                array(
255
                    "grant_type" => "client_credentials",
256
                    "client_id" => $this->clientId,
257
                    "client_secret" => $this->clientSecret,
258
                )
259
            )
260
        );
261
262
        $token = json_decode($token, true);
263
264
        if (empty($token)) {
265
            throw new \Exception("Access token request return empty response");
266
        }
267
268
        if (! empty($this->tokenSaver)) {
269
            $this->getTokenSaver()(
270
                $token['access_token']
271
            );
272
        }
273
274
        return $this->setAuthorizationHeader(
275
            $token['access_token']
276
        );
277
    }
278
279
    /**
280
     * set header for OAuth 2.0
281
     *
282
     * @param string $accessToken
283
     * @return self
284
     */
285
    private function setAuthorizationHeader($accessToken)
286
    {
287
        $this->accessToken = $accessToken;
288
289
        $this->options->set(
290
            'default_headers',
291
            array_merge(
292
                $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 $array1 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

292
                /** @scrutinizer ignore-type */ $this->options->get('default_headers'),
Loading history...
293
                array(
294
                    "Authorization" => "Bearer " . $accessToken,
295
                )
296
            )
297
        );
298
299
        return $this;
300
    }
301
302
    /**
303
     * get Attachment Submission url Endpoint at rest API
304
     *
305
     * @param string $idArticle
306
     * @param string $field
307
     * @return string
308
     */
309
    private function getAttachmentEndPoint($idArticle, $field)
310
    {
311
        return $this->replaceEndPointId(
312
313
            $idArticle,
314
            $this->attachmentUrl[$field]
315
        );
316
    }
317
318
    /**
319
     * get article endpoint for deleting api
320
     *
321
     * @param string $identifier
322
     * @return string
323
     */
324
    private function getArticleWithIdEndPoint($identifier)
325
    {
326
        return self::ARTICLE_ENDPOINT . "/$identifier";
327
    }
328
329
    /**
330
     * function that actually replace article_id inside endpoint pattern
331
     *
332
     * @param string $identifier
333
     * @param string $url
334
     * @return string
335
     */
336
    private function replaceEndPointId($identifier, $url)
337
    {
338
        return str_replace(
339
            '{article_id}',
340
            $identifier,
341
            $url
342
        );
343
    }
344
345
    /**
346
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
347
     *
348
     * @param \One\Collection $collection
349
     * @return array
350
     */
351
    private function normalizePayload(Collection $collection)
352
    {
353
        return $collection->toArray();
354
    }
355
356
    /**
357
     * submitting article here, return new Object cloned from original
358
     *
359
     * @param \One\Model\Article $article
360
     * @return \One\Model\Article
361
     */
362
    public function submitArticle(Article $article)
363
    {
364
        $responseArticle = $this->post(
365
            self::ARTICLE_ENDPOINT,
366
            $this->normalizePayload(
367
                $article->getCollection()
368
            )
369
        );
370
371
        $responseArticle = json_decode($responseArticle, true);
372
        $article->setId($responseArticle['data']['id']);
373
374
        foreach ($article->getPossibleAttachment() as $field) {
375
            if ($article->hasAttachment($field)) {
376
                foreach ($article->getAttachmentByField($field) as $attachment) {
377
                    $this->submitAttachment(
378
                        $article->getId(),
379
                        $attachment,
380
                        $field
381
                    );
382
                }
383
            }
384
        }
385
386
        return $article;
387
    }
388
389
    /**
390
     * submit each attachment of an article here
391
     *
392
     * @param string $idArticle
393
     * @param \One\Model\Model $attachment
394
     * @param string $field
395
     * @return array
396
     */
397
    public function submitAttachment($idArticle, Model $attachment, $field)
398
    {
399
        return json_decode(
400
            $this->post(
401
                $this->getAttachmentEndPoint($idArticle, $field),
402
                $this->normalizePayload(
403
                    $attachment->getCollection()
404
                )
405
            ),
406
            true
407
        );
408
    }
409
410
    /**
411
     * get article from rest API
412
     *
413
     * @param string $idArticle
414
     * @return string json
415
     */
416
    public function getArticle($idArticle)
417
    {
418
        return $this->get(
419
            self::ARTICLE_CHECK_ENDPOINT . "/$idArticle"
420
        );
421
    }
422
423
    /**
424
     * get list article by publisher
425
     *
426
     * @return string json
427
     */
428
    public function listArticle()
429
    {
430
        return $this->get(
431
            self::ARTICLE_ENDPOINT
432
        );
433
    }
434
435
    /**
436
     * delete article based on id
437
     *
438
     * @param string $idArticle
439
     * @return string
440
     */
441
    public function deleteArticle($idArticle)
442
    {
443
        $articleOnRest = $this->getArticle($idArticle);
444
445
        if (!empty($articleOnRest)) {
446
            $articleOnRest = json_decode($articleOnRest, true);
447
448
            if (isset($articleOnRest['data'])) {
449
                foreach (Article::getDeleteableAttachment() as $field) {
450
                    if (isset($articleOnRest['data'][$field])) {
451
                        foreach ($articleOnRest['data'][$field] as $attachment) {
452
                            if (isset($attachment[$field . '_order'])) {
453
                                $this->deleteAttachment($idArticle, $field, $attachment[$field . '_order']);
454
                            }
455
                        }
456
                    }
457
                }
458
            }
459
460
            return $this->delete(
461
                $this->getArticleWithIdEndPoint($idArticle)
462
            );
463
        }
464
    }
465
466
    /**
467
     * delete attachment of an article
468
     *
469
     * @param string $idArticle
470
     * @param string $field
471
     * @param string $order
472
     * @return string
473
     */
474
    public function deleteAttachment($idArticle, $field, $order)
475
    {
476
        return $this->delete(
477
            $this->getAttachmentEndPoint($idArticle, $field) . "/$order"
478
        );
479
    }
480
481
    /**
482
     * Checks if Logger instance exists
483
     * @return boolean
484
     */
485
    private function hasLogger()
486
    {
487
        return isset($this->logger) && !is_null($this->logger);
488
    }
489
490
    /**
491
     * get proxy
492
     *
493
     * @param string $path
494
     * @param array $header
495
     * @param array $options
496
     * @return string
497
     */
498
    final public function get($path, $header = array(), $options = array())
499
    {
500
        return $this->requestGate(
501
            'GET',
502
            $path,
503
            $header,
504
            array(),
505
            $options
506
        );
507
    }
508
509
    /**
510
     * post proxy
511
     *
512
     * @param string $path
513
     * @param \One\Collection|array $body
514
     * @param array $header
515
     * @param array $options
516
     * @return string
517
     */
518
    final public function post($path, $body, $header = array(), $options = array())
519
    {
520
        if ($this->hasLogger()) {
521
            $this->logger->info("Post to " . $path);
522
        }
523
524
        return $this->requestGate(
525
            'POST',
526
            $path,
527
            $header,
528
            $body,
529
            $options
530
        );
531
    }
532
533
    /**
534
     * delete proxy
535
     *
536
     * @param string $path
537
     * @param \One\Collection|array $body
538
     * @param array $header
539
     * @param array $options
540
     * @return string
541
     */
542
    final public function delete($path, $body = array(), $header = array(), $options = array())
543
    {
544
        return $this->requestGate(
545
            'DELETE',
546
            $path,
547
            $header,
548
            $body,
549
            $options
550
        );
551
    }
552
553
    /**
554
     * @inheritDoc
555
     */
556
    public function setLogger(LoggerInterface $logger)
557
    {
558
        $this->logger = $logger;
559
    }
560
}
561