Passed
Push — master ( 55f92d...5fc33e )
by Dan Michael O.
05:59
created

Client::setRegion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Scriptotek\Alma;
4
5
use Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement;
6
use Http\Client\Common\Exception\ClientErrorException;
7
use Http\Client\Common\Plugin\ContentLengthPlugin;
8
use Http\Client\Common\Plugin\ErrorPlugin;
9
use Http\Client\Common\PluginClient;
10
use Http\Client\Exception\HttpException;
11
use Http\Client\Exception\NetworkException;
12
use Http\Client\HttpClient;
13
use Http\Discovery\HttpClientDiscovery;
14
use Http\Discovery\MessageFactoryDiscovery;
15
use Http\Discovery\UriFactoryDiscovery;
16
use Http\Message\MessageFactory;
17
use Http\Message\UriFactory;
18
use Psr\Http\Message\RequestInterface;
19
use Psr\Http\Message\ResponseInterface;
20
use Psr\Http\Message\UriInterface;
21
use Scriptotek\Alma\Analytics\Analytics;
22
use Scriptotek\Alma\Bibs\Bibs;
23
use Scriptotek\Alma\Exception\ClientException;
24
use Scriptotek\Alma\Exception\MaxNumberOfAttemptsExhausted;
25
use Scriptotek\Alma\Exception\SruClientNotSetException;
26
use Scriptotek\Alma\Users\Users;
27
use Scriptotek\Sru\Client as SruClient;
28
29
/**
30
 * Alma client.
31
 */
32
class Client
33
{
34
    public $baseUrl;
35
36
    /** @var string Alma zone (institution or network) */
37
    public $zone;
38
39
    /** @var string Alma Developers Network API key for this zone */
40
    public $key;
41
42
    /** @var Client Network zone instance */
43
    public $nz;
44
45
    /** @var HttpClient */
46
    protected $http;
47
48
    /** @var MessageFactory */
49
    protected $messageFactory;
50
51
    /** @var UriFactory */
52
    protected $uriFactory;
53
54
    /** @var SruClient */
55
    public $sru;
56
57
    /** @var Bibs */
58
    public $bibs;
59
60
    /** @var Analytics */
61
    public $analytics;
62
63
    /** @var Users */
64
    public $users;
65
66
    /** @var int Max number of retries if we get 429 errors */
67
    public $maxAttempts = 10;
68
69
    /** @var float Number of seconds to sleep before retrying */
70
    public $sleepTimeOnRetry = 0.5;
71
72
    /**
73
     * Create a new client to connect to a given Alma instance.
74
     *
75
     * @param string     $key        API key
76
     * @param string     $region     Hosted region code, used to build base URL
77
     * @param string     $zone       Alma zone (Either Zones::INSTITUTION or Zones::NETWORK)
78
     * @param HttpClient $http
79
     * @param MessageFactory $messageFactory
80
     * @param UriFactory $uriFactory
81
     *
82
     * @throws \ErrorException
83
     */
84
    public function __construct(
85
        $key = null,
86
        $region = 'eu',
87
        $zone = Zones::INSTITUTION,
88
        HttpClient $http = null,
89
        MessageFactory $messageFactory = null,
90
        UriFactory $uriFactory = null
91
    ) {
92
        $this->http = new PluginClient(
93
            $http ?: HttpClientDiscovery::find(), [
94
                new ContentLengthPlugin(),
95
                new ErrorPlugin(),
96
            ]
97
        );
98
        $this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find();
99
        $this->uriFactory = $uriFactory ?: UriFactoryDiscovery::find();
100
101
        $this->key = $key;
102
        $this->setRegion($region);
103
104
        $this->zone = $zone;
105
        $this->bibs = new Bibs($this);  // Or do some magic instead?
106
        $this->analytics = new Analytics($this);  // Or do some magic instead?
107
        $this->users = new Users($this);  // Or do some magic instead?
108
        if ($zone == Zones::INSTITUTION) {
109
            $this->nz = new self(null, $region, Zones::NETWORK, $this->http, $this->messageFactory, $this->uriFactory);
110
        } elseif ($zone != Zones::NETWORK) {
111
            throw new ClientException('Invalid zone name.');
112
        }
113
    }
114
115
    /**
116
     * Attach an SRU client (so you can search for Bib records).
117
     *
118
     * @param SruClient $sru
119
     */
120
    public function setSruClient(SruClient $sru)
121
    {
122
        $this->sru = $sru;
123
    }
124
125
    /**
126
     * Assert that an SRU client is connected. Throws SruClientNotSetException if not.
127
     *
128
     * @throws SruClientNotSetException
129
     */
130
    public function assertHasSruClient()
131
    {
132
        if (!isset($this->sru)) {
133
            throw new SruClientNotSetException();
134
        }
135
    }
136
137
    /**
138
     * Set the API key for this Alma instance.
139
     *
140
     * @param string $key The API key
141
     *
142
     * @return $this
143
     */
144
    public function setKey($key)
145
    {
146
        $this->key = $key;
147
148
        return $this;
149
    }
150
151
    /**
152
     * Set the Alma region code ('na' for North America, 'eu' for Europe, 'ap' for Asia Pacific).
153
     *
154
     * @param $regionCode
155
     *
156
     * @throws \ErrorException
157
     *
158
     * @return $this
159
     */
160
    public function setRegion($regionCode)
161
    {
162
        if (!in_array($regionCode, ['na', 'eu', 'ap'])) {
163
            throw new ClientException('Invalid region code');
164
        }
165
        $this->baseUrl = 'https://api-' . $regionCode . '.hosted.exlibrisgroup.com/almaws/v1';
166
167
        return $this;
168
    }
169
170
    /**
171
     * @param string $url
172
     * @param array $query
173
     *
174
     * @return UriInterface
175
     */
176
    protected function getFullUri($url, $query=[])
177
    {
178
        $query['apikey'] = $this->key;
179
180
        return $this->uriFactory->createUri($this->baseUrl . $url)
181
            ->withQuery(http_build_query($query));
182
    }
183
184
    /**
185
     * Make a synchronous HTTP request and return a PSR7 response if successful.
186
     * In the case of intermittent errors (connection problem, 429 or 5XX error), the request is
187
     * attempted a maximum of {$this->maxAttempts} times with a sleep of {$this->sleepTimeOnRetry}
188
     * between each attempt to avoid hammering the server.
189
     *
190
     * @param RequestInterface $request
191
     * @param int $attempt
192
     *
193
     * @return ResponseInterface
194
     */
195
    public function request(RequestInterface $request, $attempt = 1)
196
    {
197
        if (!$this->key) {
198
            throw new ClientException('No API key defined for ' . $this->zone);
199
        }
200
201
        try {
202
            return $this->http->sendRequest($request);
203
        } catch (HttpException $e) {
204
            // Thrown for 400 level errors
205
206
            if ($e->getResponse()->getStatusCode() == '429') {
207
                // We've run into the "Max 25 API calls per institution per second" limit.
208
                // Wait a sec and retry, unless we've tried too many times already.
209
                if ($attempt > $this->maxAttempts) {
210
                    throw new MaxNumberOfAttemptsExhausted($e);
211
                }
212
                time_nanosleep(0, $this->sleepTimeOnRetry * 1000000000);
213
                return $this->request($request, $attempt + 1);
214
            }
215
216
            throw $e;
217
218
        } catch (NetworkException $e) {
219
            // Thrown in case of a networking error
220
221
            if ($attempt > $this->maxAttempts) {
222
                throw new MaxNumberOfAttemptsExhausted($e);
223
            }
224
            time_nanosleep(0, $this->sleepTimeOnRetry * 1000000000);
225
            return $this->request($request, $attempt + 1);
226
        }
227
    }
228
229
    /**
230
     * Make a GET request.
231
     *
232
     * @param string $url
233
     * @param array  $query
234
     * @param string $contentType
235
     *
236
     * @return string Response body
237
     */
238
    public function get($url, $query = [], $contentType = 'application/json')
239
    {
240
        $uri = $this->getFullUri($url, $query);
241
        $headers = [
242
            'Accept' => $contentType
243
        ];
244
        $request = $this->messageFactory->createRequest('GET', $uri, $headers);
245
        $response = $this->request($request);
246
247
        return strval($response->getBody());
248
    }
249
250
    /**
251
     * Make a GET request, accepting JSON.
252
     *
253
     * @param string $url
254
     * @param array  $query
255
     *
256
     * @return \stdClass JSON response as an object.
257
     */
258
    public function getJSON($url, $query = [])
259
    {
260
        $responseBody = $this->get($url, $query, 'application/json');
261
262
        return json_decode($responseBody);
263
    }
264
265
    /**
266
     * Make a GET request, accepting XML.
267
     *
268
     * @param string $url
269
     * @param array  $query
270
     *
271
     * @return QuiteSimpleXMLElement
272
     */
273
    public function getXML($url, $query = [])
274
    {
275
        $responseBody = $this->get($url, $query, 'application/xml');
276
277
        return new QuiteSimpleXMLElement($responseBody);
278
    }
279
280
    /**
281
     * Make a PUT request.
282
     *
283
     * @param string $url
284
     * @param $data
285
     * @param string $contentType
286
     *
287
     * @return bool
288
     */
289
    public function put($url, $data, $contentType = 'application/json')
290
    {
291
        $uri = $this->getFullUri($url);
292
        $headers = [];
293
        if (!is_null($contentType)) {
294
            $headers['Content-Type'] = $contentType;
295
            $headers['Accept'] = $contentType;
296
        }
297
        $request = $this->messageFactory->createRequest('PUT', $uri, $headers, $data);
298
        $response = $this->request($request);
299
300
        // Consider it a success if status code is 2XX
301
        return substr($response->getStatusCode(), 0, 1) == '2';
302
    }
303
304
    /**
305
     * Make a PUT request, sending JSON data.
306
     *
307
     * @param string $url
308
     * @param $data
309
     *
310
     * @return bool
311
     */
312
    public function putJSON($url, $data)
313
    {
314
        $data = json_encode($data);
315
316
        return $this->put($url, $data, 'application/json');
317
    }
318
319
    /**
320
     * Make a PUT request, sending XML data.
321
     *
322
     * @param string $url
323
     * @param $data
324
     *
325
     * @return bool
326
     */
327
    public function putXML($url, $data)
328
    {
329
        return $this->put($url, $data, 'application/xml');
330
    }
331
332
    /**
333
     * Get the redirect target location of an URL, or null if not a redirect.
334
     *
335
     * @param string $url
336
     * @param array  $query
337
     *
338
     * @return string|null
339
     */
340
    public function getRedirectLocation($url, $query = [])
341
    {
342
343
        $uri = $this->getFullUri($url, $query);
344
        $request = $this->messageFactory->createRequest('GET', $uri);
345
346
        try {
347
            $response = $this->request($request);
348
        } catch (ClientErrorException $e) {
349
            return null;
350
        }
351
352
        $locations = $response->getHeader('Location');
353
        return count($locations) ? $locations[0] : null;
354
    }
355
}
356