commit
fa8220f639
10 changed files with 945 additions and 0 deletions
@ -0,0 +1,279 @@ |
|||||||
|
# RapidILL Request |
||||||
|
|
||||||
|
A PHP library for creating InterLibrary Loan (ILL) requests using the [RapidILL](https://exlibrisgroup.com/products/rapidill-interlibrary-loan/) API `InsertRequest` method. |
||||||
|
|
||||||
|
Built on PHP's native `SoapClient` with no framework dependencies, making it easy to use in any PHP project including Drupal 10. |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
- PHP 8.1 or higher |
||||||
|
- PHP SOAP extension (`ext-soap`) |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
### Composer |
||||||
|
|
||||||
|
```bash |
||||||
|
composer require rapidill/request |
||||||
|
``` |
||||||
|
|
||||||
|
### Drupal 10 |
||||||
|
|
||||||
|
Add the package to your Drupal project's `composer.json` as a path repository if installing from a local copy: |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"repositories": [ |
||||||
|
{ |
||||||
|
"type": "path", |
||||||
|
"url": "/path/to/rapidILL-request" |
||||||
|
} |
||||||
|
], |
||||||
|
"require": { |
||||||
|
"rapidill/request": "*" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Then run: |
||||||
|
|
||||||
|
```bash |
||||||
|
composer require rapidill/request |
||||||
|
``` |
||||||
|
|
||||||
|
## Quick Start |
||||||
|
|
||||||
|
```php |
||||||
|
use RapidIll\RapidIllClient; |
||||||
|
use RapidIll\InsertRequest; |
||||||
|
use RapidIll\RequestType; |
||||||
|
|
||||||
|
// Create a client with your RapidILL credentials. |
||||||
|
$client = new RapidIllClient( |
||||||
|
username: 'your_username', |
||||||
|
password: 'your_password', |
||||||
|
rapidCode: 'YOUR_RAPID_CODE', |
||||||
|
branchName: 'Main', |
||||||
|
); |
||||||
|
|
||||||
|
// Build and submit a request. |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::Article) |
||||||
|
->setJournalTitle('Nature') |
||||||
|
->setArticleTitle('A breakthrough in quantum computing') |
||||||
|
->setArticleAuthor('Smith, J.') |
||||||
|
->addIssn('0028-0836'); |
||||||
|
|
||||||
|
$response = $client->insertRequest($request); |
||||||
|
|
||||||
|
if ($response->isSuccessful) { |
||||||
|
echo "Request submitted. Rapid ID: {$response->rapidRequestId}\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
### Request a journal article |
||||||
|
|
||||||
|
```php |
||||||
|
use RapidIll\RapidIllClient; |
||||||
|
use RapidIll\InsertRequest; |
||||||
|
use RapidIll\RequestType; |
||||||
|
|
||||||
|
$client = new RapidIllClient( |
||||||
|
username: 'your_username', |
||||||
|
password: 'your_password', |
||||||
|
rapidCode: 'YOUR_RAPID_CODE', |
||||||
|
branchName: 'Main', |
||||||
|
); |
||||||
|
|
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::Article) |
||||||
|
->setJournalTitle('Nature') |
||||||
|
->setJournalYear('2024') |
||||||
|
->setJournalVolume('625') |
||||||
|
->setJournalIssue('7994') |
||||||
|
->setJournalMonth('January') |
||||||
|
->setArticleTitle('A breakthrough in quantum computing') |
||||||
|
->setArticleAuthor('Smith, J.') |
||||||
|
->setArticlePages('100-105') |
||||||
|
->addIssn('0028-0836') |
||||||
|
->addIssn('1476-4687') |
||||||
|
->setOclcNumber('1234567') |
||||||
|
->setPatronName('Jane Doe') |
||||||
|
->setPatronEmail('jane.doe@university.edu') |
||||||
|
->setPatronDepartment('Physics') |
||||||
|
->setXrefRequestId('ILL-2024-00042'); |
||||||
|
|
||||||
|
$response = $client->insertRequest($request); |
||||||
|
|
||||||
|
if ($response->isSuccessful) { |
||||||
|
echo "Request ID: {$response->rapidRequestId}\n"; |
||||||
|
echo "Match found: " . ($response->foundMatch ? 'yes' : 'no') . "\n"; |
||||||
|
echo "Available holdings: {$response->numberOfAvailableHoldings}\n"; |
||||||
|
} else { |
||||||
|
echo "Request failed: {$response->verificationNote}\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Request a book |
||||||
|
|
||||||
|
```php |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::Book) |
||||||
|
->setJournalTitle('Introduction to Algorithms') |
||||||
|
->setArticleAuthor('Cormen, Thomas H.') |
||||||
|
->addIsbn('978-0262033848') |
||||||
|
->setPublisher('MIT Press') |
||||||
|
->setEdition('3rd') |
||||||
|
->setPatronName('John Smith') |
||||||
|
->setPatronEmail('john.smith@university.edu'); |
||||||
|
|
||||||
|
$response = $client->insertRequest($request); |
||||||
|
``` |
||||||
|
|
||||||
|
### Request a book chapter |
||||||
|
|
||||||
|
```php |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::BookChapter) |
||||||
|
->setJournalTitle('The Oxford Handbook of Innovation') |
||||||
|
->setArticleTitle('Open Innovation') |
||||||
|
->setArticleAuthor('Chesbrough, Henry') |
||||||
|
->setArticlePages('191-213') |
||||||
|
->addIsbn('978-0199286805') |
||||||
|
->setPatronName('Alice Johnson') |
||||||
|
->setPatronEmail('alice@university.edu'); |
||||||
|
|
||||||
|
$response = $client->insertRequest($request); |
||||||
|
``` |
||||||
|
|
||||||
|
### Check holdings only (without submitting a request) |
||||||
|
|
||||||
|
```php |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::Article) |
||||||
|
->setHoldingsCheckOnly(true) |
||||||
|
->setJournalTitle('Science') |
||||||
|
->addIssn('0036-8075'); |
||||||
|
|
||||||
|
$response = $client->insertRequest($request); |
||||||
|
|
||||||
|
if ($response->isLocalHolding) { |
||||||
|
echo "Item is available locally:\n"; |
||||||
|
foreach ($response->localHoldings as $holding) { |
||||||
|
echo " Branch: {$holding['branchName']}\n"; |
||||||
|
echo " Location: {$holding['location']}\n"; |
||||||
|
echo " Call Number: {$holding['callNumber']}\n"; |
||||||
|
} |
||||||
|
} else { |
||||||
|
echo "Not held locally. {$response->numberOfAvailableHoldings} remote holdings available.\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Error handling |
||||||
|
|
||||||
|
```php |
||||||
|
use RapidIll\RapidIllException; |
||||||
|
|
||||||
|
try { |
||||||
|
$response = $client->insertRequest($request); |
||||||
|
} catch (RapidIllException $e) { |
||||||
|
// SOAP or connection error. |
||||||
|
error_log('RapidILL error: ' . $e->getMessage()); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Debugging SOAP requests |
||||||
|
|
||||||
|
The client records the raw SOAP XML when `trace` is enabled (the default): |
||||||
|
|
||||||
|
```php |
||||||
|
$response = $client->insertRequest($request); |
||||||
|
|
||||||
|
// Inspect the XML that was sent and received. |
||||||
|
echo $client->getLastRequest(); |
||||||
|
echo $client->getLastResponse(); |
||||||
|
``` |
||||||
|
|
||||||
|
## API Reference |
||||||
|
|
||||||
|
### `RapidIllClient` |
||||||
|
|
||||||
|
| Constructor Parameter | Type | Required | Description | |
||||||
|
|---|---|---|---| |
||||||
|
| `username` | string | yes | RapidILL API username | |
||||||
|
| `password` | string | yes | RapidILL API password | |
||||||
|
| `rapidCode` | string | yes | Your library's Rapid code | |
||||||
|
| `branchName` | string | yes | Your library branch name | |
||||||
|
| `wsdlUrl` | string | no | Override the default WSDL URL | |
||||||
|
| `soapOptions` | array | no | Additional options passed to `SoapClient` | |
||||||
|
|
||||||
|
### `InsertRequest` |
||||||
|
|
||||||
|
All setters return `$this` for fluent chaining. |
||||||
|
|
||||||
|
| Method | Description | |
||||||
|
|---|---| |
||||||
|
| `setRequestType(RequestType)` | `Article`, `Book`, or `BookChapter` (default: `Article`) | |
||||||
|
| `setHoldingsCheckOnly(bool)` | If true, only checks holdings without submitting | |
||||||
|
| `setBlockLocalOnly(bool)` | Block request if only local holdings exist | |
||||||
|
| `setInsertLocalHolding(bool)` | Insert a local holding record | |
||||||
|
| `addIssn(string)` | Add an ISSN | |
||||||
|
| `setIssns(array)` | Set all ISSNs at once | |
||||||
|
| `addIsbn(string)` | Add an ISBN | |
||||||
|
| `setIsbns(array)` | Set all ISBNs at once | |
||||||
|
| `addLccn(string)` | Add an LCCN | |
||||||
|
| `setLccns(array)` | Set all LCCNs at once | |
||||||
|
| `setOclcNumber(string)` | OCLC number | |
||||||
|
| `setJournalTitle(string)` | Journal or book title | |
||||||
|
| `setJournalYear(string)` | Publication year | |
||||||
|
| `setJournalVolume(string)` | Volume number | |
||||||
|
| `setJournalIssue(string)` | Issue number | |
||||||
|
| `setJournalMonth(string)` | Publication month | |
||||||
|
| `setArticleTitle(string)` | Article or chapter title | |
||||||
|
| `setArticleAuthor(string)` | Author name | |
||||||
|
| `setArticlePages(string)` | Page range | |
||||||
|
| `setEdition(string)` | Edition | |
||||||
|
| `setPublisher(string)` | Publisher | |
||||||
|
| `setPatronId(string)` | Patron identifier | |
||||||
|
| `setPatronName(string)` | Patron full name | |
||||||
|
| `setPatronDepartment(string)` | Patron department | |
||||||
|
| `setPatronEmail(string)` | Patron email address | |
||||||
|
| `setPatronPhone(string)` | Patron phone number | |
||||||
|
| `setPatronNotes(string)` | Additional notes | |
||||||
|
| `setXrefRequestId(string)` | Cross-reference ID from your ILL system | |
||||||
|
|
||||||
|
### `InsertResponse` |
||||||
|
|
||||||
|
All properties are `readonly`. |
||||||
|
|
||||||
|
| Property | Type | Description | |
||||||
|
|---|---|---| |
||||||
|
| `isSuccessful` | bool | Whether the API call succeeded | |
||||||
|
| `foundMatch` | bool | Whether a lending match was found | |
||||||
|
| `rapidRequestId` | int | The assigned Rapid request ID | |
||||||
|
| `numberOfAvailableHoldings` | int | Count of available holdings | |
||||||
|
| `isLocalHolding` | bool | Whether the item is held locally | |
||||||
|
| `verificationNote` | ?string | Error or informational message | |
||||||
|
| `matchingStandardNumber` | ?string | The matched ISSN/ISBN | |
||||||
|
| `matchingStandardNumberType` | ?string | Type of the matched number | |
||||||
|
| `duplicateRequestId` | ?string | ID if request is a duplicate | |
||||||
|
| `localHoldings` | array | Array of local holding records | |
||||||
|
|
||||||
|
### `RequestType` (enum) |
||||||
|
|
||||||
|
- `RequestType::Article` |
||||||
|
- `RequestType::Book` |
||||||
|
- `RequestType::BookChapter` |
||||||
|
|
||||||
|
## Testing |
||||||
|
|
||||||
|
```bash |
||||||
|
composer install |
||||||
|
vendor/bin/phpunit |
||||||
|
``` |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
GPL-2.0-or-later |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
{ |
||||||
|
"name": "rapidill/request", |
||||||
|
"description": "PHP library for creating InterLibrary Loan requests via the RapidILL API", |
||||||
|
"type": "library", |
||||||
|
"license": "GPL-2.0-or-later", |
||||||
|
"minimum-stability": "stable", |
||||||
|
"require": { |
||||||
|
"php": ">=8.1", |
||||||
|
"ext-soap": "*" |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"phpunit/phpunit": "^10.0" |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"RapidIll\\": "src/" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload-dev": { |
||||||
|
"psr-4": { |
||||||
|
"RapidIll\\Tests\\": "tests/" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" |
||||||
|
bootstrap="vendor/autoload.php" |
||||||
|
colors="true"> |
||||||
|
<testsuites> |
||||||
|
<testsuite name="default"> |
||||||
|
<directory>tests</directory> |
||||||
|
</testsuite> |
||||||
|
</testsuites> |
||||||
|
</phpunit> |
||||||
@ -0,0 +1,266 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace RapidIll; |
||||||
|
|
||||||
|
/** |
||||||
|
* Represents the data for a RapidILL InsertRequest API call. |
||||||
|
* |
||||||
|
* Build a request using the fluent setter methods, then pass it to |
||||||
|
* RapidIllClient::insertRequest(). |
||||||
|
*/ |
||||||
|
class InsertRequest |
||||||
|
{ |
||||||
|
private RequestType $requestType = RequestType::Article; |
||||||
|
private bool $holdingsCheckOnly = false; |
||||||
|
private bool $blockLocalOnly = false; |
||||||
|
private bool $insertLocalHolding = false; |
||||||
|
|
||||||
|
// Identifiers. |
||||||
|
private array $issns = []; |
||||||
|
private array $isbns = []; |
||||||
|
private array $lccns = []; |
||||||
|
private ?string $oclcNumber = null; |
||||||
|
|
||||||
|
// Bibliographic fields. |
||||||
|
private ?string $journalTitle = null; |
||||||
|
private ?string $journalYear = null; |
||||||
|
private ?string $journalVolume = null; |
||||||
|
private ?string $journalIssue = null; |
||||||
|
private ?string $journalMonth = null; |
||||||
|
private ?string $articleTitle = null; |
||||||
|
private ?string $articleAuthor = null; |
||||||
|
private ?string $articlePages = null; |
||||||
|
private ?string $edition = null; |
||||||
|
private ?string $publisher = null; |
||||||
|
|
||||||
|
// Patron fields. |
||||||
|
private ?string $patronId = null; |
||||||
|
private ?string $patronName = null; |
||||||
|
private ?string $patronDepartment = null; |
||||||
|
private ?string $patronEmail = null; |
||||||
|
private ?string $patronPhone = null; |
||||||
|
private ?string $patronNotes = null; |
||||||
|
|
||||||
|
// Cross-reference. |
||||||
|
private ?string $xrefRequestId = null; |
||||||
|
|
||||||
|
public function setRequestType(RequestType $type): static |
||||||
|
{ |
||||||
|
$this->requestType = $type; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setHoldingsCheckOnly(bool $value): static |
||||||
|
{ |
||||||
|
$this->holdingsCheckOnly = $value; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setBlockLocalOnly(bool $value): static |
||||||
|
{ |
||||||
|
$this->blockLocalOnly = $value; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setInsertLocalHolding(bool $value): static |
||||||
|
{ |
||||||
|
$this->insertLocalHolding = $value; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function addIssn(string $issn): static |
||||||
|
{ |
||||||
|
$this->issns[] = $issn; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setIssns(array $issns): static |
||||||
|
{ |
||||||
|
$this->issns = $issns; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function addIsbn(string $isbn): static |
||||||
|
{ |
||||||
|
$this->isbns[] = $isbn; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setIsbns(array $isbns): static |
||||||
|
{ |
||||||
|
$this->isbns = $isbns; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function addLccn(string $lccn): static |
||||||
|
{ |
||||||
|
$this->lccns[] = $lccn; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setLccns(array $lccns): static |
||||||
|
{ |
||||||
|
$this->lccns = $lccns; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setOclcNumber(string $number): static |
||||||
|
{ |
||||||
|
$this->oclcNumber = $number; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setJournalTitle(string $title): static |
||||||
|
{ |
||||||
|
$this->journalTitle = $title; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setJournalYear(string $year): static |
||||||
|
{ |
||||||
|
$this->journalYear = $year; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setJournalVolume(string $volume): static |
||||||
|
{ |
||||||
|
$this->journalVolume = $volume; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setJournalIssue(string $issue): static |
||||||
|
{ |
||||||
|
$this->journalIssue = $issue; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setJournalMonth(string $month): static |
||||||
|
{ |
||||||
|
$this->journalMonth = $month; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setArticleTitle(string $title): static |
||||||
|
{ |
||||||
|
$this->articleTitle = $title; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setArticleAuthor(string $author): static |
||||||
|
{ |
||||||
|
$this->articleAuthor = $author; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setArticlePages(string $pages): static |
||||||
|
{ |
||||||
|
$this->articlePages = $pages; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setEdition(string $edition): static |
||||||
|
{ |
||||||
|
$this->edition = $edition; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPublisher(string $publisher): static |
||||||
|
{ |
||||||
|
$this->publisher = $publisher; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPatronId(string $id): static |
||||||
|
{ |
||||||
|
$this->patronId = $id; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPatronName(string $name): static |
||||||
|
{ |
||||||
|
$this->patronName = $name; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPatronDepartment(string $department): static |
||||||
|
{ |
||||||
|
$this->patronDepartment = $department; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPatronEmail(string $email): static |
||||||
|
{ |
||||||
|
$this->patronEmail = $email; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPatronPhone(string $phone): static |
||||||
|
{ |
||||||
|
$this->patronPhone = $phone; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setPatronNotes(string $notes): static |
||||||
|
{ |
||||||
|
$this->patronNotes = $notes; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setXrefRequestId(string $id): static |
||||||
|
{ |
||||||
|
$this->xrefRequestId = $id; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Convert to the array structure expected by the SOAP client. |
||||||
|
*/ |
||||||
|
public function toSoapArray(): array |
||||||
|
{ |
||||||
|
$data = [ |
||||||
|
'RapidRequestType' => $this->requestType->value, |
||||||
|
'IsHoldingsCheckOnly' => $this->holdingsCheckOnly, |
||||||
|
'DoBlockLocalOnly' => $this->blockLocalOnly, |
||||||
|
'DoInsertLocalHolding' => $this->insertLocalHolding, |
||||||
|
]; |
||||||
|
|
||||||
|
if ($this->issns) { |
||||||
|
$data['SuggestedIssns'] = ['string' => $this->issns]; |
||||||
|
} |
||||||
|
if ($this->isbns) { |
||||||
|
$data['SuggestedIsbns'] = ['string' => $this->isbns]; |
||||||
|
} |
||||||
|
if ($this->lccns) { |
||||||
|
$data['SuggestedLccns'] = ['string' => $this->lccns]; |
||||||
|
} |
||||||
|
|
||||||
|
$optional = [ |
||||||
|
'OclcNumber' => $this->oclcNumber, |
||||||
|
'PatronJournalTitle' => $this->journalTitle, |
||||||
|
'PatronJournalYear' => $this->journalYear, |
||||||
|
'JournalVol' => $this->journalVolume, |
||||||
|
'JournalIssue' => $this->journalIssue, |
||||||
|
'JournalMonth' => $this->journalMonth, |
||||||
|
'ArticleTitle' => $this->articleTitle, |
||||||
|
'ArticleAuthor' => $this->articleAuthor, |
||||||
|
'ArticlePages' => $this->articlePages, |
||||||
|
'Edition' => $this->edition, |
||||||
|
'Publisher' => $this->publisher, |
||||||
|
'PatronId' => $this->patronId, |
||||||
|
'PatronName' => $this->patronName, |
||||||
|
'PatronDepartment' => $this->patronDepartment, |
||||||
|
'PatronEmail' => $this->patronEmail, |
||||||
|
'PatronPhone' => $this->patronPhone, |
||||||
|
'PatronNotes' => $this->patronNotes, |
||||||
|
'XRefRequestId' => $this->xrefRequestId, |
||||||
|
]; |
||||||
|
|
||||||
|
foreach ($optional as $key => $value) { |
||||||
|
if ($value !== null) { |
||||||
|
$data[$key] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace RapidIll; |
||||||
|
|
||||||
|
/** |
||||||
|
* Represents the response from a RapidILL InsertRequest API call. |
||||||
|
*/ |
||||||
|
class InsertResponse |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public readonly bool $isSuccessful, |
||||||
|
public readonly bool $foundMatch, |
||||||
|
public readonly int $rapidRequestId, |
||||||
|
public readonly int $numberOfAvailableHoldings, |
||||||
|
public readonly bool $isLocalHolding, |
||||||
|
public readonly ?string $verificationNote = null, |
||||||
|
public readonly ?string $matchingStandardNumber = null, |
||||||
|
public readonly ?string $matchingStandardNumberType = null, |
||||||
|
public readonly ?string $duplicateRequestId = null, |
||||||
|
public readonly array $localHoldings = [], |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Build an InsertResponse from the raw SOAP response object. |
||||||
|
*/ |
||||||
|
public static function fromSoapResponse(object $result): static |
||||||
|
{ |
||||||
|
$r = $result->InsertRequestResult; |
||||||
|
|
||||||
|
$holdings = []; |
||||||
|
if (isset($r->LocalHoldings->LocalHoldingItem)) { |
||||||
|
$items = $r->LocalHoldings->LocalHoldingItem; |
||||||
|
// Normalize single item to array. |
||||||
|
if (!is_array($items)) { |
||||||
|
$items = [$items]; |
||||||
|
} |
||||||
|
foreach ($items as $item) { |
||||||
|
$holdings[] = [ |
||||||
|
'branchName' => $item->BranchName ?? null, |
||||||
|
'location' => $item->LibLocation ?? null, |
||||||
|
'callNumber' => $item->CallNumber ?? null, |
||||||
|
'redirectUrl' => $item->RapidRedirectUrl ?? null, |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return new static( |
||||||
|
isSuccessful: (bool) ($r->IsSuccessful ?? false), |
||||||
|
foundMatch: (bool) ($r->FoundMatch ?? false), |
||||||
|
rapidRequestId: (int) ($r->RapidRequestId ?? 0), |
||||||
|
numberOfAvailableHoldings: (int) ($r->NumberOfAvailableHoldings ?? 0), |
||||||
|
isLocalHolding: (bool) ($r->IsLocalHolding ?? false), |
||||||
|
verificationNote: $r->VerificationNote ?? null, |
||||||
|
matchingStandardNumber: $r->MatchingStandardNumber ?? null, |
||||||
|
matchingStandardNumberType: $r->MatchingStandardNumberType ?? null, |
||||||
|
duplicateRequestId: $r->DuplicateRequestId ?? null, |
||||||
|
localHoldings: $holdings, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,123 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace RapidIll; |
||||||
|
|
||||||
|
/** |
||||||
|
* Client for the RapidILL SOAP API. |
||||||
|
* |
||||||
|
* Usage: |
||||||
|
* $client = new RapidIllClient( |
||||||
|
* username: 'user', |
||||||
|
* password: 'pass', |
||||||
|
* rapidCode: 'YOUR_CODE', |
||||||
|
* branchName: 'Main', |
||||||
|
* ); |
||||||
|
* |
||||||
|
* $request = (new InsertRequest()) |
||||||
|
* ->setRequestType(RequestType::Article) |
||||||
|
* ->setJournalTitle('Nature') |
||||||
|
* ->setArticleTitle('Example Article') |
||||||
|
* ->setArticleAuthor('Smith, J.') |
||||||
|
* ->addIssn('0028-0836'); |
||||||
|
* |
||||||
|
* $response = $client->insertRequest($request); |
||||||
|
*/ |
||||||
|
class RapidIllClient |
||||||
|
{ |
||||||
|
private const WSDL_URL = 'https://rapid.exlibrisgroup.com/rapid5api/apiservice.asmx?wsdl'; |
||||||
|
|
||||||
|
private ?\SoapClient $soapClient = null; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
private readonly string $username, |
||||||
|
private readonly string $password, |
||||||
|
private readonly string $rapidCode, |
||||||
|
private readonly string $branchName, |
||||||
|
private readonly ?string $wsdlUrl = null, |
||||||
|
private readonly array $soapOptions = [], |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Submit an ILL request to RapidILL. |
||||||
|
* |
||||||
|
* @throws RapidIllException |
||||||
|
*/ |
||||||
|
public function insertRequest(InsertRequest $request): InsertResponse |
||||||
|
{ |
||||||
|
$params = $request->toSoapArray(); |
||||||
|
|
||||||
|
// Merge authentication fields. |
||||||
|
$params['UserName'] = $this->username; |
||||||
|
$params['Password'] = $this->password; |
||||||
|
$params['RequestingRapidCode'] = $this->rapidCode; |
||||||
|
$params['RequestingBranchName'] = $this->branchName; |
||||||
|
|
||||||
|
try { |
||||||
|
$result = $this->getSoapClient()->InsertRequest(['req' => $params]); |
||||||
|
return InsertResponse::fromSoapResponse($result); |
||||||
|
} catch (\SoapFault $e) { |
||||||
|
throw new RapidIllException( |
||||||
|
'RapidILL API error: ' . $e->getMessage(), |
||||||
|
(int) $e->getCode(), |
||||||
|
$e, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get or create the SOAP client instance. |
||||||
|
* |
||||||
|
* @throws RapidIllException |
||||||
|
*/ |
||||||
|
private function getSoapClient(): \SoapClient |
||||||
|
{ |
||||||
|
if ($this->soapClient !== null) { |
||||||
|
return $this->soapClient; |
||||||
|
} |
||||||
|
|
||||||
|
$wsdl = $this->wsdlUrl ?? self::WSDL_URL; |
||||||
|
|
||||||
|
$options = array_merge([ |
||||||
|
'trace' => true, |
||||||
|
'exceptions' => true, |
||||||
|
'cache_wsdl' => WSDL_CACHE_BOTH, |
||||||
|
], $this->soapOptions); |
||||||
|
|
||||||
|
try { |
||||||
|
$this->soapClient = new \SoapClient($wsdl, $options); |
||||||
|
} catch (\SoapFault $e) { |
||||||
|
throw new RapidIllException( |
||||||
|
'Failed to initialize RapidILL SOAP client: ' . $e->getMessage(), |
||||||
|
(int) $e->getCode(), |
||||||
|
$e, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->soapClient; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Replace the SOAP client (useful for testing). |
||||||
|
*/ |
||||||
|
public function setSoapClient(\SoapClient $client): void |
||||||
|
{ |
||||||
|
$this->soapClient = $client; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the last SOAP request XML (requires trace=true). |
||||||
|
*/ |
||||||
|
public function getLastRequest(): ?string |
||||||
|
{ |
||||||
|
return $this->soapClient?->__getLastRequest(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the last SOAP response XML (requires trace=true). |
||||||
|
*/ |
||||||
|
public function getLastResponse(): ?string |
||||||
|
{ |
||||||
|
return $this->soapClient?->__getLastResponse(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace RapidIll; |
||||||
|
|
||||||
|
class RapidIllException extends \RuntimeException |
||||||
|
{ |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace RapidIll; |
||||||
|
|
||||||
|
/** |
||||||
|
* RapidILL request types. |
||||||
|
*/ |
||||||
|
enum RequestType: string |
||||||
|
{ |
||||||
|
case Article = 'Article'; |
||||||
|
case Book = 'Book'; |
||||||
|
case BookChapter = 'BookChapter'; |
||||||
|
} |
||||||
@ -0,0 +1,160 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace RapidIll\Tests; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use RapidIll\InsertRequest; |
||||||
|
use RapidIll\InsertResponse; |
||||||
|
use RapidIll\RapidIllClient; |
||||||
|
use RapidIll\RapidIllException; |
||||||
|
use RapidIll\RequestType; |
||||||
|
|
||||||
|
class RapidIllClientTest extends TestCase |
||||||
|
{ |
||||||
|
public function testInsertRequestBuildsCorrectSoapArray(): void |
||||||
|
{ |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::Article) |
||||||
|
->setJournalTitle('Nature') |
||||||
|
->setJournalYear('2024') |
||||||
|
->setJournalVolume('625') |
||||||
|
->setJournalIssue('7994') |
||||||
|
->setArticleTitle('A breakthrough in quantum computing') |
||||||
|
->setArticleAuthor('Smith, J.') |
||||||
|
->setArticlePages('100-105') |
||||||
|
->addIssn('0028-0836') |
||||||
|
->setPatronName('Jane Doe') |
||||||
|
->setPatronEmail('jane@example.edu'); |
||||||
|
|
||||||
|
$data = $request->toSoapArray(); |
||||||
|
|
||||||
|
$this->assertSame('Article', $data['RapidRequestType']); |
||||||
|
$this->assertFalse($data['IsHoldingsCheckOnly']); |
||||||
|
$this->assertSame('Nature', $data['PatronJournalTitle']); |
||||||
|
$this->assertSame('2024', $data['PatronJournalYear']); |
||||||
|
$this->assertSame('625', $data['JournalVol']); |
||||||
|
$this->assertSame('7994', $data['JournalIssue']); |
||||||
|
$this->assertSame('A breakthrough in quantum computing', $data['ArticleTitle']); |
||||||
|
$this->assertSame('Smith, J.', $data['ArticleAuthor']); |
||||||
|
$this->assertSame('100-105', $data['ArticlePages']); |
||||||
|
$this->assertSame(['string' => ['0028-0836']], $data['SuggestedIssns']); |
||||||
|
$this->assertSame('Jane Doe', $data['PatronName']); |
||||||
|
$this->assertSame('jane@example.edu', $data['PatronEmail']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInsertRequestOmitsNullFields(): void |
||||||
|
{ |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setJournalTitle('Science'); |
||||||
|
|
||||||
|
$data = $request->toSoapArray(); |
||||||
|
|
||||||
|
$this->assertArrayHasKey('PatronJournalTitle', $data); |
||||||
|
$this->assertArrayNotHasKey('ArticleTitle', $data); |
||||||
|
$this->assertArrayNotHasKey('PatronEmail', $data); |
||||||
|
$this->assertArrayNotHasKey('SuggestedIssns', $data); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInsertRequestMultipleIdentifiers(): void |
||||||
|
{ |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->addIssn('0028-0836') |
||||||
|
->addIssn('1476-4687') |
||||||
|
->addIsbn('978-0-123456-78-9') |
||||||
|
->setOclcNumber('12345678'); |
||||||
|
|
||||||
|
$data = $request->toSoapArray(); |
||||||
|
|
||||||
|
$this->assertSame(['string' => ['0028-0836', '1476-4687']], $data['SuggestedIssns']); |
||||||
|
$this->assertSame(['string' => ['978-0-123456-78-9']], $data['SuggestedIsbns']); |
||||||
|
$this->assertSame('12345678', $data['OclcNumber']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInsertResponseFromSoapResponse(): void |
||||||
|
{ |
||||||
|
$soapResult = (object) [ |
||||||
|
'InsertRequestResult' => (object) [ |
||||||
|
'IsSuccessful' => true, |
||||||
|
'FoundMatch' => true, |
||||||
|
'RapidRequestId' => 99999, |
||||||
|
'NumberOfAvailableHoldings' => 3, |
||||||
|
'IsLocalHolding' => false, |
||||||
|
'VerificationNote' => null, |
||||||
|
'MatchingStandardNumber' => '0028-0836', |
||||||
|
'MatchingStandardNumberType' => 'ISSN', |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
$response = InsertResponse::fromSoapResponse($soapResult); |
||||||
|
|
||||||
|
$this->assertTrue($response->isSuccessful); |
||||||
|
$this->assertTrue($response->foundMatch); |
||||||
|
$this->assertSame(99999, $response->rapidRequestId); |
||||||
|
$this->assertSame(3, $response->numberOfAvailableHoldings); |
||||||
|
$this->assertFalse($response->isLocalHolding); |
||||||
|
$this->assertSame('0028-0836', $response->matchingStandardNumber); |
||||||
|
$this->assertSame('ISSN', $response->matchingStandardNumberType); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInsertResponseWithLocalHoldings(): void |
||||||
|
{ |
||||||
|
$soapResult = (object) [ |
||||||
|
'InsertRequestResult' => (object) [ |
||||||
|
'IsSuccessful' => true, |
||||||
|
'FoundMatch' => true, |
||||||
|
'RapidRequestId' => 12345, |
||||||
|
'NumberOfAvailableHoldings' => 1, |
||||||
|
'IsLocalHolding' => true, |
||||||
|
'LocalHoldings' => (object) [ |
||||||
|
'LocalHoldingItem' => (object) [ |
||||||
|
'BranchName' => 'Main Library', |
||||||
|
'LibLocation' => 'Stacks', |
||||||
|
'CallNumber' => 'Q1 .N2', |
||||||
|
'RapidRedirectUrl' => 'https://example.com/redirect', |
||||||
|
], |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
$response = InsertResponse::fromSoapResponse($soapResult); |
||||||
|
|
||||||
|
$this->assertTrue($response->isLocalHolding); |
||||||
|
$this->assertCount(1, $response->localHoldings); |
||||||
|
$this->assertSame('Main Library', $response->localHoldings[0]['branchName']); |
||||||
|
$this->assertSame('Q1 .N2', $response->localHoldings[0]['callNumber']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testClientWrapsSOAPFaultInException(): void |
||||||
|
{ |
||||||
|
$mockSoap = $this->createMock(\SoapClient::class); |
||||||
|
$mockSoap->method('__call') |
||||||
|
->willThrowException(new \SoapFault('Server', 'Authentication failed')); |
||||||
|
|
||||||
|
$client = new RapidIllClient('user', 'pass', 'CODE', 'Main'); |
||||||
|
$client->setSoapClient($mockSoap); |
||||||
|
|
||||||
|
$this->expectException(RapidIllException::class); |
||||||
|
$this->expectExceptionMessage('RapidILL API error: Authentication failed'); |
||||||
|
|
||||||
|
$client->insertRequest(new InsertRequest()); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBookRequest(): void |
||||||
|
{ |
||||||
|
$request = (new InsertRequest()) |
||||||
|
->setRequestType(RequestType::Book) |
||||||
|
->setJournalTitle('Introduction to Algorithms') |
||||||
|
->addIsbn('978-0262033848') |
||||||
|
->setArticleAuthor('Cormen, T.H.') |
||||||
|
->setPublisher('MIT Press') |
||||||
|
->setEdition('3rd') |
||||||
|
->setPatronName('John Smith'); |
||||||
|
|
||||||
|
$data = $request->toSoapArray(); |
||||||
|
|
||||||
|
$this->assertSame('Book', $data['RapidRequestType']); |
||||||
|
$this->assertSame('Introduction to Algorithms', $data['PatronJournalTitle']); |
||||||
|
$this->assertSame('MIT Press', $data['Publisher']); |
||||||
|
$this->assertSame('3rd', $data['Edition']); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue