Completed
Branch cleanup (857352)
by Paul
08:43
created

Client::makeAPIRequest()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 45
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.205

Importance

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