Completed
Branch cleanup (eec673)
by Paul
01:32
created

Client::setAPIRoot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace LibLynx\Connect;
4
5
use GuzzleHttp\Exception\RequestException;
6
use GuzzleHttp\Psr7\Request;
7
use kamermans\OAuth2\GrantType\ClientCredentials;
8
use kamermans\OAuth2\OAuth2Middleware;
9
use GuzzleHttp\HandlerStack;
10
use GuzzleHttp\Client as GuzzleClient;
11
use Kevinrob\GuzzleCache\CacheMiddleware;
12
use Kevinrob\GuzzleCache\Storage\Psr16CacheStorage;
13
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
14
use LibLynx\Connect\Exception\APIException;
15
use LibLynx\Connect\Exception\LogicException;
16
use Psr\Log\LoggerAwareInterface;
17
use Psr\Log\LoggerInterface;
18
use Psr\Log\NullLogger;
19
use Psr\SimpleCache\CacheInterface;
20
use Psr\SimpleCache\InvalidArgumentException;
21
22
/**
23
 * LibLynx Connect API client
24
 *
25
 * $liblynx=new Liblynx\Connect\Client;
26
 * $liblynx->setCredentials('your client id', 'your client secret');
27
 *
28
 * //must set a PSR-16 cache - this can be used for testing
29
 * $liblynx->setCache(new \Symfony\Component\Cache\Simple\ArrayCache);
30
 *
31
 * $identification=$liblynx->authorize(Identification::fromSuperglobals());
32
 * if ($identification->isIdentified()) {
33
 *     //good to go
34
 * }
35
 *
36
 * if ($identification->requiresWayf()) {
37
 *     $identification->doWayfRedirect();
38
 * }
39
 *
40
 * @package LibLynx\Connect
41
 */
42
class Client implements LoggerAwareInterface
43
{
44
    private $apiroot = 'https://connect.liblynx.com';
45
46
    /** @var string client ID obtain from LibLynx Connect admin portal */
47
    private $clientId;
48
49
    /** @var string client secret obtain from LibLynx Connect admin portal */
50
    private $clientSecret;
51
52
    /** @var GuzzleClient HTTP client for API requests */
53
    private $guzzle;
54
55
    /** @var \stdClass entry point resource */
56
    private $entrypoint;
57
58
    /** @var callable RequestStack handler for API requests */
59
    private $apiHandler = null;
60
61
    /** @var callable RequestStack handler for OAuth2 requests */
62
    private $oauth2Handler = null;
63
64
    /** @var CacheInterface */
65
    protected $cache;
66
67
    /** @var LoggerInterface */
68
    protected $log;
69
70
    /**
71
     * Create new LibLynx API client
72
     */
73 16
    public function __construct()
74
    {
75 16
        if (isset($_SERVER['LIBLYNX_CLIENT_ID'])) {
76 2
            $this->clientId = $_SERVER['LIBLYNX_CLIENT_ID'];
77
        }
78 16
        if (isset($_SERVER['LIBLYNX_CLIENT_SECRET'])) {
79 2
            $this->clientSecret = $_SERVER['LIBLYNX_CLIENT_SECRET'];
80
        }
81
82 16
        $this->log = new NullLogger();
83 16
    }
84
85
    /**
86
     * @inheritdoc
87
     */
88 2
    public function setLogger(LoggerInterface $logger)
89
    {
90 2
        $this->log = $logger;
91 2
    }
92
93
    /**
94
     * Set API root
95
     * An alternative root may be provided to you by LibLynx Technical Support
96
     * @param string $url
97
     */
98 10
    public function setAPIRoot($url)
99
    {
100 10
        $this->apiroot = $url;
101 10
    }
102
103
    /**
104
     * Set OAuth2.0 client credentials
105
     * These will be provided to you by LibLynx Technical Support
106
     *
107
     * @param string $clientId
108
     * @param string $clientSecret
109
     */
110 12
    public function setCredentials($clientId, $clientSecret)
111
    {
112 12
        $this->clientId = $clientId;
113 12
        $this->clientSecret = $clientSecret;
114 12
    }
115
116
    /**
117
     * @return array containing client id and secret
118
     */
119 2
    public function getCredentials()
120
    {
121 2
        return [$this->clientId, $this->clientSecret];
122
    }
123
124
    /**
125
     * Attempt an identification using the LibLynx API
126
     *
127
     * @param IdentificationRequest $request
128
     * @return Identification|null
129
     */
130 8
    public function authorize(IdentificationRequest $request)
131
    {
132 8
        $payload = $request->getRequestJSON();
133 8
        $response = $this->post('@new_identification', $payload);
134 4
        if (!isset($response->id)) {
135
            //failed
136 2
            $this->log->critical('Identification request failed {payload}', ['payload' => $payload]);
137 2
            return null;
138
        }
139
140 2
        $identification = new Identification($response);
141 2
        $this->log->info(
142 2
            'Identification request for ip {ip} on URL {url} succeeded status={status} id={id}',
143
            [
144 2
                'status' => $identification->status,
145 2
                'id' => $identification->id,
146 2
                'ip' => $identification->ip,
147 2
                'url' => $identification->url
148
            ]
149
        );
150
151 2
        return $identification;
152
    }
153
154
    /**
155
     * General purpose 'GET' request against API
156
     * @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource
157
     * @return mixed
158
     */
159
    public function get($entrypoint)
160
    {
161
        return $this->makeAPIRequest('GET', $entrypoint);
162
    }
163
164
    /**
165
     * General purpose 'POST' request against API
166
     * @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource
167
     * @param $json string contains JSON formatted data to post
168
     * @return mixed
169
     */
170 8
    public function post($entrypoint, $json)
171
    {
172 8
        return $this->makeAPIRequest('POST', $entrypoint, $json);
173
    }
174
175
    /**
176
     * General purpose 'PUT' request against API
177
     * @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource
178
     * @return mixed string contains JSON formatted data to put
179
     */
180
    public function put($entrypoint, $json)
181
    {
182
        return $this->makeAPIRequest('PUT', $entrypoint, $json);
183
    }
184
185
186
    /**
187
     * This is primarily to facilitate testing - we can add a MockHandler to return
188
     * test responses
189
     *
190
     * @param callable $handler
191
     * @return self
192
     */
193 10
    public function setAPIHandler(callable $handler)
194
    {
195 10
        $this->apiHandler = $handler;
196 10
        return $this;
197
    }
198
199
    /**
200
     * This is primarily to facilitate testing - we can add a MockHandler to return
201
     * test responses
202
     *
203
     * @param callable $handler
204
     * @return self
205
     */
206 10
    public function setOAuth2Handler(callable $handler)
207
    {
208 10
        $this->oauth2Handler = $handler;
209 10
        return $this;
210
    }
211
212
    /**
213
     * @param $method
214
     * @param $entrypoint
215
     * @param null $json
216
     * @return \stdClass object containing JSON decoded response
217
     */
218 8
    protected function makeAPIRequest($method, $entrypoint, $json = null)
219
    {
220 8
        $this->log->debug('{method} {entrypoint} {json}', [
221 8
                'method' => $method,
222 8
                'entrypoint' => $entrypoint,
223 8
                'json' => $json
224
            ]);
225 8
        $url = $this->resolveEntryPoint($entrypoint);
226 4
        $client = $this->getClient();
227
228 4
        $this->log->debug('{entrypoint} = {url}', ['entrypoint' => $entrypoint, 'url' => $url]);
229
230 4
        $headers = ['Accept' => 'application/json'];
231 4
        if (!empty($json)) {
232 4
            $headers['Content-Type'] = 'application/json';
233
        }
234
235 4
        $request = new Request($method, $url, $headers, $json);
236
237
        try {
238 4
            $response = $client->send($request);
239
240 2
            $this->log->debug('{method} {entrypoint} succeeded {status}', [
241 2
                'method' => $method,
242 2
                'entrypoint' => $entrypoint,
243 2
                'status' => $response->getStatusCode(),
244
            ]);
245 2
        } catch (RequestException $e) {
246 2
            $response = $e->getResponse();
247 2
            $this->log->error(
248 2
                '{method} {entrypoint} {json} failed ({status}): {body}',
249
                [
250 2
                    'method' => $method,
251 2
                    'json' => $json,
252 2
                    'entrypoint' => $entrypoint,
253 2
                    'status' => $response->getStatusCode(),
254 2
                    'body' => $response->getBody()
255
                ]
256
            );
257
        }
258
259 4
        $payload = json_decode($response->getBody());
260 4
        return $payload;
261
    }
262
263 8
    public function resolveEntryPoint($nameOrUrl)
264
    {
265 8
        if ($nameOrUrl[0] === '@') {
266 8
            return $this->getEntryPoint($nameOrUrl);
267
        }
268
        //it's a URL
269
        return $nameOrUrl;
270
    }
271
272 14
    public function getEntryPoint($name)
273
    {
274 14
        if (!is_array($this->entrypoint)) {
275 14
            $key = 'entrypoint' . $this->clientId;
276
277 14
            if ($this->cacheHas($key)) {
278 2
                $this->log->debug('loading entrypoint from persistent cache');
279 2
                $this->entrypoint = $this->cacheGet($key);
280
                ;
281
            } else {
282 12
                $this->log->debug('entrypoint not cached, requesting from API');
283 12
                $client = $this->getClient();
284
285 10
                $request = new Request('GET', 'api', [
286 10
                    'Content-Type' => 'application/json',
287
                    'Accept' => 'application/json',
288
                ]);
289
290
                try {
291 10
                    $response = $client->send($request);
292
293 8
                    $payload = json_decode($response->getBody());
294 8
                    if (is_object($payload) && isset($payload->_links)) {
295 8
                        $this->log->info('entrypoint loaded from API and cached');
296 8
                        $this->entrypoint = $payload;
297
298 8
                        $this->cacheSet($key, $payload, 86400);
299
                    }
300 2
                } catch (RequestException $e) {
301 10
                    throw new APIException("Unable to obtain LibLynx API entry point resource", 0, $e);
302
                }
303
            }
304
        } else {
305
            $this->log->debug('using previously loaded entrypoint');
306
        }
307
308 8
        if (!isset($this->entrypoint->_links->$name->href)) {
309 2
            throw new LogicException("Invalid LibLynx API entrypoint $name requested");
310
        }
311
312 6
        return $this->entrypoint->_links->$name->href;
313
    }
314
315
    /**
316
     * @param $key
317
     * @return bool
318
     * @codeCoverageIgnore
319
     */
320 View Code Duplication
    protected function cacheHas($key)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
    {
322
        $cache = $this->getCache();
323
        try {
324
            return $cache->has($key);
325
        } catch (InvalidArgumentException $e) {
326
            throw new LogicException("Cache check rejected key $key", 0, $e);
327
        }
328
    }
329
330
    /**
331
     * @param $key
332
     * @return mixed
333
     * @codeCoverageIgnore
334
     */
335 View Code Duplication
    protected function cacheGet($key)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
336
    {
337
        $cache = $this->getCache();
338
        try {
339
            return $cache->get($key);
340
        } catch (InvalidArgumentException $e) {
341
            throw new LogicException("Cache retrieval rejected key $key", 0, $e);
342
        }
343
    }
344
345
    /**
346
     * @param $key
347
     * @param $value
348
     * @param null $ttl
349
     * @return mixed
350
     * @codeCoverageIgnore
351
     */
352 View Code Duplication
    protected function cacheSet($key, $value, $ttl = null)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
353
    {
354
        $cache = $this->getCache();
355
        try {
356
            return $cache->set($key, $value, $ttl);
357
        } catch (InvalidArgumentException $e) {
358
            throw new LogicException("Cache storage rejected key $key", 0, $e);
359
        }
360
    }
361
362 14
    public function getCache()
363
    {
364 14
        if (is_null($this->cache)) {
365 2
            throw new LogicException('LibLynx Connect Client requires a PSR-16 compatible cache');
366
        }
367 12
        return $this->cache;
368
    }
369
370 12
    public function setCache(CacheInterface $cache)
371
    {
372 12
        $this->cache = $cache;
373 12
    }
374
375
    /**
376
     * Internal helper to provide an OAuth2 capable HTTP client
377
     */
378 12
    protected function getClient()
379
    {
380 12
        if (empty($this->clientId)) {
381 2
            throw new LogicException('Cannot make API calls until setCredentials has been called');
382
        }
383 10
        if (!is_object($this->guzzle)) {
384
            //create our handler stack (which may be mocked in tests) and add the oauth and cache middleware
385 10
            $handlerStack = HandlerStack::create($this->apiHandler);
386 10
            $handlerStack->push($this->createOAuth2Middleware());
387 10
            $handlerStack->push($this->createCacheMiddleware(), 'cache');
388
389
            //now we can make our client
390 10
            $this->guzzle = new GuzzleClient([
391 10
                'handler' => $handlerStack,
392 10
                'auth' => 'oauth',
393 10
                'base_uri' => $this->apiroot
394
            ]);
395
        }
396
397 10
        return $this->guzzle;
398
    }
399
400 10
    protected function createOAuth2Middleware(): OAuth2Middleware
401
    {
402 10
        $handlerStack = HandlerStack::create($this->oauth2Handler);
403
404
        // Authorization client - this is used to request OAuth access tokens
405 10
        $reauth_client = new GuzzleClient([
406 10
            'handler' => $handlerStack,
407
            // URL for access_token request
408 10
            'base_uri' => $this->apiroot . '/oauth/v2/token',
409
        ]);
410
        $reauth_config = [
411 10
            "client_id" => $this->clientId,
412 10
            "client_secret" => $this->clientSecret
413
        ];
414 10
        $grant_type = new ClientCredentials($reauth_client, $reauth_config);
415 10
        $oauth = new OAuth2Middleware($grant_type);
416
417
        //use our cache to store tokens
418 10
        $oauth->setTokenPersistence(new SimpleCacheTokenPersistence($this->getCache()));
419
420 10
        return $oauth;
421
    }
422
423 10
    protected function createCacheMiddleware(): CacheMiddleware
424
    {
425 10
        return new CacheMiddleware(
426 10
            new PrivateCacheStrategy(
427 10
                new Psr16CacheStorage($this->cache)
428
            )
429
        );
430
    }
431
}
432