1: <?php
2:
3: namespace Scopus;
4:
5: use Exception;
6: use GuzzleHttp\ClientInterface;
7: use GuzzleHttp\Exception\GuzzleException;
8: use Scopus\Exception\JsonException;
9: use Scopus\Exception\XmlException;
10: use Scopus\Response\AbstractCitations;
11: use Scopus\Response\Abstracts;
12: use Scopus\Response\Author;
13: use Scopus\Response\CitationCount;
14: use Scopus\Response\Entry;
15: use Scopus\Response\SearchResults;
16: use Scopus\Util\XmlUtil;
17:
18: class ScopusApi
19: {
20: const SEARCH_URI = 'https://api.elsevier.com/content/search/scopus';
21: const ABSTRACT_URI = 'https://api.elsevier.com/content/abstract/scopus_id/';
22: const AUTHOR_URI = 'https://api.elsevier.com/content/author/author_id/';
23: const AFFILIATION_URI = 'https://api.elsevier.com/content/affiliation/affiliation_id/';
24: const SEARCH_AUTHOR_URI = 'https://api.elsevier.com/content/search/author';
25: const CITATION_OVERVIEW_URI = 'https://api.elsevier.com/content/abstract/citations';
26: const CITATION_COUNT_URI = 'https://api.elsevier.com/content/abstract/citation-count';
27:
28: private $client;
29:
30: public function __construct(ClientInterface $httpClient)
31: {
32: $this->client = $httpClient;
33: }
34:
35: /**
36: * @return SearchQuery
37: */
38: public function query($query)
39: {
40: return new SearchQuery($this, $query);
41: }
42:
43: /**
44: * @param string $uri
45: * @param array $options
46: *
47: * @return array|Abstracts|Author|SearchResults|AbstractCitations|CitationCount[]
48: *
49: * @throws Exception|GuzzleException
50: */
51: public function retrieve($uri, array $options = [])
52: {
53: $response = $this->client->get($uri, $options);
54:
55: if ($response->getStatusCode() === 200) {
56: $body = $response->getBody();
57: $contentType = $response->getHeader('Content-Type');
58: if ($contentType && strpos(strtolower($contentType[0]), '/xml') !== false) {
59: $xml = simplexml_load_string($body, "SimpleXMLElement", LIBXML_NOCDATA);
60: if ($xml === false) {
61: $error = libxml_get_last_error();
62: throw new XmlException(sprintf('Xml response could not be parsed "%s" (%d) for %s', $error->message, $error->code, $uri), $error->code);
63: }
64: $body = json_encode(XmlUtil::toArray($xml));
65: }
66: $json = json_decode($body, true);
67: if (!is_array($json)) {
68: $message = json_last_error_msg();
69: $error = json_last_error();
70: throw new JsonException(sprintf('Json response could not be decoded "%s" (%d) for "%s"', $message, $error, $uri), $error);
71: }
72:
73: $type = key($json);
74: switch ($type) {
75: case 'search-results':
76: return new SearchResults($json['search-results']);
77: case 'abstracts-retrieval-response':
78: return new Abstracts($json['abstracts-retrieval-response']);
79: case 'abstracts-retrieval-multidoc-response':
80: return array_map(function ($data) {
81: return new Abstracts($data);
82: }, $json['abstracts-retrieval-multidoc-response']['abstracts-retrieval-response']);
83: case 'author-retrieval-response':
84: return new Author($json['author-retrieval-response'][0]);
85: case 'author-retrieval-response-list':
86: return array_map(function ($data) {
87: if ($data['@status'] === 'found') {
88: return new Author($data);
89: }
90: }, $json['author-retrieval-response-list']['author-retrieval-response']);
91: case 'abstract-citations-response':
92: return new AbstractCitations($json['abstract-citations-response']);
93: case 'citation-count-response':
94: $document = $json['citation-count-response']['document'];
95:
96: return array_map(function ($data) {
97: return new CitationCount($data);
98: }, isset($document['@status']) ? [$document] : $document);
99: default:
100: throw new Exception(sprintf('Unsupported response type: "%s" for "%s"', $type, $uri));
101: }
102: }
103: }
104:
105: /**
106: * https://dev.elsevier.com/documentation/ScopusSearchAPI.wadl
107: * @param array $query
108: * @return SearchResults
109: */
110: public function search(array $query)
111: {
112: return $this->retrieve(self::SEARCH_URI, [
113: 'query' => $query,
114: ]);
115: }
116:
117: /**
118: * I look for authors by name, surname or affiliation
119: * with https://dev.elsevier.com/documentation/AuthorSearchAPI.wadl
120: *
121: * @param string|null $lastName last name of author to look for
122: * @param string|null $firstName first name of author to look for
123: * @param string|null $affiliation affiliation of author to look for
124: *
125: * @param array $options -> https://dev.elsevier.com/tips/AuthorSearchTips.htm
126: *
127: * @return SearchResults use getEntries() method for get the array of author format -> https://dev.elsevier.com/guides/AuthorSearchViews.htm
128: */
129: public function searchAuthors(string $lastName = null, string $firstName = null, string $affiliation = null, array $options = [])
130: {
131: if (empty($lastName) && empty($firstName) && empty($affiliation)) return null;
132:
133: $query = (!empty($lastName)) ? 'authlast("' . $lastName . '")' : "";
134: if (!empty($firstName)) {
135: $query .= (empty($query)) ? "" : " and ";
136: $query .= 'authfirst("' . $firstName . '")';
137: }
138: if (!empty($affiliation)) {
139: $query .= (empty($query)) ? "" : " and ";
140: $query .= 'affil("' . $affiliation . '")';
141: }
142:
143: $query = array_merge($options, $this->query($query)->toArray());
144: return $this->retrieve(self::SEARCH_AUTHOR_URI, [
145: 'query' => $query,
146: ]);
147: }
148:
149: /**
150: * I recover the citations on a specific document
151: * https://dev.elsevier.com/documentation/AbstractCitationAPI.wadl
152: *
153: * I can set a range of years to show: startYear - endYear
154: *
155: * @param array/String $documentId Scopus_id of the document or array of the document Scopus_id
156: * @param string|null $startYear Start year range
157: * @param string|null $endYear End of range year
158: * @param array $options -> https://dev.elsevier.com/documentation/AbstractCitationAPI.wadl#simple
159: *
160: * @return AbstractCitations[] Return all quotes over the years grouped by 25 documents.
161: *
162: * Call the getCompactInfo() method, in the single instance, to retrieve the document citations in details (max 25 for instance),
163: * Call the getTotalCompactInfo() method, in the single instance, to retrieve all documents citations (max 25 for instance)
164: */
165: public function overviewCitation($documentId, string $startYear = null, string $endYear = null, array $options = [])
166: {
167: if ($startYear && $endYear) $options["date"] = $startYear . "-" . $endYear;
168: if (!is_array($documentId)) $documentId = [$documentId];
169:
170: $responses = [];
171: $pieces = array_chunk($documentId, 25);
172: foreach ($pieces as $piece) {
173: $options["scopus_id"] = "(" . implode(",", $piece) . ")";
174: array_push($responses, $this->retrieve(self::CITATION_OVERVIEW_URI, [
175: 'query' => $options,
176: ]));
177: }
178: return $responses;
179: }
180:
181: /**
182: * @param string|string[] $scopusId
183: * @param array $options
184: *
185: * @return CitationCount[]
186: *
187: * @throws Exception
188: */
189: public function retrieveCitationCount($scopusId, array $options = [])
190: {
191: if (is_array($scopusId)) {
192: $scopusId = implode(',', $scopusId);
193: }
194:
195: if (count(explode(',', $scopusId)) > 25) {
196: throw new Exception("The maximum number of 25 document id's exceeded!");
197: }
198:
199: $options['scopus_id'] = $scopusId;
200:
201: return $this->retrieve(self::CITATION_COUNT_URI, [
202: 'query' => $options
203: ]);
204: }
205:
206: /**
207: * @param $scopusId
208: * @param array $options
209: * @return Abstracts|Abstracts[]
210: * @throws Exception
211: */
212: public function retrieveAbstract($scopusId, array $options = [])
213: {
214: if (is_array($scopusId)) {
215: $scopusId = implode(',', $scopusId);
216: }
217: if (count(explode(',', $scopusId)) > 25) {
218: throw new Exception("The maximum number of 25 abstract id's exceeded!");
219: }
220: return $this->retrieve(self::ABSTRACT_URI . $scopusId, [
221: 'query' => $options
222: ]);
223: }
224:
225: /**
226: * @param $scopusIds
227: * @param array $options
228: * @return Abstracts[]
229: */
230: public function retrieveAbstracts($scopusIds, array $options = [])
231: {
232: $scopusIds = array_unique($scopusIds);
233:
234: if (count($scopusIds) > 1) {
235: $chunks = array_chunk($scopusIds, 25);
236: $abstracts = [];
237: foreach ($chunks as $chunk) {
238: $abstracts = array_merge($abstracts, array_combine($chunk, $this->retrieveAbstract($chunk, $options)));
239: }
240: return $abstracts;
241: } else {
242: try {
243: return [
244: $scopusIds[0] => $this->retrieveAbstract($scopusIds[0], $options),
245: ];
246: } catch (Exception $e) {
247: return [];
248: }
249: }
250: }
251:
252: /**
253: * Retrieve an author with
254: * https://dev.elsevier.com/documentation/AuthorRetrievalAPI.wadl
255: * @param $authorId author id
256: * @param array $options -> ['view'=>'ENHANCED'] https://dev.elsevier.com/guides/AuthorRetrievalViews.htm
257: * @return Author|Author[] an Author returns
258: * @throws Exception
259: */
260: public function retrieveAuthor($authorId, array $options = [])
261: {
262: if (is_array($authorId)) {
263: $authorId = implode(',', $authorId);
264: }
265: if (count(explode(',', $authorId)) > 25) {
266: throw new Exception("The maximum number of 25 author id's exceeded!");
267: }
268: return $this->retrieve(self::AUTHOR_URI . $authorId, [
269: 'query' => $options
270: ]);
271: }
272:
273: /**
274: * @param $authorIds
275: * @param array $options
276: * @return Author[]
277: */
278: public function retrieveAuthors($authorIds, array $options = [])
279: {
280: $scopusIds = array_unique($authorIds);
281: if (count($scopusIds) > 1) {
282: $chunks = array_chunk($authorIds, 25);
283: $authors = [];
284: foreach ($chunks as $chunk) {
285: $authors = array_merge($authors, array_combine($chunk, $this->retrieveAuthor($chunk, $options)));
286: }
287: return $authors;
288: } else {
289: try {
290: return [
291: $authorIds[0] => $this->retrieveAuthor($authorIds[0], $options),
292: ];
293: } catch (Exception $e) {
294: return [];
295: }
296: }
297: }
298:
299: public function retrieveAffiliation($affiliationId, array $options = [])
300: {
301: return $this->retrieve(self::AFFILIATION_URI . $affiliationId, $options);
302: }
303:
304: /**
305: * I recover the documents of a specific Author with
306: * https://dev.elsevier.com/documentation/ScopusSearchAPI.wadl
307: * cursor next = prendo i successivi 25
308: *
309: * I can set a search range: startYear < AnnoDocumento < endYear
310: * ! Warning: do not startYear <= Document Year <= endYear
311: *
312: * @param string $authorId AUTHOR_ID
313: * @param string|null $startYear Start year range
314: * @param string|null $endYear End of range year
315: * @param bool $jrDocument If I only want items of type j or r
316: *
317: * @return Entry[] Return all articles by the selected author https://dev.elsevier.com/guides/ScopusSearchViews.htm
318: */
319: public function retrieveDocumentsAuthor(string $authorId, string $startYear = null, string $endYear = null, bool $jrDocument = false)
320: {
321: //Query parameters -> https://dev.elsevier.com/tips/ScopusSearchTips.htm
322: $query = "au-id(" . $authorId . ")";
323: if ($startYear != null && $endYear != null) $query .= " and PUBYEAR > $startYear AND PUBYEAR < $endYear";
324: if ($jrDocument) $query .= " and SRCTYPE(r OR j)";
325:
326: $searchResults = $this->query($query)->withCursor()->viewComplete()->search();
327: $documents = $searchResults->getEntries(); //Entries[]
328:
329: $numDocument = $searchResults->getTotalResults() - $searchResults->countEntries();
330: while ($numDocument > 0) { //recover all documents
331: $cursor = $searchResults->getNextCursor();
332: $searchResults = $this->query($query)->setCursor($cursor)->viewComplete()->search();
333: $documents = array_merge($documents, $searchResults->getEntries());
334: $numDocument -= $searchResults->countEntries();
335: }
336: return $documents;
337: }
338: }
339: