Completed
Push — master ( e41bcf...2a3c85 )
by Dan Michael O.
02:12
created

Client::getXML()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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