Test Failed
Push — master ( c6b6d3...528954 )
by Dan Michael O.
03:21
created

Client::setKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
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 as HttpClientErrorException;
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\NetworkException;
11
use Http\Client\HttpClient;
12
use Http\Discovery\HttpClientDiscovery;
13
use Http\Discovery\MessageFactoryDiscovery;
14
use Http\Discovery\UriFactoryDiscovery;
15
use Http\Message\MessageFactory;
16
use Http\Message\UriFactory;
17
use Psr\Http\Message\RequestInterface;
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\UriInterface;
20
use Scriptotek\Alma\Analytics\Analytics;
21
use Scriptotek\Alma\Bibs\Bibs;
22
use Scriptotek\Alma\Bibs\Items;
23
use Scriptotek\Alma\Conf\Conf;
24
use Scriptotek\Alma\Conf\Libraries;
25
use Scriptotek\Alma\Exception\ClientException as AlmaClientException;
26
use Scriptotek\Alma\Exception\InvalidApiKey;
27
use Scriptotek\Alma\Exception\MaxNumberOfAttemptsExhausted;
28
use Scriptotek\Alma\Exception\RequestFailed;
29
use Scriptotek\Alma\Exception\ResourceNotFound;
30
use Scriptotek\Alma\Exception\SruClientNotSetException;
31
use Scriptotek\Alma\Users\Users;
32
use Scriptotek\Sru\Client as SruClient;
33
34
/**
35
 * Alma client.
36
 */
37
class Client
38
{
39
    public $baseUrl;
40
41
    /** @var string Alma zone (institution or network) */
42
    public $zone;
43
44
    /** @var string Alma Developers Network API key for this zone */
45
    public $key;
46
47
    /** @var Client Network zone instance */
48
    public $nz;
49
50
    /** @var HttpClient */
51
    protected $http;
52
53
    /** @var MessageFactory */
54
    protected $messageFactory;
55
56
    /** @var UriFactory */
57
    protected $uriFactory;
58
59
    /** @var SruClient */
60
    public $sru;
61
62
    /** @var Bibs */
63
    public $bibs;
64
65
    /** @var Analytics */
66
    public $analytics;
67
68
    /** @var Users */
69
    public $users;
70
71
    /** @var Items */
72
    public  $items;
73
74
    /** @var int Max number of retries if we get 429 errors */
75
    public $maxAttempts = 10;
76
77
    /** @var float Number of seconds to sleep before retrying */
78
    public $sleepTimeOnRetry = 0.5;
79
80
    /**
81
     * Create a new client to connect to a given Alma instance.
82
     *
83
     * @param string         $key            API key
84
     * @param string         $region         Hosted region code, used to build base URL
85
     * @param string         $zone           Alma zone (Either Zones::INSTITUTION or Zones::NETWORK)
86
     * @param HttpClient     $http
87
     * @param MessageFactory $messageFactory
88
     * @param UriFactory     $uriFactory
89
     *
90
     * @throws \ErrorException
91
     */
92
    public function __construct(
93
        $key = null,
94
        $region = 'eu',
95
        $zone = Zones::INSTITUTION,
96
        HttpClient $http = null,
97
        MessageFactory $messageFactory = null,
98
        UriFactory $uriFactory = null
99
    ) {
100
        $this->http = new PluginClient(
101
            $http ?: HttpClientDiscovery::find(), [
102
                new ContentLengthPlugin(),
103
                new ErrorPlugin(),
104
            ]
105
        );
106
        $this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find();
107
        $this->uriFactory = $uriFactory ?: UriFactoryDiscovery::find();
108
109
        $this->key = $key;
110
        $this->setRegion($region);
111
112
        $this->zone = $zone;
113
114
        $this->bibs = new Bibs($this);
115
        $this->items = new Items($this); // Only needed for the fromBarcode method :/
116
117
        $this->analytics = new Analytics($this);
118
        $this->users = new Users($this);
119
120
        $this->conf = new Conf($this);
0 ignored issues
show
Bug introduced by
The property conf does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
121
        $this->libraries = $this->conf->libraries;  // shortcut
0 ignored issues
show
Bug introduced by
The property libraries does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Bug introduced by
The property libraries does not seem to exist in Scriptotek\Alma\Conf\Conf.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
122
123
        if ($zone == Zones::INSTITUTION) {
124
            $this->nz = new self(null, $region, Zones::NETWORK, $this->http, $this->messageFactory, $this->uriFactory);
125
        } elseif ($zone != Zones::NETWORK) {
126
            throw new AlmaClientException('Invalid zone name.');
127
        }
128
    }
129
130
    /**
131
     * Attach an SRU client (so you can search for Bib records).
132
     *
133
     * @param SruClient $sru
134
     */
135
    public function setSruClient(SruClient $sru)
136
    {
137
        $this->sru = $sru;
138
    }
139
140
    /**
141
     * Assert that an SRU client is connected. Throws SruClientNotSetException if not.
142
     *
143
     * @throws SruClientNotSetException
144
     */
145
    public function assertHasSruClient()
146
    {
147
        if (!isset($this->sru)) {
148
            throw new SruClientNotSetException();
149
        }
150
    }
151
152
    /**
153
     * Set the API key for this Alma instance.
154
     *
155
     * @param string $key The API key
156
     *
157
     * @return $this
158
     */
159
    public function setKey($key)
160
    {
161
        $this->key = $key;
162
163
        return $this;
164
    }
165
166
    /**
167
     * Set the Alma region code ('na' for North America, 'eu' for Europe, 'ap' for Asia Pacific).
168
     *
169
     * @param $regionCode
170
     *
171
     * @throws \ErrorException
172
     *
173
     * @return $this
174
     */
175
    public function setRegion($regionCode)
176
    {
177
        if (!in_array($regionCode, ['na', 'eu', 'ap'])) {
178
            throw new AlmaClientException('Invalid region code');
179
        }
180
        $this->baseUrl = 'https://api-' . $regionCode . '.hosted.exlibrisgroup.com/almaws/v1';
181
182
        return $this;
183
    }
184
185
    /**
186
     * @param string $url|UriInterface
0 ignored issues
show
Bug introduced by
There is no parameter named $url|UriInterface. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
187
     * @param array  $query
188
     *
189
     * @return UriInterface
190
     */
191
    public function buildUrl($url, $query = [])
192
    {
193
        if (!is_a($url, UriInterface::class)) {
194
            if (strpos($url, $this->baseUrl) === false) {
195
                $url = $this->baseUrl . $url;
196
            }
197
            $url = $this->uriFactory->createUri($url);
198
        }
199
200
        $query['apikey'] = $this->key;
201
202
        return $url->withQuery(http_build_query($query));
203
    }
204
205
    /**
206
     * Make a synchronous HTTP request and return a PSR7 response if successful.
207
     * In the case of intermittent errors (connection problem, 429 or 5XX error), the request is
208
     * attempted a maximum of {$this->maxAttempts} times with a sleep of {$this->sleepTimeOnRetry}
209
     * between each attempt to avoid hammering the server.
210
     *
211
     * @param RequestInterface $request
212
     * @param int              $attempt
213
     *
214
     * @return ResponseInterface
215
     */
216
    public function request(RequestInterface $request, $attempt = 1)
217
    {
218
        if (!$this->key) {
219
            throw new AlmaClientException('No API key defined for ' . $this->zone);
220
        }
221
222
        try {
223
            return $this->http->sendRequest($request);
224
        } catch (HttpClientErrorException $e) {
225
            // Thrown for 400 level errors
226
227
            if ($e->getResponse()->getStatusCode() == '429') {
228
                // We've run into the "Max 25 API calls per institution per second" limit.
229
                // Wait a sec and retry, unless we've tried too many times already.
230
                if ($attempt > $this->maxAttempts) {
231
                    throw new MaxNumberOfAttemptsExhausted(
232
                        'Rate limiting error - max number of retry attempts exhausted.',
233
                        0,
234
                        $e
235
                    );
236
                }
237
                time_nanosleep(0, $this->sleepTimeOnRetry * 1000000000);
238
                return $this->request($request, $attempt + 1);
239
            }
240
241
            // Throw exception for other 4XX errors
242
            throw $this->exceptionFromClientError($e);
243
244
        } catch (NetworkException $e) {
245
            // Thrown in case of a networking error
246
            // Wait a sec and retry, unless we've tried too many times already.
247
            if ($attempt > $this->maxAttempts) {
248
                throw new MaxNumberOfAttemptsExhausted(
249
                    'Network error - max number of retry attempts exhausted.',
250
                    0,
251
                    $e
252
                );
253
            }
254
            time_nanosleep(0, $this->sleepTimeOnRetry * 1000000000);
255
            return $this->request($request, $attempt + 1);
256
        }
257
    }
258
259
    /**
260
     * Make a GET request.
261
     *
262
     * @param string $url|UriInterface
0 ignored issues
show
Bug introduced by
There is no parameter named $url|UriInterface. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
263
     * @param array  $query
264
     * @param string $contentType
265
     *
266
     * @return string Response body
267
     */
268
    public function get($url, $query = [], $contentType = 'application/json')
269
    {
270
        $url = $this->buildUrl($url, $query);
271
        $headers = [
272
            'Accept' => $contentType,
273
        ];
274
        $request = $this->messageFactory->createRequest('GET', $url, $headers);
275
        $response = $this->request($request);
276
277
        return strval($response->getBody());
278
    }
279
280
    /**
281
     * Make a GET request, accepting JSON.
282
     *
283
     * @param string $url
284
     * @param array  $query
285
     *
286
     * @return \stdClass JSON response as an object.
287
     */
288
    public function getJSON($url, $query = [])
289
    {
290
        $responseBody = $this->get($url, $query, 'application/json');
291
292
        return json_decode($responseBody);
293
    }
294
295
    /**
296
     * Make a GET request, accepting XML.
297
     *
298
     * @param string $url
299
     * @param array  $query
300
     *
301
     * @return QuiteSimpleXMLElement
302
     */
303
    public function getXML($url, $query = [])
304
    {
305
        $responseBody = $this->get($url, $query, 'application/xml');
306
307
        return new QuiteSimpleXMLElement($responseBody);
308
    }
309
310
    /**
311
     * Make a PUT request.
312
     *
313
     * @param string $url
314
     * @param $data
315
     * @param string $contentType
316
     *
317
     * @return bool
318
     */
319 View Code Duplication
    public function put($url, $data, $contentType = 'application/json')
0 ignored issues
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...
320
    {
321
        $uri = $this->buildUrl($url);
322
        $headers = [];
323
        if (!is_null($contentType)) {
324
            $headers['Content-Type'] = $contentType;
325
            $headers['Accept'] = $contentType;
326
        }
327
        $request = $this->messageFactory->createRequest('PUT', $uri, $headers, $data);
328
        $response = $this->request($request);
329
330
        // Consider it a success if status code is 2XX
331
        return substr($response->getStatusCode(), 0, 1) == '2';
332
    }
333
334
    /**
335
     * Make a PUT request, sending JSON data.
336
     *
337
     * @param string $url
338
     * @param $data
339
     *
340
     * @return bool
341
     */
342
    public function putJSON($url, $data)
343
    {
344
        $data = json_encode($data);
345
346
        return $this->put($url, $data, 'application/json');
347
    }
348
349
    /**
350
     * Make a PUT request, sending XML data.
351
     *
352
     * @param string $url
353
     * @param $data
354
     *
355
     * @return bool
356
     */
357
    public function putXML($url, $data)
358
    {
359
        return $this->put($url, $data, 'application/xml');
360
    }
361
362
    /**
363
     * Make a POST request.
364
     *
365
     * @param string $url
366
     * @param $data
367
     * @param string $contentType
368
     *
369
     * @return bool
370
     */
371 View Code Duplication
    public function post($url, $data, $contentType = 'application/json')
0 ignored issues
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...
372
    {
373
        $uri = $this->buildUrl($url);
374
        $headers = [];
375
        if (!is_null($contentType)) {
376
            $headers['Content-Type'] = $contentType;
377
            $headers['Accept'] = $contentType;
378
        }
379
        $request = $this->messageFactory->createRequest('POST', $uri, $headers, $data);
380
        $response = $this->request($request);
381
382
        // Consider it a success if status code is 2XX
383
        return substr($response->getStatusCode(), 0, 1) == '2';
384
    }
385
386
    /**
387
     * Make a POST request, sending JSON data.
388
     *
389
     * @param string $url
390
     * @param $data
391
     *
392
     * @return bool
393
     */
394
    public function postJSON($url, $data = null)
395
    {
396
        $data = json_encode($data);
397
398
        return $this->post($url, $data, 'application/json');
399
    }
400
401
    /**
402
     * Make a POST request, sending XML data.
403
     *
404
     * @param string $url
405
     * @param $data
406
     *
407
     * @return bool
408
     */
409
    public function postXML($url, $data = null)
410
    {
411
        return $this->post($url, $data, 'application/xml');
412
    }
413
414
    /**
415
     * Get the redirect target location of an URL, or null if not a redirect.
416
     *
417
     * @param string $url
418
     * @param array  $query
419
     *
420
     * @return string|null
421
     */
422
    public function getRedirectLocation($url, $query = [])
423
    {
424
        $url = $this->buildUrl($url, $query);
425
426
        $request = $this->messageFactory->createRequest('GET', $url);
427
428
        try {
429
            $response = $this->request($request);
430
        } catch (ResourceNotFound $e) {
431
            return;
432
        }
433
434
        $locations = $response->getHeader('Location');
435
436
        return count($locations) ? $locations[0] : null;
437
    }
438
439
    /**
440
     * @param class $className
441
     * @param array ...$params
442
     * @return mixed
443
     */
444
    public function make($className, ...$params)
445
    {
446
        return new $className($this, ...$params);
447
    }
448
449
    /**
450
     * Generate a client exception.
451
     *
452
     * @param HttpClientErrorException $exception
453
     * @return RequestFailed
454
     */
455
    protected function exceptionFromClientError(HttpClientErrorException $exception)
456
    {
457
        $responseBody = $exception->getResponse()->getBody();
458
459
        if (strtolower($responseBody) == 'invalid api key') {
460
            return new InvalidApiKey($responseBody, 0, $exception);
461
        }
462
        $res = json_decode($responseBody, true);
463
        $message = $res['errorList']['error'][0]['errorMessage'] ?? 'Bad request';
464
        $code = $res['errorList']['error'][0]['errorCode'] ?? 0;
465
466
        if (preg_match('/no items? found/i', $message) !== false) {
467
            return new ResourceNotFound($message, $code, $exception);
468
        }
469
470
        return new RequestFailed($message, $code, $exception);
471
    }
472
}
473