Passed
Pull Request — master (#12)
by kenny
19:40
created

Publisher::doLogging()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 2
dl 0
loc 8
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://dev.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
     * constructor
73
     *
74
     * @param string $clientId
75
     * @param string $clientSecret
76
     * @param array $options
77
     */
78
    public function __construct($clientId, $clientSecret, $options = array())
79
    {
80
        $this->clientId = $clientId;
81
        $this->clientSecret = $clientSecret;
82
83
        $this->assessOptions($options);
84
85
        $this->attachmentUrl = array(
86
            Article::ATTACHMENT_FIELD_GALLERY => self::ARTICLE_ENDPOINT . '/{article_id}/gallery',
87
            Article::ATTACHMENT_FIELD_PAGE => self::ARTICLE_ENDPOINT . '/{article_id}/page',
88
            Article::ATTACHMENT_FIELD_PHOTO => self::ARTICLE_ENDPOINT . '/{article_id}/photo',
89
            Article::ATTACHMENT_FIELD_VIDEO => self::ARTICLE_ENDPOINT . '/{article_id}/video',
90
        );
91
    }
92
93
    /**
94
     * recycleToken from callback. If use external token storage could leveraged on this
95
     *
96
     * @param \Closure $tokenProducer
97
     * @return self
98
     */
99
    public function recycleToken(\Closure $tokenProducer)
100
    {
101
        return $this->setAuthorizationHeader($tokenProducer());
102
    }
103
104
    /**
105
     * assessing and custom option
106
     *
107
     * @param array $options
108
     * @return void
109
     */
110
    private function assessOptions($options)
111
    {
112
        $defaultOptions = array(
113
            'rest_server' => self::REST_SERVER,
114
            'auth_url' => self::AUTHENTICATION,
115
            'max_attempt' => self::DEFAULT_MAX_ATTEMPT,
116
            'default_headers' => array(
117
                "Accept" => "application/json",
118
            ),
119
        );
120
121
        $this->options = new Collection(
122
            array_merge(
123
                $defaultOptions,
124
                $options
125
            )
126
        );
127
128
        if (isset($options['access_token'])) {
129
            $this->setAuthorizationHeader($options['access_token']);
130
        }
131
132
        $this->httpClient = new Client(
133
            $this->options->get('rest_server')
134
        );
135
    }
136
137
    /**
138
     * one gate menu for request creation.
139
     *
140
     * @param string $method
141
     * @param string $path
142
     * @param \One\Collection|array $header
143
     * @param \One\Collection|array $body
144
     * @param array $options
145
     * @return string
146
     */
147
    private function requestGate($method, $path, $header = array(), $body = array(), $options = array())
148
    {
149
        if (empty($this->accessToken)) {
150
            $this->renewAuthToken();
151
        }
152
153
        return (string) $this->sendRequest(
154
            $this->httpClient->createRequest(
155
                $method,
156
                $path,
157
                array_merge(
158
                    $this->options->get('default_headers'),
159
                    $header
160
                ),
161
                $body,
0 ignored issues
show
Bug introduced by
It seems like $body defined by parameter $body on line 147 can also be of type object<One\Collection>; however, Guzzle\Http\Client::createRequest() does only seem to accept string|resource|array|ob...tityBodyInterface>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
162
                $options
163
            )
164
        );
165
    }
166
167
    /**
168
     * actually send request created here, separated for easier attempt count and handling exception
169
     *
170
     * @param \Guzzle\Http\Message\RequestInterface $request
171
     * @param integer $attempt
172
     * @return \Guzzle\Http\EntityBodyInterface|string|null
173
     * @throws \Exception
174
     * @throws \Guzzle\Http\Exception\ClientErrorResponseException
175
     * @throws \Guzzle\Http\Exception\BadResponseException
176
     */
177
    private function sendRequest(RequestInterface $request, $attempt = 0)
178
    {
179
        if ($attempt >= $this->options->get('max_attempt')) {
180
            throw new \Exception("MAX attempt reached for " . $request->getUrl() . " with payload " . (string) $request);
181
        }
182
183
        try {
184
            $response = $request->send();
185
            if ($response->getStatusCode() == 200) {
186
                return $response->getBody();
187
            }
188
            if ($response->getStatusCode() == 429) {
189
                $this->renewAuthToken();
190
            }
191
192
            return $this->sendRequest($request, $attempt++);
193
        } catch (ClientErrorResponseException $err) {
194
            if ($err->getResponse()->getStatusCode() == 429) {
195
                $this->renewAuthToken();
196
                return $this->sendRequest($err->getRequest(), $attempt++);
197
            }
198
199
            throw $err;
200
        } catch (\Exception $err) {
201
            throw $err;
202
        }
203
    }
204
205
    /**
206
     * renewing access_token
207
     *
208
     * @return self
209
     * @throws \Exception
210
     */
211
    private function renewAuthToken()
212
    {
213
        $token = (string) $this->sendRequest(
214
            $this->httpClient->post(
215
                self::AUTHENTICATION,
216
                $this->options->get('default_headers'),
217
                array(
218
                    "grant_type" => "client_credentials",
219
                    "client_id" => $this->clientId,
220
                    "client_secret" => $this->clientSecret,
221
                )
222
            )
223
        );
224
225
        $token = json_decode($token, true);
226
227
        if (empty($token)) {
228
            throw new \Exception("Access token request return empty response");
229
        }
230
231
        return $this->setAuthorizationHeader(
232
            $token['access_token']
233
        );
234
    }
235
236
    /**
237
     * set header for OAuth 2.0
238
     *
239
     * @param string $accessToken
240
     * @return self
241
     */
242
    private function setAuthorizationHeader($accessToken)
243
    {
244
        $this->accessToken = $accessToken;
245
246
        $this->options->set(
247
            'default_headers',
248
            array_merge(
249
                $this->options->get('default_headers'),
250
                array(
251
                    "Authorization" => "Bearer " . $accessToken,
252
                )
253
            )
254
        );
255
256
        return $this;
257
    }
258
259
    /**
260
     * get Attachment Submission url Endpoint at rest API
261
     *
262
     * @param string $idArticle
263
     * @param string $field
264
     * @return string
265
     */
266
    private function getAttachmentEndPoint($idArticle, $field)
267
    {
268
        return $this->replaceEndPointId(
269
            $idArticle,
270
            $this->attachmentUrl[$field]
271
        );
272
    }
273
274
    /**
275
     * get article endpoint for deleting api
276
     *
277
     * @param string $identifier
278
     * @return string
279
     */
280
    private function getArticleWithIdEndPoint($identifier)
281
    {
282
        return self::ARTICLE_ENDPOINT . "/$identifier";
283
    }
284
285
    /**
286
     * function that actually replace article_id inside endpoint pattern
287
     *
288
     * @param string $identifier
289
     * @param string $url
290
     * @return string
291
     */
292
    private function replaceEndPointId($identifier, $url)
293
    {
294
        return str_replace(
295
            '{article_id}',
296
            $identifier,
297
            $url
298
        );
299
    }
300
301
    /**
302
     * normalizing payload. not yet implemented totally, currently just bypass a toArray() function from collection.
303
     *
304
     * @param \One\Collection $collection
305
     * @return array
306
     */
307
    private function normalizePayload(Collection $collection)
308
    {
309
        return $collection->toArray();
310
    }
311
312
    /**
313
     * submitting article here, return new Object cloned from original
314
     *
315
     * @param \One\Model\Article $article
316
     * @return \One\Model\Article
317
     */
318
    public function submitArticle(Article $article)
319
    {
320
        $responseArticle = $this->post(
321
            self::ARTICLE_ENDPOINT,
322
            $this->normalizePayload(
323
                $article->getCollection()
324
            )
325
        );
326
327
        $responseArticle = json_decode($responseArticle, true);
328
        $article->setId($responseArticle['data']['id']);
329
330
        foreach (Article::getPossibleAttachment() as $field) {
331
            if ($article->hasAttachment($field)) {
332
                foreach ($article->getAttachmentByField($field) as $attachment) {
333
                    $this->submitAttachment(
334
                        $article->getId(),
335
                        $attachment,
336
                        $field
337
                    );
338
                }
339
            }
340
        }
341
342
        return $article;
343
    }
344
345
    /**
346
     * submit each attachment of an article here
347
     *
348
     * @param string $idArticle
349
     * @param \One\Model\Model $attachment
350
     * @param string $field
351
     * @return array
352
     */
353
    public function submitAttachment($idArticle, Model $attachment, $field)
354
    {
355
        return json_decode(
356
            $this->post(
357
                $this->getAttachmentEndPoint($idArticle, $field),
358
                $this->normalizePayload(
359
                    $attachment->getCollection()
360
                )
361
            ),
362
            true
363
        );
364
    }
365
366
    /**
367
     * get article from rest API
368
     *
369
     * @param string $idArticle
370
     * @return string json
371
     */
372
    public function getArticle($idArticle)
373
    {
374
        return $this->get(
375
            self::ARTICLE_CHECK_ENDPOINT . "/$idArticle"
376
        );
377
    }
378
379
    /**
380
     * get list article by publisher
381
     *
382
     * @return string json
383
     */
384
    public function listArticle()
385
    {
386
        return $this->get(
387
            self::ARTICLE_ENDPOINT
388
        );
389
    }
390
391
    /**
392
     * delete article based on id
393
     *
394
     * @param string $idArticle
395
     * @return string
396
     */
397
    public function deleteArticle($idArticle)
398
    {
399
        $articleOnRest = $this->getArticle($idArticle);
400
401
        if (!empty($articleOnRest)) {
402
            $articleOnRest = json_decode($articleOnRest, true);
403
404
            if (isset($articleOnRest['data'])) {
405
                foreach (Article::getDeleteableAttachment() as $field) {
406
                    if (isset($articleOnRest['data'][$field])) {
407
                        foreach ($articleOnRest['data'][$field] as $attachment) {
408
                            if (isset($attachment[$field . '_order'])) {
409
                                $this->deleteAttachment($idArticle, $field, $attachment[$field . '_order']);
410
                            }
411
                        }
412
                    }
413
                }
414
            }
415
416
            return $this->delete(
417
                $this->getArticleWithIdEndPoint($idArticle)
418
            );
419
        }
420
    }
421
422
    /**
423
     * delete attachment of an article
424
     *
425
     * @param string $idArticle
426
     * @param string $field
427
     * @param string $order
428
     * @return string
429
     */
430
    public function deleteAttachment($idArticle, $field, $order)
431
    {
432
        return $this->delete(
433
            $this->getAttachmentEndPoint($idArticle, $field) . "/$order"
434
        );
435
    }
436
437
    /**
438
     * Log some messages out
439
     * @param  [string] $message message that will be output
0 ignored issues
show
Documentation introduced by
The doc-type [string] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
440
     * @param  [stirng] $from    From source ?
0 ignored issues
show
Documentation introduced by
The doc-type [stirng] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
441
     * @return void          only echoes
442
     */
443
    private function doLogging($message, $from)
444
    {
445
        if (isset($this->logger) && !is_null($this->logger)) {
446
            $output = $from . " --> " . $message . "\n";
447
448
            $this->logger->info($output);
449
        }
450
    }
451
452
    /**
453
     * get proxy
454
     *
455
     * @param string $path
456
     * @param \One\Collection|array $header
457
     * @param array $options
458
     * @return string
459
     */
460
    final public function get($path, $header = array(), $options = array())
461
    {
462
        $this->doLogging($path, 'Method get ');
463
464
        return $this->requestGate(
465
            'GET',
466
            $path,
467
            $header,
468
            array(),
469
            $options
470
        );
471
    }
472
473
    /**
474
     * post proxy
475
     *
476
     * @param string $path
477
     * @param \One\Collection|array $body
478
     * @param \One\Collection|array $header
479
     * @param array $options
480
     * @return string
481
     */
482
    final public function post($path, $body, $header = array(), $options = array())
483
    {
484
        $this->doLogging($path, 'Method post ');
485
486
        return $this->requestGate(
487
            'POST',
488
            $path,
489
            $header,
490
            $body,
491
            $options
492
        );
493
    }
494
495
    /**
496
     * delete proxy
497
     *
498
     * @param string $path
499
     * @param \One\Collection|array $body
500
     * @param \One\Collection|array $header
501
     * @param array $options
502
     * @return string
503
     */
504
    final public function delete($path, $body = array(), $header = array(), $options = array())
505
    {
506
        return $this->requestGate(
507
            'DELETE',
508
            $path,
509
            $header,
510
            $body,
511
            $options
512
        );
513
    }
514
515
    /**
516
     * @inheritDoc
517
     */
518
    public function setLogger(LoggerInterface $logger)
519
    {
520
        $this->logger = $logger;
521
    }
522
}
523