Passed
Push — php-5x ( 68877c...89531b )
by Charis
01:24
created

Publisher::sendRequest()   A

Complexity

Conditions 6
Paths 15

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 15
nop 2
dl 0
loc 22
rs 9.2222
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)
116
    {
117
        $this->tokenSaver = $tokenSaver;
118
        return $this;
119
    }
120
121
    /**
122
     * get Token Saver
123
     */
124
    public function getTokenSaver()
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['recycle_token']) && is_callable($options['recycle_token'])) {
158
            $this->recycleToken(
159
                $options['recycle_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
226
            return $this->sendRequest($request, $attempt++);
227
        } catch (ClientErrorResponseException $err) {
228
            if ($err->getResponse()->getStatusCode() == 401) {
229
                $this->renewAuthToken();
230
                return $this->sendRequest($err->getRequest(), $attempt++);
231
            }
232
233
            throw $err;
234
        } catch (\Exception $err) {
235
            throw $err;
236
        }
237
    }
238
239
    /**
240
     * renewing access_token
241
     *
242
     * @return self
243
     * @throws \Exception
244
     */
245
    private function renewAuthToken()
246
    {
247
        $token = (string) $this->sendRequest(
248
            $this->httpClient->post(
249
                self::AUTHENTICATION,
250
                $this->options->get('default_headers'),
251
                array(
252
                    "grant_type" => "client_credentials",
253
                    "client_id" => $this->clientId,
254
                    "client_secret" => $this->clientSecret,
255
                )
256
            )
257
        );
258
259
        $token = json_decode($token, true);
260
261
        if (empty($token)) {
262
            throw new \Exception("Access token request return empty response");
263
        }
264
265
        if (! empty($this->tokenSaver)) {
266
            $tokenSaver = $this->getTokenSaver();
267
            $tokenSaver(
268
                $token['access_token']
269
            );
270
        }
271
272
        return $this->setAuthorizationHeader(
273
            $token['access_token']
274
        );
275
    }
276
277
    /**
278
     * set header for OAuth 2.0
279
     *
280
     * @param string $accessToken
281
     * @return self
282
     */
283
    private function setAuthorizationHeader($accessToken)
284
    {
285
        $this->accessToken = $accessToken;
286
287
        $this->options->set(
288
            'default_headers',
289
            array_merge(
290
                $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

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