Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ObalkyKnihService
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 8
1482
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 getHttpClient
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 createCacheKey
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getData
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getFromService
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
182
 createLocalIdentifier
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getBaseUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getAliveUrl
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3/**
4 * Service class for ObalkyKnih
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Moravian Library 2019.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Content
25 * @author   Josef Moravec <moravec@mzk.cz>
26 * @license  https://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFind\Content;
31
32use function count;
33
34/**
35 * Service class for ObalkyKnih
36 *
37 * @category VuFind
38 * @package  Content
39 * @author   Josef Moravec <moravec@mzk.cz>
40 * @license  https://opensource.org/licenses/gpl-2.0.php GNU General Public License
41 * @link     https://vufind.org/wiki/development Wiki
42 */
43class ObalkyKnihService implements
44    \VuFindHttp\HttpServiceAwareInterface,
45    \Laminas\Log\LoggerAwareInterface
46{
47    use \VuFindHttp\HttpServiceAwareTrait;
48    use \VuFind\Cache\CacheTrait;
49    use \VuFind\Log\LoggerAwareTrait;
50
51    /**
52     * Available base URLs
53     *
54     * @var array
55     */
56    protected $baseUrls = [];
57
58    /**
59     * Http referrer
60     *
61     * @var string
62     */
63    protected $referrer;
64
65    /**
66     * Sigla - library identifier
67     *
68     * @var string
69     */
70    protected $sigla;
71
72    /**
73     * Array with endpoints, possible endpoints(array keys) are: books, cover, toc,
74     * authority, citation, recommend, alive
75     *
76     * @var array
77     */
78    protected $endpoints;
79
80    /**
81     * Whether to check servers availability before API calls
82     *
83     * @var bool
84     */
85    protected $checkServersAvailability = false;
86
87    /**
88     * Constructor
89     *
90     * @param \Laminas\Config\Config $config Configuration for service
91     */
92    public function __construct(\Laminas\Config\Config $config)
93    {
94        if (
95            !isset($config->base_url) || count($config->base_url) < 1
96            || !isset($config->books_endpoint)
97        ) {
98            throw new \Exception(
99                'Configuration for ObalkyKnih.cz service is not valid'
100            );
101        }
102        $this->baseUrls = $config->base_url;
103        $this->cacheLifetime = 1800;
104        $this->referrer = $config->referrer ?? null;
105        $this->sigla = $config->sigla ?? null;
106        foreach ($config->toArray() as $configItem => $configValue) {
107            $parts = explode('_', $configItem);
108            if ($parts[1] ?? '' === 'endpoint') {
109                $this->endpoints[$parts[0]] = $configValue;
110            }
111        }
112        $this->checkServersAvailability
113            = $config->checkServersAvailability ?? false;
114    }
115
116    /**
117     * Get an HTTP client
118     *
119     * @param string $url URL for client to use
120     *
121     * @return \Laminas\Http\Client
122     */
123    protected function getHttpClient(string $url = null)
124    {
125        if (null === $this->httpService) {
126            throw new \Exception('HTTP service missing.');
127        }
128        $client = $this->httpService->createClient($url);
129        if (isset($this->referrer)) {
130            $client->getRequest()->getHeaders()
131                ->addHeaderLine('Referer', $this->referrer);
132        }
133        return $client;
134    }
135
136    /**
137     * Creates cache key based on ids
138     *
139     * @param array $ids Record identifiers
140     *
141     * @return string
142     */
143    protected function createCacheKey(array $ids)
144    {
145        $key = $ids['recordid'] ?? '';
146        $key = !empty($key) ? $key
147            : (isset($ids['isbn']) ? $ids['isbn']->get13() : null);
148        $key = !empty($key) ? $key : sha1(json_encode($ids));
149        return $key;
150    }
151
152    /**
153     * Get data from cache, or from service
154     *
155     * @param array $ids Record identifiers
156     *
157     * @return \stdClass|null
158     */
159    public function getData(array $ids): ?\stdClass
160    {
161        $cacheKey = $this->createCacheKey($ids);
162        $cachedData = $this->getCachedData($cacheKey);
163        if ($cachedData === null) {
164            $cachedData = $this->getFromService($ids);
165            $this->putCachedData($cacheKey, $cachedData);
166        }
167        return $cachedData;
168    }
169
170    /**
171     * Get data from service
172     *
173     * @param array $ids Record identifiers
174     *
175     * @return \stdClass|null
176     * @throws \Exception
177     *
178     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
179     */
180    protected function getFromService(array $ids): ?\stdClass
181    {
182        $param = 'multi';
183        $query = [];
184        $isbn = null;
185        if (!empty($ids['isbns'])) {
186            $isbn = array_map(
187                function ($isbn) {
188                    return $isbn->get13();
189                },
190                $ids['isbns']
191            );
192        } elseif (!empty($ids['isbn'])) {
193            $isbn = $ids['isbn']->get13();
194        }
195        $isbn ??= $ids['upc'] ?? $ids['issn'] ?? null;
196        $oclc = $ids['oclc'] ?? null;
197        $isbn = $isbn ?? (isset($ids['ismn']) ? $ids['ismn']->get13() : null);
198        $ismn = isset($ids['ismn']) ? $ids['ismn']->get10() : null;
199        $nbn = $ids['nbn'] ?? $this->createLocalIdentifier($ids['recordid'] ?? '');
200        $uuid = null;
201        if (isset($ids['uuid'])) {
202            $uuid = str_starts_with($ids['uuid'], 'uuid:')
203                ? $ids['uuid']
204                : ('uuid:' . $ids['uuid']);
205        }
206        foreach (['isbn', 'oclc', 'ismn', 'nbn', 'uuid'] as $identifier) {
207            if (isset($$identifier)) {
208                $query[$identifier] = $$identifier;
209            }
210        }
211
212        $url = $this->getBaseUrl();
213        if ($url === '') {
214            $this->logWarning('All ObalkyKnih servers are down.');
215            return null;
216        }
217        $url .= $this->endpoints['books'] . '?';
218        $url .= http_build_query([$param => json_encode([$query])]);
219        $client = $this->getHttpClient($url);
220        try {
221            $response = $client->send();
222        } catch (\Exception $e) {
223            $this->logError('Unexpected ' . $e::class . ': ' . $e->getMessage());
224            return null;
225        }
226        if ($response->isSuccess()) {
227            $json = json_decode($response->getBody());
228            return empty($json) ? null : $json[0];
229        }
230        return null;
231    }
232
233    /**
234     * Create identifier of local record
235     *
236     * @param string $recordid Record identifier
237     *
238     * @return string|null
239     */
240    protected function createLocalIdentifier(string $recordid): ?string
241    {
242        if (str_contains($recordid, '.')) {
243            [, $recordid] = explode('.', $recordid, 2);
244        }
245        return (empty($this->sigla) || empty($recordid)) ? null :
246            $this->sigla . '-' . str_replace('-', '', $recordid);
247    }
248
249    /**
250     * Get currently available base URL
251     *
252     * @return string
253     */
254    protected function getBaseUrl(): string
255    {
256        return $this->checkServersAvailability
257            ? $this->getAliveUrl() : $this->baseUrls[0];
258    }
259
260    /**
261     * Check base URLs and return the first available
262     *
263     * @return string
264     */
265    protected function getAliveUrl(): string
266    {
267        $aliveUrl = $this->getCachedData('baseUrl');
268        if ($aliveUrl !== null) {
269            return $aliveUrl;
270        }
271        foreach ($this->baseUrls as $baseUrl) {
272            $url = $baseUrl . $this->endpoints['alive'];
273            $client = $this->getHttpClient($url);
274            $client->setOptions(['timeout' => 2]);
275            $response = $client->send();
276            if ($response->isSuccess()) {
277                $this->putCachedData('baseUrl', $baseUrl, 60);
278                return $baseUrl;
279            }
280        }
281        return '';
282    }
283}