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